mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
128 lines
5.7 KiB
TypeScript
128 lines
5.7 KiB
TypeScript
/**
|
|
* Task C6 (audit #4, major) — a restored archive carries its OWN
|
|
* `app_settings` storage.* rows, which can name a different category
|
|
* assignment than the pre-restore install. Before this fix,
|
|
* `restoreFromZip` never reloaded the storage registry after reopening the
|
|
* DB, so uploads rehydration ran against the STALE pre-restore driver/
|
|
* category map — files landed on the wrong backend.
|
|
*
|
|
* `backup.impl.test.ts` (BACKUP-045f / BACKUP-061f) pins the *ordering*
|
|
* inside `restoreFromZip` with a stubbed StorageService: reinitialize() →
|
|
* storage.reloadConfig() → rehydration, and that reload is skipped when
|
|
* reopen fails. This test pins the other half — that `reloadConfig()`
|
|
* genuinely causes a changed category assignment to take effect for real
|
|
* byte placement — using the REAL StorageRegistryService/StorageService/
|
|
* DatabaseService trio (same pattern as storage-registry.service.test.ts),
|
|
* with two real LocalDriver backends over real tmp directories, so the
|
|
* category under test ('files', reassigned between nas-a/nas-b) never
|
|
* touches a production path. Note: registry construction itself still
|
|
* mkdirs the built-in uploads-local/backups-local roots (server/uploads,
|
|
* server/data/backups) as an unconditional side effect of onModuleInit() —
|
|
* accepted precedent from storage-registry.service.test.ts, harmless (mkdir
|
|
* -p on an existing dir), and orthogonal to what this test actually exercises.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const Database = require('better-sqlite3');
|
|
|
|
import { createTables } from '../../src/db/schema';
|
|
import { runMigrations } from '../../src/db/migrations';
|
|
import type { RuntimeEnvService } from '../../src/nest/app-config/runtime-env.service';
|
|
import { StorageEventsService } from '../../src/nest/storage/storage-events.service';
|
|
import { BACKENDS_KEY, CATEGORIES_KEY, StorageRegistryService } from '../../src/nest/storage/storage-registry.service';
|
|
import { StorageService } from '../../src/nest/storage/storage.service';
|
|
import { createTestUnitOfWork, createTestAppSettingsRepo, sharedTestOrm } from '../helpers/test-uow';
|
|
|
|
const testDb = new Database(':memory:');
|
|
testDb.exec('PRAGMA journal_mode = WAL');
|
|
testDb.exec('PRAGMA foreign_keys = ON');
|
|
|
|
beforeAll(() => {
|
|
createTables(testDb);
|
|
runMigrations(testDb);
|
|
});
|
|
|
|
const tmpDirs: string[] = [];
|
|
function makeTmpDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trek-restore-reload-'));
|
|
tmpDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function envStub(): RuntimeEnvService {
|
|
return { env: () => ({ paths: {} }) } as unknown as RuntimeEnvService;
|
|
}
|
|
|
|
function setSetting(key: string, value: unknown): void {
|
|
testDb
|
|
.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)')
|
|
.run(key, JSON.stringify(value));
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (tmpDirs.length) fs.rmSync(tmpDirs.pop()!, { recursive: true, force: true });
|
|
testDb.prepare("DELETE FROM app_settings WHERE key LIKE 'storage.%'").run();
|
|
});
|
|
|
|
describe('C6 — restore reloads the storage registry (audit #4)', () => {
|
|
it("reloadConfig() picks up the restored archive's category assignment before any byte is rehydrated", async () => {
|
|
const rootA = makeTmpDir();
|
|
const rootB = makeTmpDir();
|
|
|
|
// Pre-restore world: 'files' resolves to backend nas-a.
|
|
setSetting(BACKENDS_KEY, [
|
|
{ name: 'nas-a', type: 'local', options: { root: rootA } },
|
|
{ name: 'nas-b', type: 'local', options: { root: rootB } },
|
|
]);
|
|
setSetting(CATEGORIES_KEY, { files: 'nas-a' });
|
|
|
|
const registry = new StorageRegistryService(
|
|
await createTestAppSettingsRepo(testDb),
|
|
envStub(),
|
|
new StorageEventsService(),
|
|
await createTestUnitOfWork(testDb),
|
|
(await sharedTestOrm(testDb)).orm,
|
|
);
|
|
await registry.onModuleInit();
|
|
const storage = new StorageService(registry);
|
|
|
|
expect(registry.resolve('files').backendName).toBe('nas-a');
|
|
|
|
// A pre-restore object lives under nas-a.
|
|
const preSrc = path.join(makeTmpDir(), 'old.txt');
|
|
fs.writeFileSync(preSrc, 'pre-restore bytes');
|
|
await storage.put('files', 'old.txt', { tmpPath: preSrc });
|
|
expect(fs.existsSync(path.join(rootA, 'files', 'old.txt'))).toBe(true);
|
|
|
|
// Simulate what restoreFromZip's DB swap + reinitialize() actually does in
|
|
// production: the restored archive's travel.db is now live, and IT names a
|
|
// different backend for 'files' (an admin on the source install pointed
|
|
// 'files' at a different backend before taking that backup).
|
|
setSetting(CATEGORIES_KEY, { files: 'nas-b' });
|
|
|
|
// Audit #4, pre-fix: the registry is still holding the pre-restore driver
|
|
// map at this point — resolve() is stale until something calls reload().
|
|
expect(registry.resolve('files').backendName).toBe('nas-a');
|
|
|
|
// The fix under test: StorageService.reloadConfig(), the narrow passthrough
|
|
// restoreFromZip now calls right after reinitialize() and right before
|
|
// rehydration (see backup.impl.ts).
|
|
await storage.reloadConfig();
|
|
|
|
expect(registry.resolve('files').backendName).toBe('nas-b');
|
|
|
|
// Rehydration (the same storage.put({ tmpPath }) shape backup.impl.ts's
|
|
// rehydrateUploads uses for every extracted upload entry) now lands under
|
|
// the RESTORED config's backend, never the stale pre-restore one.
|
|
const restoredSrc = path.join(makeTmpDir(), 'restored.txt');
|
|
fs.writeFileSync(restoredSrc, 'restored bytes');
|
|
await storage.put('files', 'restored.txt', { tmpPath: restoredSrc });
|
|
|
|
expect(fs.existsSync(path.join(rootB, 'files', 'restored.txt'))).toBe(true);
|
|
expect(fs.existsSync(path.join(rootA, 'files', 'restored.txt'))).toBe(false);
|
|
});
|
|
});
|