'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { createBackupReplicationService } = require('../../src/services/backup-replication-service'); test('backup replication copies core files and verifies every hash', async t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-replication-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const backups = path.join(root, 'source', 'backups'); const target = path.join(root, 'target'); const tmp = path.join(root, 'tmp'); fs.mkdirSync(path.join(backups, 'save-vault'), { recursive: true }); fs.mkdirSync(target, { recursive: true }); fs.mkdirSync(tmp, { recursive: true }); fs.writeFileSync(path.join(backups, 'save-vault', 'PPSA00001.zip'), 'encrypted-save-data'); const service = createBackupReplicationService({ paths: { backups, tmp }, targetRoot: target, gameBackupsRoot: path.join(root, 'games') }); const result = await service.replicate({ label: 'Veilige test' }); assert.equal(result.phase, 'complete'); assert.equal(result.copiedFiles, 1); assert.equal(service.list()[0].label, 'Veilige test'); const verified = await service.verify({ id: result.id }); assert.equal(verified.ok, true); assert.equal(verified.files, 1); const drill = await service.restoreDrill({ id: result.id }); assert.equal(drill.ok, true); assert.equal(drill.ps5Modified, false); assert.equal(drill.temporaryTargetRemoved, true); assert.equal(fs.readdirSync(path.join(tmp, 'restore-drills')).length, 0); const protection = service.status().protection; assert.equal(protection.threeTwoOneSatisfied, false); assert.equal(protection.copiesObserved, 2); assert.equal(protection.immutableObserved, false); assert.ok(protection.lastVerifiedAt); assert.ok(protection.lastRestoreDrillAt); assert.ok(Number.isFinite(protection.lastRestoreDrillDurationMs)); assert.ok(service.list()[0].lastRestoreDrillAt); const copied = path.join(target, result.id, 'data', 'chimera-core', 'save-vault', 'PPSA00001.zip'); fs.appendFileSync(copied, 'tampered'); await assert.rejects(() => service.verify({ id: result.id }), /Bestandsgrootte|Hash wijkt af/); }); test('replication retention keeps at least two valid snapshots and exposes immutable-target truthfully', async t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-replication-retention-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const backups = path.join(root, 'backups'); const target = path.join(root, 'target'); const tmp = path.join(root, 'tmp'); for (const directory of [backups, target, tmp]) fs.mkdirSync(directory, { recursive: true }); fs.writeFileSync(path.join(backups, 'settings.json'), '{}'); const values = new Map([['backup.replicationRetentionEnabled', true], ['backup.replicationRetention', 2], ['backup.replicationMaxAgeDays', 180]]); const settings = { get: (key, fallback) => values.has(key) ? values.get(key) : fallback, set: (key, value) => values.set(key, value) }; const service = createBackupReplicationService({ paths: { backups, tmp }, settings, targetRoot: target, gameBackupsRoot: path.join(root, 'games') }); await service.replicate({ label: 'one' }); await new Promise(resolve => setTimeout(resolve, 5)); await service.replicate({ label: 'two' }); await new Promise(resolve => setTimeout(resolve, 5)); const third = await service.replicate({ label: 'three' }); assert.equal(third.retention.removed, 1); assert.equal(service.list().length, 2); assert.equal(service.status().targetContract.immutable, false); assert.equal(service.retentionPreview().retainedMinimum, 2); }); test('backup replication refuses a target nested in its source', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-replication-unsafe-')); try { const backups = path.join(root, 'backups'); const target = path.join(backups, 'replication'); fs.mkdirSync(target, { recursive: true }); const service = createBackupReplicationService({ paths: { backups }, targetRoot: target, gameBackupsRoot: path.join(root, 'games') }); const status = service.status(); assert.equal(status.available, false); assert.match(status.error, /mogen niet in elkaar liggen/); } finally { fs.rmSync(root, { recursive: true, force: true }); } });