test(server): close serving-swap gaps surfaced by the full tier

This commit is contained in:
jubnl
2026-08-16 18:36:38 +02:00
parent 94490b7d31
commit 31478e232a
4 changed files with 69 additions and 11 deletions
+18 -11
View File
@@ -46,9 +46,9 @@ import { PermissionsService } from '../../src/nest/permissions/permissions.servi
let checkPermission: MockInstance;
// Overridden in the container rather than path-mocked: the cache is a provider now.
const serveFilePath = vi.fn();
// serveKey hands the controller a bare photos-google storage name (slice 3).
const serveKey = vi.fn();
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { createTables } from '../../src/db/schema';
@@ -66,7 +66,7 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
async function build() {
const moduleRef = await Test.createTestingModule({ imports: [DatabaseModule, ShareModule] })
.overrideProvider(PlacePhotoCacheService).useValue({ serveFilePath })
.overrideProvider(PlacePhotoCacheService).useValue({ serveKey })
.compile();
const nest = moduleRef.createNestApplication();
nest.use(cookieParser());
@@ -97,7 +97,7 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
db.exec('DELETE FROM trips');
tripId = Number(db.prepare('INSERT INTO trips (user_id, title) VALUES (1, ?)').run('Trip').lastInsertRowid);
checkPermission.mockReturnValue(true);
serveFilePath.mockReset();
serveKey.mockReset();
});
afterAll(async () => {
@@ -166,29 +166,36 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
});
describe('public place-photo proxy (/api/shared/:token/place-photo/:placeId/bytes)', () => {
const photoFile = path.join(os.tmpdir(), 'trek-share-photo.e2e.jpg');
// The controller streams (category 'photos-google', name) through the real
// storage facade, so the fixture has to live in the real photos-google root.
const photoName = 'trek-share-photo.e2e.jpg';
const photoFile = path.join(__dirname, '../../uploads/photos/google', photoName);
const photoBytes = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); // JPEG-ish header
beforeAll(() => fs.writeFileSync(photoFile, photoBytes));
beforeAll(() => {
fs.mkdirSync(path.dirname(photoFile), { recursive: true });
fs.writeFileSync(photoFile, photoBytes);
});
afterAll(() => { try { fs.unlinkSync(photoFile); } catch { /* ignore */ } });
it('streams cached bytes with no cookie (unguarded) for a valid token + place', async () => {
const created = await request(server).post(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1)).send({});
db.prepare('INSERT INTO places (trip_id, name, image_url) VALUES (?, ?, ?)')
.run(tripId, 'Louvre', '/api/maps/place-photo/ChIJabc/bytes');
serveFilePath.mockReturnValueOnce(photoFile);
serveKey.mockReturnValueOnce(photoName);
const res = await request(server).get(`/api/shared/${created.body.token}/place-photo/ChIJabc/bytes`);
expect(res.status).toBe(200);
expect(res.headers['content-type']).toContain('image/jpeg');
expect(res.headers['cache-control']).toContain('immutable');
expect(Buffer.from(res.body)).toEqual(photoBytes);
expect(serveFilePath).toHaveBeenCalledWith('ChIJabc');
expect(serveKey).toHaveBeenCalledWith('ChIJabc');
});
it('404 when the token/place does not resolve to a cached photo', async () => {
it('answers an empty 204 when the token/place does not resolve to a cached photo', async () => {
const res = await request(server).get('/api/shared/bad/place-photo/ChIJabc/bytes');
expect(res.status).toBe(404);
expect(res.body).toEqual({ error: 'Photo not cached' });
expect(res.status).toBe(204);
expect(res.headers['cache-control']).toBe('no-store');
expect(res.text ?? '').toBe('');
});
});
});
@@ -246,6 +246,11 @@ describe('/uploads static parity — miss falls through to the Nest 404 envelope
describe('/uploads static parity — all four mounts', () => {
it('UPLOADS-P21 — covers/journey(nested)/places each serve a hit and 404 a miss', async () => {
// Fixtures are (re)written immediately before each request: other suites
// rm their whole category dir in afterAll (collections/trips → covers,
// collections → places, journey.test → journey), so a beforeAll-written
// file can vanish mid-run when the tier runs in parallel workers.
writeFixture(`covers/${NAME}`);
const coverHit = await request(app).get(`/uploads/covers/${NAME}`);
expect(coverHit.status).toBe(200);
const coverMiss = await request(app).get('/uploads/covers/nope.png');
@@ -254,12 +259,14 @@ describe('/uploads static parity — all four mounts', () => {
// Nested name — journey thumbs live at journey/thumbs/<hash>.jpg and are
// served through the same mount; pins multi-segment storage keys.
writeFixture('journey/thumbs/uploads-static-parity.jpg');
const journeyHit = await request(app).get('/uploads/journey/thumbs/uploads-static-parity.jpg');
expect(journeyHit.status).toBe(200);
expect(journeyHit.headers['content-type']).toBe('image/jpeg');
const journeyMiss = await request(app).get('/uploads/journey/thumbs/nope.jpg');
expect(journeyMiss.status).toBe(404);
writeFixture(`places/${NAME}`);
const placeHit = await request(app).get(`/uploads/places/${NAME}`);
expect(placeHit.status).toBe(200);
const placeMiss = await request(app).get('/uploads/places/nope.png');
@@ -221,6 +221,18 @@ describe('streamPhoto — dispatch', () => {
expect(storage.sendToResponse).not.toHaveBeenCalled();
});
it('RESOLVE-019: a rejecting exists check (invalid key) reads as a local miss for thumb and original', async () => {
const id = insertPhoto({ provider: 'local', file_path: 'journey/x.jpg', thumbnail_path: 'journey/thumbs/h.jpg' });
storage.exists.mockRejectedValue(new Error('invalid storage key'));
const res = makeRes();
await svc.streamPhoto(res as never, 1, id, 'thumbnail');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'File not found' });
expect(storage.sendToResponse).not.toHaveBeenCalled();
});
it('RESOLVE-018: a file_path outside journey/ reads as a local miss without touching storage', async () => {
const id = insertPhoto({ provider: 'local', file_path: 'nope/other.jpg' });
const res = makeRes();
@@ -236,6 +236,38 @@ describe('applyPlatformUploads', () => {
expect(res.statusCode).toBe(404);
expect(res.body).toBe('Not found');
});
it('rethrows a non-miss send failure to the route error handler', async () => {
h.exists.mockResolvedValue(true);
h.verifyJwtAndLoadUser.mockReturnValue({ id: 1 });
const boom = new Error('disk on fire');
h.sendToResponse.mockRejectedValue(boom);
const res = makeRes();
const localNext = vi.fn();
await photoHandler()(
{ params: { filename: 'a.jpg' }, headers: { authorization: 'Bearer jwt123' }, query: {} },
res,
localNext,
);
expect(localNext).toHaveBeenCalledWith(boom);
expect(res.statusCode).toBe(200); // untouched — finalhandler owns it now
});
it('does not write a second 404 when the send fails after headers were flushed', async () => {
h.exists.mockResolvedValue(true);
h.verifyJwtAndLoadUser.mockReturnValue({ id: 1 });
h.sendToResponse.mockRejectedValue(new StorageNotFoundError('photos/a.jpg'));
const res = makeRes();
(res as unknown as { headersSent: boolean }).headersSent = true;
const localNext = vi.fn();
await photoHandler()(
{ params: { filename: 'a.jpg' }, headers: { authorization: 'Bearer jwt123' }, query: {} },
res,
localNext,
);
expect(res.status).not.toHaveBeenCalled();
expect(localNext).toHaveBeenCalledWith(expect.any(StorageNotFoundError));
});
});
});