Files
TREK/server/tests/integration/storage-restore-reload.test.ts
T
2026-10-06 21:50:59 +02:00

116 lines
5.4 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 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 { createSnapshotTestDb } from '../helpers/db-mock';
import { createTestUnitOfWork, createTestAppSettingsRepo, sharedTestOrm } from '../helpers/test-uow';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, it, expect, afterEach } from 'vitest';
const testDb = createSnapshotTestDb();
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);
});
});