mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
f73f395817
The buildApp() suites for auth, bookings, budget, journeys, assignments, passkeys, profiles, OAuth, the AirTrail tick and the plugin boot order, plus the storage restore suite, seeded settings, addons and fixture rows and read back what a request left behind through raw better-sqlite3 statements. They now write and read through the ORM factories and readers, against the app's own MikroORM (or the suite's test ORM where one already existed). The account deletion case keeps every one of its foreign key checks; a row that used to read back as undefined now reads back as null.
118 lines
5.5 KiB
TypeScript
118 lines
5.5 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, afterEach } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { createSnapshotTestDb } from '../helpers/db-mock';
|
|
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';
|
|
import { deleteRows } from '../helpers/factories/rows';
|
|
import { setAppSetting } from '../helpers/factories/settings';
|
|
import { AppSettings } from '../../src/db/entities/AppSettings.entity';
|
|
|
|
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(): { placePhotoDir: string | undefined } {
|
|
return { placePhotoDir: undefined };
|
|
}
|
|
|
|
async function setSetting(key: string, value: unknown): Promise<void> {
|
|
await setAppSetting(await sharedTestOrm(testDb), key, JSON.stringify(value));
|
|
}
|
|
|
|
afterEach(async () => {
|
|
while (tmpDirs.length) fs.rmSync(tmpDirs.pop()!, { recursive: true, force: true });
|
|
await deleteRows(await sharedTestOrm(testDb), AppSettings, { key: { $like: 'storage.%' } });
|
|
});
|
|
|
|
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.
|
|
await setSetting(BACKENDS_KEY, [
|
|
{ name: 'nas-a', type: 'local', options: { root: rootA } },
|
|
{ name: 'nas-b', type: 'local', options: { root: rootB } },
|
|
]);
|
|
await 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).
|
|
await 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);
|
|
});
|
|
});
|