19 Commits

Author SHA1 Message Date
531a02a6e1 feat(ci): add compile steps to CI workflow
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
CI / build (push) Successful in 15s
- Introduces compile steps for amd64 and arm64 targets in CI workflow
- Updates failure condition to include compile step outcomes
- Enhances local CI task to include amd64 build process
2025-05-30 12:24:10 +02:00
f3f2c61da0 fix(build): update included files to use .jsonc format 2025-05-30 12:23:51 +02:00
32d472a606 chore(pr): Support JSONC locale files and update i18n handling #4
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
Auto Changelog & Release / release (push) Has been skipped
CI / build (push) Successful in 7s
Auto Changelog & Release / changelog-only (push) Successful in 5s
- Add `@std/jsonc` as a dependency in `deno.jsonc` and `deno.lock`
- Update i18n loader to support `.jsonc` files with comments
- Rename locale files from `.json` to `.jsonc` and add inline comments
- Set VSCode to use Deno JSONC formatter for `jsonc` files

This change enables the i18n module to load translation files written in JSONC format, allowing for inline comments and improved readability. The `@std/jsonc` library is added as a dependency and used to parse both JSON and JSONC files. The loader prioritizes `.jsonc` files when both `.json` and `.jsonc` exist. VSCode settings are updated to use the Deno formatter for JSONC files. Locale files are renamed and enhanced with comments for clarity.

Merged from feature/change-language-json-to-jsonc into main
2025-05-30 11:46:33 +02:00
c6d5bf60fc chore(changelog): update unreleased changelog
All checks were successful
CI / build (pull_request) Successful in 7s
2025-05-30 09:45:37 +00:00
c7af1fb6ca feat(vscode): add JSONC formatter configuration
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
2025-05-30 11:45:15 +02:00
5226269ec2 style(i18n): add comments for clarity and rename files
- Adds inline comments to JSON files to improve readability
- Renames `.json` files to `.jsonc` to support comments
- Improves organization of translation entries
2025-05-30 11:45:00 +02:00
4ac5dd4c88 feat(i18n): support loading JSONC translation files
- Adds support for `.jsonc` format alongside `.json` for locale files
- Prioritizes `.jsonc` format when both file types are available
2025-05-30 11:44:09 +02:00
8f1cb3fad7 feat(config): add @std/jsonc dependency
- Adds @std/jsonc dependency to improve JSONC handling capabilities
- Updates lock file to include integrity and dependency data for @std/jsonc
2025-05-30 11:43:46 +02:00
f4c7b2e18f chore(pr): add rollback tests and update test descriptions #3
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
CI / build (push) Successful in 6s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 5s
- Add tests for rollback behavior in `writeUnitFiles` on errors
- Simulate file write and delete failures in test scenarios
- Log rollback failures when file deletion is not possible
- Update all test descriptions and comments to English
- Add `testing/mock.ts` dependency in `deno.lock`

This change adds comprehensive tests for the `writeUnitFiles` function to verify that files are properly rolled back if errors occur during their creation. The tests simulate failures when writing `.service` or `.timer` files, as well as when file deletion is blocked, and check that the function responds by cleaning up as expected or logging rollback failures. All test comments and descriptions have been updated to English for clarity. The `testing/mock.ts` dependency was added to enable function stubbing in tests.

#2

