Compare commits
9 Commits
v0.4.0
...
e5f9f2c45a
Author | SHA1 | Date | |
---|---|---|---|
e5f9f2c45a
|
|||
f3c46e1222
|
|||
c4f4614a2d
|
|||
6039d236eb
|
|||
333341d3fd
|
|||
38afcf210e | |||
28b18dc994
|
|||
ede012317b | |||
a22c156dd3
|
5
.gitea/default_merge_message/MERGE_TEMPLATE.md
Normal file
5
.gitea/default_merge_message/MERGE_TEMPLATE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
chore(pr): ${PullRequestTitle} ${PullRequestReference}
|
||||
|
||||
${PullRequestDescription}
|
||||
|
||||
Merged from ${HeadBranch} into ${BaseBranch}
|
13
CHANGELOG.md
13
CHANGELOG.md
@@ -2,6 +2,19 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [unreleased]
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- *(fs)* Update test descriptions and comments to English - ([5f55c73](https://git.0xmax42.io/maxp/systemd-timer/commit/5f55c735f96c787bb90b88e1aebb05660e3a4a42))
|
||||
- *(fs)* Add rollback tests for writeUnitFiles errors - ([140b18b](https://git.0xmax42.io/maxp/systemd-timer/commit/140b18b7cdd51d0e263003c105c9c809e7a594f1))
|
||||
|
||||
## [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
|
||||
|
||||
### 🚀 Features
|
||||
|
@@ -5,8 +5,8 @@
|
||||
"fmt": "deno fmt --check",
|
||||
"lint": "deno lint",
|
||||
"ci": "deno task fmt && deno task lint && deno task test", // 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: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: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 --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 --allow-read --output dist/systemd-timer-linux-arm64 src/mod.ts"
|
||||
},
|
||||
"compilerOptions": {},
|
||||
"fmt": {
|
||||
|
3
deno.lock
generated
3
deno.lock
generated
@@ -168,7 +168,8 @@
|
||||
"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/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": {
|
||||
"dependencies": [
|
||||
|
@@ -3,11 +3,13 @@ import {
|
||||
assertExists,
|
||||
assertStringIncludes,
|
||||
} 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 { resolveUnitTargetPath, writeUnitFiles } from '../mod.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 options = {
|
||||
@@ -26,15 +28,15 @@ Deno.test('writeUnitFiles schreibt .service und .timer korrekt', async () => {
|
||||
options as TimerOptions,
|
||||
) as { servicePath: string; timerPath: string };
|
||||
|
||||
// Überprüfe Pfade
|
||||
// Check file paths
|
||||
assertEquals(servicePath, join(tmp, 'test-backup.service'));
|
||||
assertEquals(timerPath, join(tmp, 'test-backup.timer'));
|
||||
|
||||
// Existieren Dateien?
|
||||
// Check if files exist
|
||||
assertExists(await Deno.stat(servicePath));
|
||||
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 readTimer = await Deno.readTextFile(timerPath);
|
||||
|
||||
@@ -42,18 +44,237 @@ Deno.test('writeUnitFiles schreibt .service und .timer korrekt', async () => {
|
||||
assertStringIncludes(readTimer, 'OnCalendar=daily');
|
||||
});
|
||||
|
||||
Deno.test('resolveUnitTargetPath mit --output', () => {
|
||||
Deno.test('resolveUnitTargetPath: with --output', () => {
|
||||
const result = resolveUnitTargetPath({ output: '/tmp/units', user: false });
|
||||
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');
|
||||
const result = resolveUnitTargetPath({ output: undefined, user: true });
|
||||
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 });
|
||||
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();
|
||||
});
|
||||
|
Reference in New Issue
Block a user