mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
ba1504d6e9
Eight registerAs tokens existed while every consumer read the same values through readEnv() instead, so the boot-stable layer the repo rules describe was unused and nothing said which path owned a variable. FORCE_HTTPS, for instance, was frozen by the middleware through the http token and read live by the cookie code at the same time. A variable is now owned by exactly one side. TRUST_PROXY and HSTS_INCLUDE_SUBDOMAINS belong to httpConfig, which buildApp() already injects into the pre-init middleware, and TREK_PLACE_PHOTO_DIR to the new storageConfig, which the storage registry now injects instead of reading it through RuntimeEnvService. Their derive functions moved to boot-derive.ts, which deriveAll() never calls. The six tokens nothing injected are gone: their values are needed before the container exists (config.ts, the database, src/mcp, the SSRF guard) and stay on readEnv(). config-ownership.test.ts records the keys each side reads through a Proxy and fails on a variable read through both, on a token nothing injects and on a variable the schema does not validate. env-reference.test.ts holds server/.env.example to the schema, which had drifted by 34 variables (SMTP, WebAuthn, plugins, MCP session tuning, TREK_MANAGED and others); they are documented now. jwt-verify.ts no longer cites a middleware file that is gone.
117 lines
5.3 KiB
TypeScript
117 lines
5.3 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';
|
|
|
|
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 };
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|