Merged from test/fail-write-and-rollback into main
2025-05-30 11:21:41 +02:00
84a883fd8f chore(changelog): update unreleased changelog 2025-05-30 09:21:20 +00:00
e5f9f2c45a chore(changelog): update unreleased changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
Auto Changelog & Release / release (push) Has been skipped
CI / build (pull_request) Successful in 6s
Auto Changelog & Release / changelog-only (push) Successful in 7s
2025-05-30 11:21:10 +02:00
f3c46e1222 chore(deps): update deno.lock with new dependency
- Adds a new dependency to deno.lock for testing utilities
2025-05-30 11:21:10 +02:00
c4f4614a2d test(fs): update test descriptions and comments to English
- Translate all test descriptions and inline comments from German to English
2025-05-30 11:21:10 +02:00
6039d236eb test(fs): add rollback tests for writeUnitFiles errors
- Add tests to verify rollback behavior when file writing fails
- Simulate various error scenarios to ensure proper cleanup
- Log rollback failures when file deletion is prohibited
2025-05-30 11:21:10 +02:00
333341d3fd chore(pr): add default merge message template
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
CI / build (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Introduces a template for merge messages in pull requests
- Standardizes format with placeholders for PR details
2025-05-30 11:20:46 +02:00
38afcf210e chore(changelog): update changelog for v0.4.1
All checks were successful
Upload Assets / upload-assets (amd64, linux) (release) Successful in 28s
Upload Assets / upload-assets (arm64, linux) (release) Successful in 29s
2025-05-28 16:34:22 +00:00
28b18dc994 chore(version): bump version to 0.4.1
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Has been skipped
CI / build (push) Successful in 7s
Auto Changelog & Release / release (push) Successful in 7s
2025-05-28 18:34:09 +02:00
ede012317b chore(changelog): update unreleased changelog 2025-05-28 16:34:01 +00:00
a22c156dd3 fix(tasks): add read permissions to build scripts
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
CI / build (push) Successful in 6s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Allow read permissions in build scripts for both amd64 and arm64 targets
- Ensure compatibility with files required during the build process
2025-05-28 18:33:47 +02:00
11 changed files with 319 additions and 29 deletions

View File

@@ -0,0 +1,5 @@
chore(pr): ${PullRequestTitle} ${PullRequestReference}
${PullRequestDescription}
Merged from ${HeadBranch} into ${BaseBranch}

View File

@@ -36,11 +36,19 @@ jobs:
continue-on-error: true continue-on-error: true
run: deno task test run: deno task test
- name: Compile
id: compile
continue-on-error: true
run: |
deno task build:amd64
deno task build:arm64
- name: Fail if any step failed - name: Fail if any step failed
if: | if: |
steps.format.outcome != 'success' || steps.format.outcome != 'success' ||
steps.lint.outcome != 'success' || steps.lint.outcome != 'success' ||
steps.test.outcome != 'success' steps.test.outcome != 'success' ||
steps.compile.outcome != 'success'
run: | run: |
echo "::error::One or more steps failed" echo "::error::One or more steps failed"
exit 1 exit 1

View File

@@ -17,5 +17,8 @@
"[json]": { "[json]": {
"editor.defaultFormatter": "denoland.vscode-deno" "editor.defaultFormatter": "denoland.vscode-deno"
}, },
"[jsonc]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"editor.formatOnSave": true "editor.formatOnSave": true
} }

View File

