mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
128 lines
5.5 KiB
TypeScript
128 lines
5.5 KiB
TypeScript
import type { BookingImportService } from '../../../../src/nest/booking-import/booking-import.service';
|
|
import { ReservationImportController } from '../../../../src/nest/reservation-import/reservation-import.controller';
|
|
import type { User } from '../../../../src/types';
|
|
import { HttpException, type Type } from '@nestjs/common';
|
|
import { INTERCEPTORS_METADATA } from '@nestjs/common/constants';
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const user = { id: 1, role: 'user' } as User;
|
|
const file = (name = 'a.pdf') => ({ originalname: name, buffer: Buffer.from('x') }) as Express.Multer.File;
|
|
|
|
function make(over: Partial<BookingImportService> = {}) {
|
|
const svc = {
|
|
verifyTripAccess: vi.fn(() => ({ user_id: 1 })),
|
|
canEdit: vi.fn(() => true),
|
|
isAvailable: vi.fn(() => true),
|
|
aiAvailable: vi.fn(() => true),
|
|
readsImages: vi.fn(async () => true),
|
|
preview: vi.fn(async () => ({ items: [], warnings: [], files: [] })),
|
|
...over,
|
|
} as unknown as BookingImportService;
|
|
// airtrailImport is the third slot; these cases never reach it.
|
|
return { c: new ReservationImportController(svc, undefined as never, undefined as never), svc };
|
|
}
|
|
|
|
async function status(fn: () => Promise<unknown>): Promise<number> {
|
|
try {
|
|
await fn();
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(HttpException);
|
|
return (e as HttpException).getStatus();
|
|
}
|
|
throw new Error('expected throw');
|
|
}
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
describe('ReservationImportController.preview', () => {
|
|
it('rejects an invalid mode with 400', async () => {
|
|
const { c } = make();
|
|
expect(await status(() => c.preview(user, 't1', [file()], { mode: 'bogus' }))).toBe(400);
|
|
});
|
|
|
|
it('returns 409 for force-ai when AI is not configured', async () => {
|
|
const { c } = make({ aiAvailable: vi.fn(() => false) as any });
|
|
expect(await status(() => c.preview(user, 't1', [file()], { mode: 'force-ai' }))).toBe(409);
|
|
});
|
|
|
|
it('returns 503 for no-ai when the extractor is unavailable', async () => {
|
|
const { c } = make({ isAvailable: vi.fn(() => false) as any });
|
|
expect(await status(() => c.preview(user, 't1', [file()], { mode: 'no-ai' }))).toBe(503);
|
|
});
|
|
|
|
it('returns 400 when no files are uploaded', async () => {
|
|
const { c } = make();
|
|
expect(await status(() => c.preview(user, 't1', [], { mode: 'no-ai' }))).toBe(400);
|
|
});
|
|
|
|
it('passes the parsed mode and user id through to the service', async () => {
|
|
const { c, svc } = make();
|
|
await c.preview(user, 't1', [file()], { mode: 'fallback-on-empty' });
|
|
expect(svc.preview).toHaveBeenCalledWith([expect.anything()], 'fallback-on-empty', 1);
|
|
});
|
|
|
|
it('takes a photo when the model reads images, without asking otherwise', async () => {
|
|
const { c, svc } = make();
|
|
await c.preview(user, 't1', [file('ticket.JPG')], { mode: 'fallback-on-empty' });
|
|
expect(svc.preview).toHaveBeenCalledWith([expect.anything()], 'fallback-on-empty', 1);
|
|
(svc.readsImages as ReturnType<typeof vi.fn>).mockClear();
|
|
await c.preview(user, 't1', [file('a.pdf')], { mode: 'fallback-on-empty' });
|
|
expect(svc.readsImages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('refuses a photo with 400 when the model does not read images, or AI is not asked', async () => {
|
|
const { c, svc } = make({ readsImages: vi.fn(async () => false) });
|
|
expect(
|
|
await status(() => c.preview(user, 't1', [file('a.pdf'), file('ticket.png')], { mode: 'fallback-on-empty' })),
|
|
).toBe(400);
|
|
const { c: c2 } = make();
|
|
expect(await status(() => c2.preview(user, 't1', [file('ticket.png')], { mode: 'no-ai' }))).toBe(400);
|
|
expect(svc.preview).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('still refuses a file type it does not know, HEIC included', async () => {
|
|
const { c } = make();
|
|
expect(await status(() => c.preview(user, 't1', [file('IMG_1.heic')], { mode: 'fallback-on-empty' }))).toBe(400);
|
|
});
|
|
|
|
it('names the photo formats in that refusal too', async () => {
|
|
const { c } = make();
|
|
const err = await c
|
|
.preview(user, 't1', [file('IMG_1.heic')], { mode: 'fallback-on-empty' })
|
|
.catch((e: unknown) => e);
|
|
expect((err as HttpException).getResponse()).toEqual({
|
|
error:
|
|
'Unsupported file type: IMG_1.heic. Accepted: EML, PDF, PKPass, HTML, TXT, JPG, JPEG, PNG, WEBP (photos when the AI model reads images)',
|
|
});
|
|
});
|
|
|
|
it('defaults the mode to no-ai when omitted', async () => {
|
|
const { c, svc } = make();
|
|
await c.preview(user, 't1', [file()], {});
|
|
expect(svc.preview).toHaveBeenCalledWith([expect.anything()], 'no-ai', 1);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Multer decodes a multipart filename as latin1 unless told otherwise, and the
|
|
* booking import did not tell it: "Bestätigung.pdf" reached the warnings as
|
|
* "Bestätigung.pdf", and the review could not attach the file it no longer
|
|
* recognised by name (#2477). Read off the interceptor both upload routes carry.
|
|
*/
|
|
describe('ReservationImportController upload options', () => {
|
|
const multerOf = (handler: keyof ReservationImportController) => {
|
|
const [Interceptor] = Reflect.getMetadata(
|
|
INTERCEPTORS_METADATA,
|
|
ReservationImportController.prototype[handler],
|
|
) as Type<{ multer: { defParamCharset: string; limits: unknown } }>[];
|
|
return new Interceptor().multer;
|
|
};
|
|
|
|
it.each(['preview', 'previewAsync'] as const)('%s decodes non-ASCII filenames as UTF-8', (handler) => {
|
|
const multer = multerOf(handler);
|
|
expect(multer.defParamCharset).toBe('utf8');
|
|
expect(multer.limits).toEqual({ fileSize: 10 * 1024 * 1024, files: 5 });
|
|
});
|
|
});
|