mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
test(server): close serving-swap gaps surfaced by the full tier
This commit is contained in:
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user