From 7349ee95c2dad20bd63d9e7d0ea8fdec5bad9157 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 15:44:39 +0200 Subject: [PATCH] test(server): seed the atlas repository and storage admin specs through the factories The visited countries and place regions repository specs seed their rows through the ORM; the legacy statements they hold the repositories to stay raw behind an allow marker, since those are the oracles the parity is checked against. The storage admin e2e moves from hand-rolled tables to the migrated snapshot, seeds its two users through the factories with the ids their session cookies are signed for, and reads the stored backends and the audit rows through the readers instead of raw SELECTs. --- server/tests/e2e/storage-admin.e2e.test.ts | 78 +++++++++---------- .../PlaceRegions.repository.test.ts | 40 ++++++---- .../VisitedCountries.repository.test.ts | 20 +++-- 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/server/tests/e2e/storage-admin.e2e.test.ts b/server/tests/e2e/storage-admin.e2e.test.ts index 3b58272b2..4b61e9296 100644 --- a/server/tests/e2e/storage-admin.e2e.test.ts +++ b/server/tests/e2e/storage-admin.e2e.test.ts @@ -1,7 +1,7 @@ /** * Storage admin e2e — exercises the migrated /api/admin/storage surface * (StorageAdminController) through the real JwtAuthGuard + AdminGuard + - * ManagedGuard against a temp SQLite db. DI-native: no service mock, so the + * ManagedGuard against a migrated temp SQLite db. DI-native: no service mock, so the * registry's real boot/seed/reload pipeline and StorageAdminService run for * real. Covers auth (401), the admin gate (403), managed-mode refusal (403, * the first e2e to assert it — ManagedGuard is otherwise only wired in @@ -20,29 +20,12 @@ import { APP_GUARD } from '@nestjs/core'; import { StorageModule } from '../../src/nest/storage/storage.module'; import { ManagedGuard } from '../../src/nest/common/managed.guard'; import { Test } from '@nestjs/testing'; -import { seedUser, sessionCookie } from './harness'; +import { sessionCookie } from './harness'; -const { db } = vi.hoisted(() => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const Database = require('better-sqlite3'); - const tmp = new Database(':memory:'); - tmp.exec('PRAGMA journal_mode = WAL'); - // `users` carries the columns listUsers/createUser/updateUser select, plus the - // is_guest flag the #1362 COALESCE guards read (admin.e2e.test.ts DDL). - tmp.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, - email TEXT NOT NULL UNIQUE, role TEXT NOT NULL DEFAULT 'user', password_version INTEGER NOT NULL DEFAULT 0, - password_hash TEXT, avatar TEXT, is_guest INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_login DATETIME);`); - tmp.exec('CREATE TABLE app_settings (key TEXT PRIMARY KEY, value TEXT);'); - // Slim audit_log mirror (no FKs), same shape as admin.e2e.test.ts. - tmp.exec(`CREATE TABLE audit_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - user_id INTEGER, action TEXT NOT NULL, resource TEXT, details TEXT, ip TEXT);`); - return { db: tmp }; +vi.mock('../../src/db/database', async () => { + const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock'); + return buildDbMock(createSnapshotTestDb()); }); - -vi.mock('../../src/db/database', () => ({ db, closeDb: () => {}, reinitialize: () => {} })); // The audit domain is DI-native: writeAudit runs for real against the temp db's // audit_log table; only the file logger is silenced. vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() })); @@ -57,9 +40,16 @@ vi.mock('../../src/config', () => ({ ENCRYPTION_KEY: 'e2e-storage-key', JWT_SECR import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; -import { createTestMikroOrmModule } from '../helpers/test-orm'; +import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm'; +import { db } from '../../src/db/database'; +import { AppSettings } from '../../src/db/entities/AppSettings.entity'; +import { AuditLog } from '../../src/db/entities/AuditLog.entity'; +import { makeAdmin, makeUser } from '../helpers/factories/users'; +import { countRows, findRow } from '../helpers/factories/rows'; -describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLite)', () => { +let orm: TestOrm; + +describe('Storage admin e2e (real auth + admin guard + managed guard + migrated temp SQLite)', () => { let server: Server; let app: Awaited>; let adminCookie: string; @@ -80,8 +70,10 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi beforeAll(async () => { process.env.ENCRYPTION_KEY = 'e2e-storage-key'; - seedUser(db as never, { id: 1 }); - seedUser(db as never, { id: 2, role: 'admin', email: 'e2e-storage-admin@example.test' }); + orm = await createTestOrm(db); + // Pinned ids: sessionCookie(1) and (2) sign for exactly these users. + await makeUser(orm, { id: 1, email: 'e2e@example.test' }); + await makeAdmin(orm, { id: 2, email: 'e2e-storage-admin@example.test' }); userCookie = sessionCookie(1); adminCookie = sessionCookie(2); app = await build(); @@ -101,6 +93,7 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi afterAll(async () => { await app.close(); + await orm.close(); delete process.env.ENCRYPTION_KEY; }); @@ -154,13 +147,13 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi expect(offBox.options.secretAccessKey).toBe('••••••••'); // masked, never echoed expect(res.body.categories.backups).toEqual({ backend: 'nas-backups', source: 'settings' }); - const row = db.prepare("SELECT value FROM app_settings WHERE key = 'storage.backends'").get() as { value: string }; - expect(row.value).toContain('enc:v1:'); - expect(row.value).not.toContain('sk-e2e'); + const row = await findRow(orm, AppSettings, { key: 'storage.backends' }); + expect(row?.value).toContain('enc:v1:'); + expect(row?.value).not.toContain('sk-e2e'); - const audit = db.prepare("SELECT details FROM audit_log WHERE action = 'admin.storage_update'").get() as { details: string }; - expect(audit.details).toContain('***'); - expect(audit.details).not.toContain('sk-e2e'); + const audit = await findRow(orm, AuditLog, { action: 'admin.storage_update' }); + expect(audit?.details).toContain('***'); + expect(audit?.details).not.toContain('sk-e2e'); }); it('STORE2E-005 PUT with a semantic violation → 400 with the registry message verbatim', async () => { @@ -204,9 +197,9 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi (b) => b.name === 'off-box', )!; expect(offBox.options.secretAccessKey).not.toBe('sk'); - const row = db.prepare("SELECT value FROM app_settings WHERE key = 'storage.backends'").get() as { value: string }; - expect(row.value).not.toContain('"sk"'); - expect(row.value).toContain('enc:v1:'); + const row = await findRow(orm, AppSettings, { key: 'storage.backends' }); + expect(row?.value).not.toContain('"sk"'); + expect(row?.value).toContain('enc:v1:'); } finally { process.env.ENCRYPTION_KEY = 'e2e-storage-key'; } @@ -234,8 +227,8 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi .send({ backend: { name: 'cand', type: 'local', options: { root } } }); expect(res.status).toBe(200); expect(res.body).toEqual({ ok: true, targets: [{ name: 'cand', ok: true }] }); - const audit = db.prepare("SELECT details FROM audit_log WHERE action = 'admin.storage_test'").get() as { details: string }; - expect(JSON.parse(audit.details)).toMatchObject({ backend: 'cand', type: 'local', ok: true }); + const audit = await findRow(orm, AuditLog, { action: 'admin.storage_test' }); + expect(JSON.parse(String(audit?.details))).toMatchObject({ backend: 'cand', type: 'local', ok: true }); }); it('STORE2E-010 migration moves a category end to end', async () => { @@ -267,8 +260,8 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi } expect(status).toMatchObject({ status: 'done' }); expect(stateBody!.categories.journey).toEqual({ backend: 'dest', source: 'settings' }); - const audit = db.prepare("SELECT details FROM audit_log WHERE action = 'admin.storage_migration'").get() as { details: string }; - expect(JSON.parse(audit.details)).toEqual({ category: 'journey', to: 'dest' }); + const audit = await findRow(orm, AuditLog, { action: 'admin.storage_migration' }); + expect(JSON.parse(String(audit?.details))).toEqual({ category: 'journey', to: 'dest' }); }); it('STORE2E-011 backfill guards: 401 anon, 404 non-mirror, 409 while running is covered by unit — here the 404', async () => { @@ -314,8 +307,8 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi } expect(status).toMatchObject({ status: 'done' }); expect(fs.existsSync(path.join(nasRoot, 'pre-mirror.zip'))).toBe(true); - const audit = db.prepare("SELECT details FROM audit_log WHERE action = 'admin.storage_backfill'").get() as { details: string }; - expect(JSON.parse(audit.details)).toMatchObject({ backend: 'm' }); + const audit = await findRow(orm, AuditLog, { action: 'admin.storage_backfill' }); + expect(JSON.parse(String(audit?.details))).toMatchObject({ backend: 'm' }); }); it('STORE2E-013 cancel 404s with no active run; stats refresh returns real numbers and audits', async () => { @@ -324,8 +317,7 @@ describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLi expect(res.status).toBe(200); expect(res.body.computedAt).toBeGreaterThan(0); expect(res.body.categories.backups.objects).toBeGreaterThanOrEqual(1); // pre-mirror.zip at least - const audit = db.prepare("SELECT COUNT(*) AS n FROM audit_log WHERE action = 'admin.storage_stats_refresh'").get() as { n: number }; - expect(audit.n).toBe(1); + expect(await countRows(orm, AuditLog, { action: 'admin.storage_stats_refresh' })).toBe(1); }); it('STORE2E-014 regression (audit #7): a migration flip while the admin form is open makes the stale save 409, and the flip survives', async () => { diff --git a/server/tests/unit/db/repositories/PlaceRegions.repository.test.ts b/server/tests/unit/db/repositories/PlaceRegions.repository.test.ts index 25bff54bd..b9803d097 100644 --- a/server/tests/unit/db/repositories/PlaceRegions.repository.test.ts +++ b/server/tests/unit/db/repositories/PlaceRegions.repository.test.ts @@ -10,6 +10,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { createSnapshotTestDb } from '../../../helpers/db-mock'; import { resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; +import { insertRow } from '../../../helpers/factories/rows'; import { createPlace, createTrip, createUser, addTripMember } from '../../../helpers/factories'; import { PlaceRegions } from '../../../../src/db/entities/PlaceRegions.entity'; import type { PlaceRegionsRepository } from '../../../../src/db/repositories/PlaceRegions.repository'; @@ -25,9 +26,8 @@ beforeAll(async () => { beforeEach(() => { resetTestDb(testDb); t.clear(); }); afterAll(async () => { await t.close(); testDb.close(); }); -function insertPlaceRegion(placeId: number, countryCode: string, regionCode: string, regionName: string): void { - testDb.prepare('INSERT INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)') - .run(placeId, countryCode, regionCode, regionName); +async function insertPlaceRegion(placeId: number, countryCode: string, regionCode: string, regionName: string): Promise { + await insertRow(t, PlaceRegions, { place: placeId, country_code: countryCode, region_code: regionCode, region_name: regionName }); } describe('PlaceRegionsRepository.listCountryCodesForPlaceIds (AT3)', () => { @@ -37,9 +37,10 @@ describe('PlaceRegionsRepository.listCountryCodesForPlaceIds (AT3)', () => { const p1 = createPlace(testDb, trip.id, { name: 'Eiffel Tower' }); const p2 = createPlace(testDb, trip.id, { name: 'Louvre' }); const p3 = createPlace(testDb, trip.id, { name: 'Not geocoded' }); - insertPlaceRegion(p1.id, 'FR', 'FR-IDF', 'Île-de-France'); - insertPlaceRegion(p2.id, 'FR', 'FR-IDF', 'Île-de-France'); + await insertPlaceRegion(p1.id, 'FR', 'FR-IDF', 'Île-de-France'); + await insertPlaceRegion(p2.id, 'FR', 'FR-IDF', 'Île-de-France'); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare(`SELECT place_id, country_code FROM place_regions WHERE place_id IN (${[p1.id, p2.id, p3.id].join(',')})`).all(); const typed = await repo.listCountryCodesForPlaceIds([p1.id, p2.id, p3.id]); expect(typed).toEqual(legacy); @@ -57,9 +58,10 @@ describe('PlaceRegionsRepository.listForPlaceIds (AT28)', () => { const trip = createTrip(testDb, user.id); const p1 = createPlace(testDb, trip.id); const p2 = createPlace(testDb, trip.id); - insertPlaceRegion(p1.id, 'US', 'US-NY', 'New York'); - insertPlaceRegion(p2.id, 'US', 'US-CA', 'California'); + await insertPlaceRegion(p1.id, 'US', 'US-NY', 'New York'); + await insertPlaceRegion(p2.id, 'US', 'US-CA', 'California'); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare(`SELECT * FROM place_regions WHERE place_id IN (${[p1.id, p2.id].join(',')})`).all(); const typed = await repo.listForPlaceIds([p1.id, p2.id]); expect(typed).toEqual(legacy); @@ -77,10 +79,11 @@ describe('PlaceRegionsRepository.listDistinctRegionCodesForCountryAndPlaces (AT2 const p1 = createPlace(testDb, trip.id); const p2 = createPlace(testDb, trip.id); const p3 = createPlace(testDb, trip.id); - insertPlaceRegion(p1.id, 'FR', 'FR-IDF', 'Île-de-France'); - insertPlaceRegion(p2.id, 'FR', 'FR-IDF', 'Île-de-France'); // same region, dedup target - insertPlaceRegion(p3.id, 'DE', 'DE-BY', 'Bavaria'); // different country, excluded + await insertPlaceRegion(p1.id, 'FR', 'FR-IDF', 'Île-de-France'); + await insertPlaceRegion(p2.id, 'FR', 'FR-IDF', 'Île-de-France'); // same region, dedup target + await insertPlaceRegion(p3.id, 'DE', 'DE-BY', 'Bavaria'); // different country, excluded + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare( `SELECT DISTINCT region_code FROM place_regions WHERE country_code = ? AND place_id IN (${[p1.id, p2.id, p3.id].join(',')})`, ).all('FR'); @@ -103,14 +106,15 @@ describe('PlaceRegionsRepository.countPlacesByCountryForTrip (AT40)', () => { const fr2 = createPlace(testDb, trip.id); const de1 = createPlace(testDb, trip.id); const noRegion = createPlace(testDb, trip.id); - insertPlaceRegion(fr1.id, 'FR', 'FR-IDF', 'Île-de-France'); - insertPlaceRegion(fr2.id, 'FR', 'FR-PAC', 'Provence'); - insertPlaceRegion(de1.id, 'DE', 'DE-BY', 'Bavaria'); + await insertPlaceRegion(fr1.id, 'FR', 'FR-IDF', 'Île-de-France'); + await insertPlaceRegion(fr2.id, 'FR', 'FR-PAC', 'Provence'); + await insertPlaceRegion(de1.id, 'DE', 'DE-BY', 'Bavaria'); // A different trip's place must not leak in. const foreign = createPlace(testDb, other.id); - insertPlaceRegion(foreign.id, 'FR', 'FR-IDF', 'Île-de-France'); + await insertPlaceRegion(foreign.id, 'FR', 'FR-IDF', 'Île-de-France'); void noRegion; + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare(` SELECT pr.country_code, COUNT(DISTINCT p.id) AS places FROM place_regions pr JOIN places p ON p.id = pr.place_id @@ -138,13 +142,14 @@ describe('PlaceRegionsRepository.listVisitedCountryCodesForUser (AT44)', () => { addTripMember(testDb, started.id, member.id); const startedPlace = createPlace(testDb, started.id); - insertPlaceRegion(startedPlace.id, 'FR', 'FR-IDF', 'Île-de-France'); + await insertPlaceRegion(startedPlace.id, 'FR', 'FR-IDF', 'Île-de-France'); const startedPlace2 = createPlace(testDb, started.id); - insertPlaceRegion(startedPlace2.id, 'FR', 'FR-PAC', 'Provence'); // same country, dedup target + await insertPlaceRegion(startedPlace2.id, 'FR', 'FR-PAC', 'Provence'); // same country, dedup target const notStartedPlace = createPlace(testDb, notStarted.id); - insertPlaceRegion(notStartedPlace.id, 'DE', 'DE-BY', 'Bavaria'); // not started, excluded + await insertPlaceRegion(notStartedPlace.id, 'DE', 'DE-BY', 'Bavaria'); // not started, excluded const today = '2026-01-01'; + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare(` SELECT DISTINCT pr.country_code FROM place_regions pr @@ -160,6 +165,7 @@ describe('PlaceRegionsRepository.listVisitedCountryCodesForUser (AT44)', () => { expect(typedOwner).toEqual(legacy.map((r: unknown) => (r as { country_code: string }).country_code)); expect(typedOwner).toEqual(['FR']); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacyMember = testDb.prepare(` SELECT DISTINCT pr.country_code FROM place_regions pr diff --git a/server/tests/unit/db/repositories/VisitedCountries.repository.test.ts b/server/tests/unit/db/repositories/VisitedCountries.repository.test.ts index a9b04fbe1..e2a21def1 100644 --- a/server/tests/unit/db/repositories/VisitedCountries.repository.test.ts +++ b/server/tests/unit/db/repositories/VisitedCountries.repository.test.ts @@ -10,6 +10,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { createSnapshotTestDb } from '../../../helpers/db-mock'; import { resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; +import { insertRows } from '../../../helpers/factories/rows'; import { createUser } from '../../../helpers/factories'; import { VisitedCountries } from '../../../../src/db/entities/VisitedCountries.entity'; import type { VisitedCountriesRepository } from '../../../../src/db/repositories/VisitedCountries.repository'; @@ -29,13 +30,13 @@ describe('VisitedCountriesRepository.listForUser (AT10)', () => { it('VISCOUNTRYREPO-001: matches SELECT country_code, created_at, source FROM visited_countries WHERE user_id = ? ORDER BY created_at DESC run raw, scoped to the user', async () => { const { user } = createUser(testDb); const { user: other } = createUser(testDb, { username: 'other' }); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code, source, created_at) VALUES (?, ?, ?, ?)') - .run(user.id, 'DE', 'manual', '2026-01-01T00:00:00.000Z'); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code, source, created_at) VALUES (?, ?, ?, ?)') - .run(user.id, 'FR', 'dawarich', '2026-02-01T00:00:00.000Z'); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code, source, created_at) VALUES (?, ?, ?, ?)') - .run(other.id, 'JP', 'manual', '2026-03-01T00:00:00.000Z'); + await insertRows(t, VisitedCountries, [ + { user: user.id, country_code: 'DE', source: 'manual', created_at: '2026-01-01T00:00:00.000Z' }, + { user: user.id, country_code: 'FR', source: 'dawarich', created_at: '2026-02-01T00:00:00.000Z' }, + { user: other.id, country_code: 'JP', source: 'manual', created_at: '2026-03-01T00:00:00.000Z' }, + ]); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare('SELECT country_code, created_at, source FROM visited_countries WHERE user_id = ? ORDER BY created_at DESC').all(user.id); const rows = await repo.listForUser(user.id); @@ -52,9 +53,12 @@ describe('VisitedCountriesRepository.listForUser (AT10)', () => { describe('VisitedCountriesRepository.listCodesForUser (AT5/AT7/AT43)', () => { it('VISCOUNTRYREPO-003: matches SELECT country_code FROM visited_countries WHERE user_id = ? run raw', async () => { const { user } = createUser(testDb); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code, source) VALUES (?, ?, ?)').run(user.id, 'DE', 'manual'); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code, source) VALUES (?, ?, ?)').run(user.id, 'FR', 'manual'); + await insertRows(t, VisitedCountries, [ + { user: user.id, country_code: 'DE', source: 'manual' }, + { user: user.id, country_code: 'FR', source: 'manual' }, + ]); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare('SELECT country_code FROM visited_countries WHERE user_id = ?').all(user.id); const codes = await repo.listCodesForUser(user.id);