mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
148 lines
6.5 KiB
TypeScript
148 lines
6.5 KiB
TypeScript
/**
|
|
* Plan 3b Task 0 (D6): `servePhoto` — the pre-init `/uploads/photos/:filename`
|
|
* Express route mounted by `applyPlatformUploads` (`bootstrap.ts`, BEFORE
|
|
* `app.init()`) — now runs inside `withRequestContext(orm, …)`.
|
|
*
|
|
* Per the inventory (§5/§6): `applyPlatformUploads` is mounted permanently on
|
|
* the raw Express instance, not just during a boot window, so every request
|
|
* to `/uploads/photos/*` forever ran outside Nest's per-request EntityManager
|
|
* fork. Converting `jwt-verify.ts`'s read off the raw `db` proxy (Task 1)
|
|
* without this wrapper first would break every authenticated photo request
|
|
* the moment it landed.
|
|
*
|
|
* Three proofs, mirroring Plan 3a's WSAD-040/041 shape:
|
|
* - PHOTOCTX-001: parity — an authenticated request still 200s exactly as
|
|
* before the wrap.
|
|
* - PHOTOCTX-002: load-bearing (positive) — a repository read forced inside
|
|
* the handler (as Task 1's `jwt-verify.ts` conversion will add one)
|
|
* succeeds, because `withRequestContext` genuinely covers the whole async
|
|
* chain of `servePhoto`, not just its synchronous prologue.
|
|
* - PHOTOCTX-003: load-bearing (negative) — the SAME real, exported
|
|
* `applyPlatformUploads`, called with no ORM, answers 500 through
|
|
* Express's error path BEFORE `servePhoto` ever runs (fail-closed, per
|
|
* the Plan 3b Rulings) — proving the wrapper is what makes PHOTOCTX-002
|
|
* succeed, not an accident of the test setup.
|
|
*/
|
|
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
import request from 'supertest';
|
|
import express from 'express';
|
|
import type { Application } from 'express';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { MikroORM } from '@mikro-orm/core';
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
vi.mock('../../src/config', () => ({
|
|
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
|
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
|
updateJwtSecret: () => {},
|
|
SESSION_DURATION: '24h',
|
|
SESSION_DURATION_MS: 86400000,
|
|
SESSION_DURATION_SECONDS: 86400,
|
|
DEFAULT_LANGUAGE: 'en',
|
|
}));
|
|
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
|
|
|
import { db as testDb } from '../../src/db/database';
|
|
import { buildApp } from '../../src/bootstrap';
|
|
import { resetTestDb } from '../helpers/test-db';
|
|
import { createUser } from '../helpers/factories';
|
|
import { generateToken } from '../helpers/auth';
|
|
import { applyPlatformUploads } from '../../src/nest/platform/platform.routes';
|
|
import { StorageService } from '../../src/nest/storage/storage.service';
|
|
import { Users } from '../../src/db/entities/Users.entity';
|
|
|
|
let nestApp: INestApplication;
|
|
let app: Application;
|
|
|
|
// A real file under the local driver's actual 'photos' root (DEFAULT_UPLOADS_ROOT,
|
|
// server/uploads — same fixture pattern as tests/e2e/share.e2e.test.ts's
|
|
// photos-google fixture, one category over).
|
|
const photoName = 'trek-context-photo.integration.jpg';
|
|
const photoFile = path.join(__dirname, '../../uploads/photos', photoName);
|
|
const photoBytes = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); // JPEG-ish header
|
|
|
|
beforeAll(async () => {
|
|
fs.mkdirSync(path.dirname(photoFile), { recursive: true });
|
|
fs.writeFileSync(photoFile, photoBytes);
|
|
nestApp = await buildApp();
|
|
app = nestApp.getHttpAdapter().getInstance();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await nestApp.close();
|
|
try { fs.unlinkSync(photoFile); } catch { /* ignore */ }
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
});
|
|
|
|
describe('GET /uploads/photos/:filename — request context (Plan 3b Task 0, D6)', () => {
|
|
it('PHOTOCTX-001: a valid JWT session still serves the photo — the wrap changes nothing observable', async () => {
|
|
const { user } = createUser(testDb);
|
|
const res = await request(app)
|
|
.get(`/uploads/photos/${photoName}`)
|
|
.set('Authorization', `Bearer ${generateToken(user.id)}`);
|
|
expect(res.status).toBe(200);
|
|
expect(Buffer.from(res.body as Buffer)).toEqual(photoBytes);
|
|
});
|
|
|
|
it('PHOTOCTX-002: a repository read forced inside the handler succeeds under the wrap (load-bearing positive proof)', async () => {
|
|
const { user } = createUser(testDb);
|
|
const storage = nestApp.get(StorageService);
|
|
const orm = nestApp.get(MikroORM);
|
|
let caught: unknown;
|
|
let found: unknown;
|
|
// storage.exists is the very first thing servePhoto calls — forcing a
|
|
// genuine repository read here, as if a converted jwt-verify.ts (Task 1)
|
|
// already read `users` through UsersRepository, proves withRequestContext
|
|
// covers the wrapped call's entire async chain, not just its synchronous
|
|
// prologue (RequestContext.create is AsyncLocalStorage.run; see
|
|
// trek-ws.adapter.ts's docstring for the same property on the WS side).
|
|
const spy = vi.spyOn(storage, 'exists').mockImplementation(async () => {
|
|
try {
|
|
found = await orm.em.getRepository(Users).findOne({ id: user.id });
|
|
} catch (e) {
|
|
caught = e;
|
|
}
|
|
return true;
|
|
});
|
|
try {
|
|
const res = await request(app)
|
|
.get(`/uploads/photos/${photoName}`)
|
|
.set('Authorization', `Bearer ${generateToken(user.id)}`);
|
|
expect(caught).toBeUndefined();
|
|
expect(found).toBeTruthy();
|
|
expect(res.status).toBe(200);
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('applyPlatformUploads without an ORM — fail-closed (Plan 3b Task 0, D6; mutation proof for PHOTOCTX-001/002)', () => {
|
|
it('PHOTOCTX-003: the SAME real registration, called with no ORM, answers 500 BEFORE servePhoto ever runs', async () => {
|
|
const bare = express();
|
|
const storage = nestApp.get(StorageService);
|
|
const existsSpy = vi.spyOn(storage, 'exists');
|
|
// The real, exported applyPlatformUploads — no test double, no hand-call
|
|
// of a private wrapper — with its optional `orm` param simply omitted,
|
|
// the same shape TrekWsAdapter's WSAD-040 uses to prove its own D6 wrap.
|
|
applyPlatformUploads(bare, storage);
|
|
try {
|
|
const res = await request(bare).get(`/uploads/photos/${photoName}`);
|
|
expect(res.status).toBe(500);
|
|
// The fail-closed branch throws BEFORE servePhoto calls storage.exists —
|
|
// the handler never even started, exactly like WSAD-040's handlerRan check.
|
|
expect(existsSpy).not.toHaveBeenCalled();
|
|
} finally {
|
|
existsSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|