mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
test(files): upload filters fail closed on an excluded or unresolvable type
This commit is contained in:
@@ -343,7 +343,7 @@ describe('updateFile', () => {
|
||||
const { user, trip } = seedTrip();
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const file = await makeFile(trip.id, user.id, {}, { description: 'keep me', place_id: String(place.id) });
|
||||
const current = await svc.getFileById(file.id, trip.id)!;
|
||||
const current = (await svc.getFileById(file.id, trip.id))!;
|
||||
const updated = await svc.updateFile(file.id, current, {}) as Record<string, unknown>;
|
||||
expect(updated.description).toBe('keep me');
|
||||
expect(updated.place_id).toBe(place.id);
|
||||
@@ -354,7 +354,7 @@ describe('updateFile', () => {
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const reservation = createReservation(testDb, trip.id);
|
||||
const file = await makeFile(trip.id, user.id, {}, { description: 'old', place_id: String(place.id), reservation_id: String(reservation.id) });
|
||||
const current = await svc.getFileById(file.id, trip.id)!;
|
||||
const current = (await svc.getFileById(file.id, trip.id))!;
|
||||
const updated = await svc.updateFile(file.id, current, { description: '', place_id: '', reservation_id: null }) as Record<string, unknown>;
|
||||
expect(updated.description).toBeNull(); // '' → NULL on update too (post-migration fix: symmetric with createFile)
|
||||
expect(updated.place_id).toBeNull();
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { Request } from 'express';
|
||||
import { filesUploadFileFilter } from '../../../../src/nest/files/files.controller';
|
||||
import { journeyImageFileFilter } from '../../../../src/nest/journey/journey.controller';
|
||||
import type { AllowedFileTypesService } from '../../../../src/nest/files/allowed-file-types.service';
|
||||
|
||||
/**
|
||||
* Unit coverage for the two upload fileFilters that read the operator's
|
||||
* allowed-extension list asynchronously (files.controller.ts,
|
||||
* journey.controller.ts). Both filters run a synchronous pre-check first (a
|
||||
* blocked extension / non-image mimetype), then a detached async IIFE that
|
||||
* always answers through `cb` exactly once — even when the allow-list lookup
|
||||
* itself rejects (fail closed, never `cb(null, true)`).
|
||||
*/
|
||||
|
||||
const req = {} as Request;
|
||||
|
||||
function fakeFile(overrides: Partial<Express.Multer.File> = {}): Express.Multer.File {
|
||||
return {
|
||||
fieldname: 'file',
|
||||
originalname: 'photo.jpg',
|
||||
encoding: '7bit',
|
||||
mimetype: 'image/jpeg',
|
||||
size: 100,
|
||||
stream: undefined as never,
|
||||
destination: '',
|
||||
filename: '',
|
||||
path: '',
|
||||
buffer: undefined as never,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/** A stub whose get() can either resolve to a comma-separated list or reject. */
|
||||
function allowedTypesStub(result: string | Error): AllowedFileTypesService {
|
||||
return {
|
||||
get: vi.fn(() => (result instanceof Error ? Promise.reject(result) : Promise.resolve(result))),
|
||||
} as unknown as AllowedFileTypesService;
|
||||
}
|
||||
|
||||
/** Wait a couple of microtask turns so the filter's detached async IIFE settles. */
|
||||
async function flush(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('filesUploadFileFilter', () => {
|
||||
it('UPLOAD-FILTER-001: a blocked extension is rejected synchronously, before the allow-list is read', async () => {
|
||||
const allowedTypes = allowedTypesStub('jpg,png');
|
||||
const filter = filesUploadFileFilter(allowedTypes)!;
|
||||
const cb = vi.fn();
|
||||
filter(req, fakeFile({ originalname: 'malware.exe', mimetype: 'application/octet-stream' }), cb);
|
||||
await flush();
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
const err = cb.mock.calls[0][0] as (Error & { statusCode?: number }) | null;
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(err?.statusCode).toBe(400);
|
||||
expect(allowedTypes.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('UPLOAD-FILTER-002: an unresolvable allow-list rejects the file, never cb(null, true)', async () => {
|
||||
const allowedTypes = allowedTypesStub(new Error('db down'));
|
||||
const filter = filesUploadFileFilter(allowedTypes)!;
|
||||
const cb = vi.fn();
|
||||
filter(req, fakeFile({ originalname: 'photo.jpg', mimetype: 'image/jpeg' }), cb);
|
||||
await flush();
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
const [err, accept] = cb.mock.calls[0] as [(Error & { statusCode?: number }) | null, boolean | undefined];
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(accept).toBeUndefined();
|
||||
});
|
||||
|
||||
it('UPLOAD-FILTER-003: an allowed extension resolves cb(null, true) exactly once', async () => {
|
||||
const allowedTypes = allowedTypesStub('jpg,png');
|
||||
const filter = filesUploadFileFilter(allowedTypes)!;
|
||||
const cb = vi.fn();
|
||||
filter(req, fakeFile({ originalname: 'photo.jpg', mimetype: 'image/jpeg' }), cb);
|
||||
await flush();
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenCalledWith(null, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('journeyImageFileFilter', () => {
|
||||
it('UPLOAD-FILTER-004: a non-image mimetype is rejected synchronously, before the allow-list is read', async () => {
|
||||
const allowedTypes = allowedTypesStub('jpg,png');
|
||||
const filter = journeyImageFileFilter(allowedTypes)!;
|
||||
const cb = vi.fn();
|
||||
filter(req, fakeFile({ originalname: 'notes.pdf', mimetype: 'application/pdf' }), cb);
|
||||
await flush();
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
const err = cb.mock.calls[0][0] as (Error & { statusCode?: number }) | null;
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(err?.statusCode).toBe(400);
|
||||
expect(allowedTypes.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('UPLOAD-FILTER-005: an unresolvable allow-list rejects the file, never cb(null, true)', async () => {
|
||||
const allowedTypes = allowedTypesStub(new Error('db down'));
|
||||
const filter = journeyImageFileFilter(allowedTypes)!;
|
||||
const cb = vi.fn();
|
||||
filter(req, fakeFile({ originalname: 'photo.jpg', mimetype: 'image/jpeg' }), cb);
|
||||
await flush();
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
const [err, accept] = cb.mock.calls[0] as [(Error & { statusCode?: number }) | null, boolean | undefined];
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(accept).toBeUndefined();
|
||||
});
|
||||
|
||||
it('UPLOAD-FILTER-006: an allowed image extension resolves cb(null, true) exactly once', async () => {
|
||||
const allowedTypes = allowedTypesStub('jpg,png');
|
||||
const filter = journeyImageFileFilter(allowedTypes)!;
|
||||
const cb = vi.fn();
|
||||
filter(req, fakeFile({ originalname: 'photo.jpg', mimetype: 'image/jpeg' }), cb);
|
||||
await flush();
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenCalledWith(null, true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user