@@ -2,6 +2,29 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [unreleased]
### 🚀 Features
- *(vscode)* Add JSONC formatter configuration - ([c7af1fb](https://git.0xmax42.io/maxp/systemd-timer/commit/c7af1fb6caa46c22b84229745067d05bf60b6f64))
- *(i18n)* Support loading JSONC translation files - ([4ac5dd4](https://git.0xmax42.io/maxp/systemd-timer/commit/4ac5dd4c88324f99cb6827283ad85bb9718abbeb))
- *(config)* Add @std/jsonc dependency - ([8f1cb3f](https://git.0xmax42.io/maxp/systemd-timer/commit/8f1cb3fad71ead365d93087963ddb6c7202a9b4f))
### 🎨 Styling
- *(i18n)* Add comments for clarity and rename files - ([5226269](https://git.0xmax42.io/maxp/systemd-timer/commit/5226269ec2a0b76dfa30ac8d614c3789ff3a837b))
### 🧪 Testing
- *(fs)* Update test descriptions and comments to English - ([c4f4614](https://git.0xmax42.io/maxp/systemd-timer/commit/c4f4614a2daee68f9b33b9676106214c65a1a427))
- *(fs)* Add rollback tests for writeUnitFiles errors - ([6039d23](https://git.0xmax42.io/maxp/systemd-timer/commit/6039d236eb7de449ce22b1d9ea718389a3e2261d))
## [0.4.1](https://git.0xmax42.io/maxp/systemd-timer/compare/v0.4.0..v0.4.1) - 2025-05-28
### 🐛 Bug Fixes
- *(tasks)* Add read permissions to build scripts - ([a22c156](https://git.0xmax42.io/maxp/systemd-timer/commit/a22c156dd3d2cf4a24f0eed699f7dfabfae3837a))
## [0.4.0](https://git.0xmax42.io/maxp/systemd-timer/compare/v0.3.1..v0.4.0) - 2025-05-28 ## [0.4.0](https://git.0xmax42.io/maxp/systemd-timer/compare/v0.3.1..v0.4.0) - 2025-05-28
### 🚀 Features ### 🚀 Features

View File

@@ -1 +1 @@
0.4.0 0.4.1

View File

@@ -4,9 +4,9 @@
"test": "deno test -A --coverage **/__tests__/*.test.ts", "test": "deno test -A --coverage **/__tests__/*.test.ts",
"fmt": "deno fmt --check", "fmt": "deno fmt --check",
"lint": "deno lint", "lint": "deno lint",
"ci": "deno task fmt && deno task lint && deno task test", // For local CI checks "ci": "deno task fmt && deno task lint && deno task test && build:amd64", // For local CI checks
"build:amd64": "deno compile --target x86_64-unknown-linux-gnu --include VERSION --include src/i18n/de.json --include src/i18n/en.json --allow-env --allow-write --output dist/systemd-timer-linux-amd64 src/mod.ts", "build:amd64": "deno compile --target x86_64-unknown-linux-gnu --include VERSION --include src/i18n/de.jsonc --include src/i18n/en.jsonc --allow-env --allow-write --allow-read --output dist/systemd-timer-linux-amd64 src/mod.ts",
"build:arm64": "deno compile --target aarch64-unknown-linux-gnu --include VERSION --include src/i18n/de.json --include src/i18n/en.json --allow-env --allow-write --output dist/systemd-timer-linux-arm64 src/mod.ts" "build:arm64": "deno compile --target aarch64-unknown-linux-gnu --include VERSION --include src/i18n/de.jsonc --include src/i18n/en.jsonc --allow-env --allow-write --allow-read --output dist/systemd-timer-linux-arm64 src/mod.ts"
}, },
"compilerOptions": {}, "compilerOptions": {},
"fmt": { "fmt": {
@@ -23,6 +23,7 @@
}, },
"exclude": [], "exclude": [],
"imports": { "imports": {
"@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7" "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7",
"@std/jsonc": "jsr:@std/jsonc@^1.0.2"
} }
} }

17
deno.lock generated
View File

@@ -6,6 +6,8 @@
"jsr:@cliffy/internal@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/internal@1.0.0-rc.7": "1.0.0-rc.7",
"jsr:@cliffy/table@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/table@1.0.0-rc.7": "1.0.0-rc.7",
"jsr:@std/fmt@~1.0.2": "1.0.7", "jsr:@std/fmt@~1.0.2": "1.0.7",
"jsr:@std/json@^1.0.2": "1.0.2",
"jsr:@std/jsonc@^1.0.2": "1.0.2",
"jsr:@std/text@~1.0.7": "1.0.13" "jsr:@std/text@~1.0.7": "1.0.13"
}, },
"jsr": { "jsr": {
@@ -37,6 +39,15 @@
"@std/fmt@1.0.7": { "@std/fmt@1.0.7": {
"integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb" "integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb"
}, },
"@std/json@1.0.2": {
"integrity": "d9e5497801c15fb679f55a2c01c7794ad7a5dfda4dd1bebab5e409cb5e0d34d4"
},
"@std/jsonc@1.0.2": {
"integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7",
"dependencies": [
"jsr:@std/json"
]
},
"@std/text@1.0.13": { "@std/text@1.0.13": {
"integrity": "2191c90e6e667b0c3b7dea1cd082137580a93b3c136bad597c0212d5fe006eb1" "integrity": "2191c90e6e667b0c3b7dea1cd082137580a93b3c136bad597c0212d5fe006eb1"
} }
@@ -168,11 +179,13 @@
"https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7",
"https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c" "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
"https://deno.land/std@0.224.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db"
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@cliffy/command@1.0.0-rc.7" "jsr:@cliffy/command@1.0.0-rc.7",
"jsr:@std/jsonc@^1.0.2"
] ]
} }
} }

View File

@@ -1,6 +1,8 @@
{ {
// General
"cli_description": "CLI-Tool zum Erzeugen von systemd .timer und .service Units", "cli_description": "CLI-Tool zum Erzeugen von systemd .timer und .service Units",
"cli_create_description": "Erzeugt eine systemd .service und .timer Unit", "cli_create_description": "Erzeugt eine systemd .service und .timer Unit",
// Options
"option_name": "Name der Unit-Dateien (optional, wird sonst aus dem Exec generiert)", "option_name": "Name der Unit-Dateien (optional, wird sonst aus dem Exec generiert)",
"option_exec": "Kommando, das durch systemd ausgeführt werden soll", "option_exec": "Kommando, das durch systemd ausgeführt werden soll",
"option_calendar": "OnCalendar-Ausdruck für den Timer", "option_calendar": "OnCalendar-Ausdruck für den Timer",
@@ -14,9 +16,11 @@
"option_environment": "Environment-Variablen im Format KEY=VALUE", "option_environment": "Environment-Variablen im Format KEY=VALUE",
"option_logfile": "Dateipfad für Log-Ausgabe (stdout/stderr)", "option_logfile": "Dateipfad für Log-Ausgabe (stdout/stderr)",
"option_dry_run": "Gibt die Unit-Dateien nur aus, ohne sie zu schreiben", "option_dry_run": "Gibt die Unit-Dateien nur aus, ohne sie zu schreiben",
// Messages
"unit_written_service": "Service Unit geschrieben in: {path}", "unit_written_service": "Service Unit geschrieben in: {path}",
"unit_written_timer": "Timer Unit geschrieben in: {path}", "unit_written_timer": "Timer Unit geschrieben in: {path}",
"hint_header": "\nℹ️ Hinweis:", "hint_header": "\nℹ️ Hinweis:",
// Error messages
"error_write_units": "Fehler beim Schreiben der Units:", "error_write_units": "Fehler beim Schreiben der Units:",
"rollback_failed": "Rollback fehlgeschlagen:" "rollback_failed": "Rollback fehlgeschlagen:"
} }

View File

@@ -1,6 +1,8 @@
{ {
// General
"cli_description": "CLI tool for generating systemd .timer and .service units", "cli_description": "CLI tool for generating systemd .timer and .service units",
"cli_create_description": "Generates a systemd .service and .timer unit", "cli_create_description": "Generates a systemd .service and .timer unit",
// Options
"option_name": "Name of the unit files (optional, otherwise derived from the exec command)", "option_name": "Name of the unit files (optional, otherwise derived from the exec command)",
"option_exec": "Command to be executed by systemd", "option_exec": "Command to be executed by systemd",
"option_calendar": "OnCalendar expression for the timer", "option_calendar": "OnCalendar expression for the timer",
@@ -14,9 +16,11 @@
"option_environment": "Environment variables in the format KEY=VALUE", "option_environment": "Environment variables in the format KEY=VALUE",
"option_logfile": "File path for log output (stdout/stderr)", "option_logfile": "File path for log output (stdout/stderr)",
"option_dry_run": "Only outputs the unit files without writing them", "option_dry_run": "Only outputs the unit files without writing them",
// Messages
"unit_written_service": "Service unit written to: {path}", "unit_written_service": "Service unit written to: {path}",
"unit_written_timer": "Timer unit written to: {path}", "unit_written_timer": "Timer unit written to: {path}",
"hint_header": "\nℹ️ Note:", "hint_header": "\nℹ️ Note:",
// Error messages
"error_write_units": "Error while writing unit files:", "error_write_units": "Error while writing unit files:",
"rollback_failed": "Rollback failed:" "rollback_failed": "Rollback failed:"
} }

View File

@@ -1,3 +1,5 @@
import { parse as parseJsonc } from '@std/jsonc';
/** /**
* Initializes the i18n module by loading * Initializes the i18n module by loading
* the appropriate locale file based on the system language. * the appropriate locale file based on the system language.
@@ -15,29 +17,35 @@ let translations: Record<string, string> = {};
/** /**
* Loads the translation file for the specified locale. * Loads the translation file for the specified locale.
* *
* Expects a JSON file in the same directory named like `de.json` or `en.json`. * Accepts both `.jsonc` (JSON with comments) and plain `.json`.
* When both exist, `.jsonc` takes precedence.
* Falls back to English ('en') if the specified file does not exist. * Falls back to English ('en') if the specified file does not exist.
* *
* @param locale - The language code (e.g., 'de', 'en') to load * @param locale - The language code (e.g., 'de', 'en') to load
* @returns Promise that resolves once the translations have been loaded * @returns Promise that resolves once the translations have been loaded
*/ */
export async function loadLocale(locale: string): Promise<void> { export async function loadLocale(locale: string): Promise<void> {
const extensions = ['jsonc', 'json'];
for (const ext of extensions) {
try { try {
const localeUrl = new URL(`./${locale}.json`, import.meta.url); const localeUrl = new URL(`./${locale}.${ext}`, import.meta.url);
const file = await Deno.readTextFile(localeUrl); const raw = await Deno.readTextFile(localeUrl);
translations = JSON.parse(file); // parseJsonc tolerates both pure JSON and JSONC, so we can use it for either.
translations = parseJsonc(raw) as Record<string, string>;
return;
} catch (err) { } catch (err) {
if (err instanceof Deno.errors.NotFound) { if (err instanceof Deno.errors.NotFound) {
console.warn( // Continue with next extension.
`Locale '${locale}' not found – falling back to 'en'.`, continue;
); }
console.error(`Error parsing locale '${locale}.${ext}':`, err);
break;
}
}
if (locale !== 'en') { if (locale !== 'en') {
console.warn(`Locale '${locale}' not found – falling back to 'en'.`);
await loadLocale('en'); await loadLocale('en');
} }
} else {
console.error('Error loading translation file:', err);
}
}
} }
/** /**

View File

@@ -3,11 +3,13 @@ import {
assertExists, assertExists,
assertStringIncludes, assertStringIncludes,
} from 'https://deno.land/std@0.224.0/assert/mod.ts'; } from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { stub } from 'https://deno.land/std@0.224.0/testing/mock.ts';
import { exists } from 'https://deno.land/std@0.224.0/fs/mod.ts';
import { join } from 'https://deno.land/std@0.224.0/path/mod.ts'; import { join } from 'https://deno.land/std@0.224.0/path/mod.ts';
import { resolveUnitTargetPath, writeUnitFiles } from '../mod.ts'; import { resolveUnitTargetPath, writeUnitFiles } from '../mod.ts';
import { TimerOptions } from '../../types/options.ts'; import { TimerOptions } from '../../types/options.ts';
Deno.test('writeUnitFiles schreibt .service und .timer korrekt', async () => { Deno.test('writeUnitFiles: writes .service and .timer files correctly', async () => {
const tmp = await Deno.makeTempDir({ prefix: 'test-units-' }); const tmp = await Deno.makeTempDir({ prefix: 'test-units-' });
const options = { const options = {
@@ -26,15 +28,15 @@ Deno.test('writeUnitFiles schreibt .service und .timer korrekt', async () => {
options as TimerOptions, options as TimerOptions,
) as { servicePath: string; timerPath: string }; ) as { servicePath: string; timerPath: string };
// Überprüfe Pfade // Check file paths
assertEquals(servicePath, join(tmp, 'test-backup.service')); assertEquals(servicePath, join(tmp, 'test-backup.service'));
assertEquals(timerPath, join(tmp, 'test-backup.timer')); assertEquals(timerPath, join(tmp, 'test-backup.timer'));
// Existieren Dateien? // Check if files exist
assertExists(await Deno.stat(servicePath)); assertExists(await Deno.stat(servicePath));
assertExists(await Deno.stat(timerPath)); assertExists(await Deno.stat(timerPath));
// Enthält die Datei den erwarteten Inhalt? // Check if file contents match expectations
const readService = await Deno.readTextFile(servicePath); const readService = await Deno.readTextFile(servicePath);
const readTimer = await Deno.readTextFile(timerPath); const readTimer = await Deno.readTextFile(timerPath);
@@ -42,18 +44,237 @@ Deno.test('writeUnitFiles schreibt .service und .timer korrekt', async () => {
assertStringIncludes(readTimer, 'OnCalendar=daily'); assertStringIncludes(readTimer, 'OnCalendar=daily');
}); });
Deno.test('resolveUnitTargetPath mit --output', () => { Deno.test('resolveUnitTargetPath: with --output', () => {
const result = resolveUnitTargetPath({ output: '/tmp/units', user: false }); const result = resolveUnitTargetPath({ output: '/tmp/units', user: false });
assertEquals(result, '/tmp/units'); assertEquals(result, '/tmp/units');
}); });
Deno.test('resolveUnitTargetPath mit --user ohne output', () => { Deno.test('resolveUnitTargetPath: with --user and no output', () => {
Deno.env.set('HOME', '/home/maxp'); Deno.env.set('HOME', '/home/maxp');
const result = resolveUnitTargetPath({ output: undefined, user: true }); const result = resolveUnitTargetPath({ output: undefined, user: true });
assertEquals(result, '/home/maxp/.config/systemd/user'); assertEquals(result, '/home/maxp/.config/systemd/user');
}); });
Deno.test('resolveUnitTargetPath ohne output und ohne user', () => { Deno.test('resolveUnitTargetPath: with no output and no user', () => {
const result = resolveUnitTargetPath({ output: undefined, user: false }); const result = resolveUnitTargetPath({ output: undefined, user: false });
assertEquals(result, '/etc/systemd/system'); assertEquals(result, '/etc/systemd/system');
}); });
Deno.test('writeUnitFiles: error writing .timer file triggers rollback', async () => {
const tmp = await Deno.makeTempDir();
const options: TimerOptions = {
output: tmp,
user: false,
exec: '/bin/true',
calendar: 'never',
};
const name = 'fail-timer';
const serviceContent = '[Service]\nExecStart=/bin/true';
const timerContent = '[Timer]\nOnCalendar=never';
const servicePath = join(tmp, `${name}.service`);
const timerPath = join(tmp, `${name}.timer`);
// Simulate: writing the .timer file fails
const originalWrite = Deno.writeTextFile;
const writeStub = stub(
Deno,
'writeTextFile',
async (path: string | URL, data: string | ReadableStream<string>) => {
if (typeof path === 'string' && path.endsWith('.timer')) {
throw new Error('Simulated write error');
} else {
return await originalWrite(path, data);
}
},
);
const result = await writeUnitFiles(
name,
serviceContent,
timerContent,
options,
);
// Expect: function returns undefined
assertEquals(result, undefined);
// Expect: both files have been deleted (rollback)
assertEquals(await exists(servicePath), false);
assertEquals(await exists(timerPath), false);
writeStub.restore();
});
Deno.test('writeUnitFiles: error writing .service file prevents further actions', async () => {
const tmp = await Deno.makeTempDir();
const options: TimerOptions = {
output: tmp,
user: false,
exec: '/bin/true',
calendar: 'weekly',
};
const name = 'fail-service';
const serviceContent = '[Service]\nExecStart=/bin/true';
const timerContent = '[Timer]\nOnCalendar=weekly';
const servicePath = join(tmp, `${name}.service`);
const timerPath = join(tmp, `${name}.timer`);
// Simulate: error writing the .service file
const writeStub = stub(
Deno,
'writeTextFile',
(path: string | URL, _data: string | ReadableStream<string>) => {
if (typeof path === 'string' && path.endsWith('.service')) {
throw new Error('Simulated service write error');
}
return Promise.resolve();
},
);
const result = await writeUnitFiles(
name,
serviceContent,
timerContent,
options,
);
// Expect: function returns undefined
assertEquals(result, undefined);
// Expect: no files were created
assertEquals(await exists(servicePath), false);
assertEquals(await exists(timerPath), false);
writeStub.restore();
});
Deno.test('writeUnitFiles: both files written, then error → full rollback', async () => {
const tmp = await Deno.makeTempDir();
const options: TimerOptions = {
output: tmp,
user: false,
exec: '/bin/true',
calendar: 'never',
};
const name = 'fail-after-write';
const serviceContent = '[Service]\nExecStart=/bin/true';
const timerContent = '[Timer]\nOnCalendar=never';
const servicePath = join(tmp, `${name}.service`);
const timerPath = join(tmp, `${name}.timer`);
let writeCount = 0;
const originalWriteTextFile = Deno.writeTextFile;
const writeStub = stub(
Deno,
'writeTextFile',
async (path: string | URL, data: string | ReadableStream<string>) => {
if (typeof path !== 'string') {
throw new Error('Unexpected path type');
}
// Simulate both writes, then throw an error after the second
writeCount++;
await originalWriteTextFile(path, data);
if (writeCount === 2) {
throw new Error('Simulated error after full write');
}
},
);
const result = await writeUnitFiles(
name,
serviceContent,
timerContent,
options,
);
// Expect: function returns undefined
assertEquals(result, undefined);
// Expect: both files were removed
assertEquals(await exists(servicePath), false);
assertEquals(await exists(timerPath), false);
writeStub.restore();
});
Deno.test('writeUnitFiles: rollback fails if files cannot be deleted', async () => {
const tmp = await Deno.makeTempDir();
const options: TimerOptions = {
output: tmp,
user: false,
exec: '/bin/true',
calendar: 'never',
};
const name = 'rollback-error';
const serviceContent = '[Service]\nExecStart=/bin/true';
const timerContent = '[Timer]\nOnCalendar=never';
const servicePath = join(tmp, `${name}.service`);
const timerPath = join(tmp, `${name}.timer`);
const originalWriteTextFile = Deno.writeTextFile;
const _originalRemove = Deno.remove;
let writeCount = 0;
const writeStub = stub(
Deno,
'writeTextFile',
async (path: string | URL, data: string | ReadableStream<string>) => {
writeCount++;
await originalWriteTextFile(path, data);
if (writeCount === 2) {
throw new Error('Error after full write');
}
},
);
const removeStub = stub(
Deno,
'remove',
// deno-lint-ignore require-await
async (_path: string | URL, _opts?: Deno.RemoveOptions) => {
throw new Error('Deletion forbidden!');
},
);
const logs: string[] = [];
const consoleStub = stub(console, 'error', (...args) => {
logs.push(args.map((a) => String(a)).join(' '));
});
const result = await writeUnitFiles(
name,
serviceContent,
timerContent,
options,
);
assertEquals(result, undefined);
// Files still exist because deletion failed
assertEquals(await exists(servicePath), true);
assertEquals(await exists(timerPath), true);
// Error output contains "rollback_failed"
const combinedLogs = logs.join('\n');
assertStringIncludes(combinedLogs, 'rollback_failed');
writeStub.restore();
removeStub.restore();
consoleStub.restore();
});