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
This commit is contained in:
@@ -3,6 +3,8 @@ 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';
|
||||||
@@ -57,3 +59,227 @@ Deno.test('resolveUnitTargetPath ohne output und ohne 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: Fehler beim Schreiben der .timer-Datei führt zu 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`);
|
||||||
|
|
||||||
|
// Simuliere: Schreiben der .timer-Datei schlägt fehl
|
||||||
|
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('Simulierter Schreibfehler');
|
||||||
|
} else {
|
||||||
|
return await originalWrite(path, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await writeUnitFiles(
|
||||||
|
name,
|
||||||
|
serviceContent,
|
||||||
|
timerContent,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erwartung: Funktion gibt undefined zurück
|
||||||
|
assertEquals(result, undefined);
|
||||||
|
|
||||||
|
// Erwartung: Beide Dateien wurden gelöscht (Rollback)
|
||||||
|
assertEquals(await exists(servicePath), false);
|
||||||
|
assertEquals(await exists(timerPath), false);
|
||||||
|
|
||||||
|
writeStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('writeUnitFiles: Fehler beim Schreiben der .service-Datei verhindert Folgeaktionen', 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`);
|
||||||
|
|
||||||
|
// Simuliere: Fehler beim Schreiben der .service-Datei
|
||||||
|
const writeStub = stub(
|
||||||
|
Deno,
|
||||||
|
'writeTextFile',
|
||||||
|
(path: string | URL, _data: string | ReadableStream<string>) => {
|
||||||
|
if (typeof path === 'string' && path.endsWith('.service')) {
|
||||||
|
throw new Error('Simulierter Service-Schreibfehler');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await writeUnitFiles(
|
||||||
|
name,
|
||||||
|
serviceContent,
|
||||||
|
timerContent,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erwartung: Funktion gibt undefined zurück
|
||||||
|
assertEquals(result, undefined);
|
||||||
|
|
||||||
|
// Erwartung: Es wurden keine Dateien angelegt
|
||||||
|
assertEquals(await exists(servicePath), false);
|
||||||
|
assertEquals(await exists(timerPath), false);
|
||||||
|
|
||||||
|
writeStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('writeUnitFiles: beide Dateien geschrieben, danach Fehler → vollständiger 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('Unerwarteter Pfadtyp');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simuliere beide Schreibvorgänge, aber wirf nach dem zweiten eine Exception
|
||||||
|
writeCount++;
|
||||||
|
|
||||||
|
await originalWriteTextFile(path, data); // wirklich schreiben
|
||||||
|
|
||||||
|
if (writeCount === 2) {
|
||||||
|
throw new Error(
|
||||||
|
'Simulierter Fehler nach vollständigem Schreiben',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await writeUnitFiles(
|
||||||
|
name,
|
||||||
|
serviceContent,
|
||||||
|
timerContent,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erwartung: Funktion gibt undefined zurück
|
||||||
|
assertEquals(result, undefined);
|
||||||
|
|
||||||
|
// Erwartung: Beide Dateien wurden wieder entfernt
|
||||||
|
assertEquals(await exists(servicePath), false);
|
||||||
|
assertEquals(await exists(timerPath), false);
|
||||||
|
|
||||||
|
writeStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('writeUnitFiles: Rollback schlägt fehl, wenn Dateien nicht gelöscht werden können', 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`);
|
||||||
|
|
||||||
|
// Originale Methoden sichern
|
||||||
|
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('Fehler nach vollständigem Schreiben');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeStub = stub(
|
||||||
|
Deno,
|
||||||
|
'remove',
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async (_path: string | URL, _opts?: Deno.RemoveOptions) => {
|
||||||
|
throw new Error('Löschen verboten!');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// capture console output
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Dateien existieren noch, weil löschen fehlschlug
|
||||||
|
assertEquals(await exists(servicePath), true);
|
||||||
|
assertEquals(await exists(timerPath), true);
|
||||||
|
|
||||||
|
// Fehlerausgabe enthält "rollback_failed"
|
||||||
|
const combinedLogs = logs.join('\n');
|
||||||
|
assertStringIncludes(combinedLogs, 'rollback_failed');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
writeStub.restore();
|
||||||
|
removeStub.restore();
|
||||||
|
consoleStub.restore();
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user