mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
f3e5be78c9
Folds the legacy services/fileService.ts into the DI-native FilesService (injected DatabaseService, byte-identical SQL). Load-time constants move to files.constants.ts; files.bridge.ts survives with a single export for the request-time getAllowedExtensions read the module-scope multer configs need outside DI. TripsService and PluginHostDepsFactory inject FilesService. Also rides the nestjs-zod ratchet (FileUploadDto/FileUpdateDto/FileLinkDto over the @trek/shared contracts, FilesController entries dropped from the body-contract allow-list) and fixes the verified legacy quirks: file-link insert failures now surface instead of being swallowed, updateFile coerces an empty-string description to NULL like createFile, and formatFile's misleading optional trip_id annotation is gone.
121 lines
6.9 KiB
TypeScript
121 lines
6.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { dbMock } = vi.hoisted(() => {
|
|
const stmt = { get: vi.fn(() => ({ id: 42 })), all: vi.fn(() => []), run: vi.fn() };
|
|
return { dbMock: { prepare: vi.fn(() => stmt), _stmt: stmt } };
|
|
});
|
|
const { canAccessTrip } = vi.hoisted(() => ({ canAccessTrip: vi.fn(() => ({ user_id: 1 })) }));
|
|
vi.mock('../../../src/db/database', () => ({ db: dbMock, canAccessTrip, closeDb: () => {}, reinitialize: () => {} }));
|
|
import { db as dbConn } from '../../../src/db/database';
|
|
import { DatabaseService } from '../../../src/nest/database/database.service';
|
|
|
|
const { broadcast } = vi.hoisted(() => ({ broadcast: vi.fn() }));
|
|
vi.mock('../../../src/websocket', () => ({ broadcast }));
|
|
const { checkPermission } = vi.hoisted(() => ({ checkPermission: vi.fn(() => true) }));
|
|
vi.mock('../../../src/services/permissions', () => ({ checkPermission }));
|
|
|
|
const { tripSvc } = vi.hoisted(() => ({
|
|
tripSvc: {
|
|
listTrips: vi.fn(), createTrip: vi.fn(), getTrip: vi.fn(), updateTrip: vi.fn(), deleteTrip: vi.fn(),
|
|
getTripRaw: vi.fn(), getTripOwner: vi.fn(), deleteOldCover: vi.fn(), updateCoverImage: vi.fn(),
|
|
listMembers: vi.fn(() => ({ owner: { id: 1 }, members: [] })), addMember: vi.fn(), removeMember: vi.fn(),
|
|
transferOwnership: vi.fn(),
|
|
createGuest: vi.fn(), renameGuest: vi.fn(), deleteGuest: vi.fn(),
|
|
exportICS: vi.fn(), copyTripById: vi.fn(), TRIP_SELECT: 'SELECT * FROM trips t',
|
|
},
|
|
}));
|
|
vi.mock('../../../src/services/tripService', () => tripSvc);
|
|
vi.mock('../../../src/services/dayService', () => ({ listDays: () => ({ days: [1] }), listAccommodations: () => [] }));
|
|
vi.mock('../../../src/services/placeService', () => ({ listPlaces: () => [] }));
|
|
vi.mock('../../../src/services/budgetService', () => ({
|
|
listBudgetItems: () => [],
|
|
rebaseTripCurrency: vi.fn(async () => {}),
|
|
}));
|
|
vi.mock('../../../src/services/reservationService', () => ({ listReservations: () => [] }));
|
|
|
|
import { TripsService } from '../../../src/nest/trips/trips.service';
|
|
import type { TodoService } from '../../../src/nest/todo/todo.service';
|
|
import type { PackingService } from '../../../src/nest/packing/packing.service';
|
|
import type { FilesService } from '../../../src/nest/files/files.service';
|
|
import { rebaseTripCurrency } from '../../../src/services/budgetService';
|
|
|
|
const todoStub = { listItems: () => [] } as unknown as TodoService;
|
|
const packingListItems = vi.fn(() => []);
|
|
const packingStub = { listItems: packingListItems } as unknown as PackingService;
|
|
const filesStub = { listFiles: () => [] } as unknown as FilesService;
|
|
function svc() { return new TripsService(new DatabaseService(dbConn), todoStub, packingStub, filesStub); }
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
describe('TripsService (wrapper delegation + bundle/copy/notify helpers)', () => {
|
|
it('delegates the simple wrappers to tripService', async () => {
|
|
const s = svc();
|
|
s.list(1, 0); expect(tripSvc.listTrips).toHaveBeenCalledWith(1, 0);
|
|
s.create(1, { title: 'T' } as never); expect(tripSvc.createTrip).toHaveBeenCalledWith(1, { title: 'T' });
|
|
s.get('9', 1); expect(tripSvc.getTrip).toHaveBeenCalledWith('9', 1);
|
|
s.getRaw('9'); expect(tripSvc.getTripRaw).toHaveBeenCalledWith('9');
|
|
s.getOwner('9'); expect(tripSvc.getTripOwner).toHaveBeenCalledWith('9');
|
|
// update() first re-anchors the budget to any incoming currency, so it is async now.
|
|
await s.update('9', 1, {} as never, 'user'); expect(tripSvc.updateTrip).toHaveBeenCalledWith('9', 1, {}, 'user');
|
|
expect(rebaseTripCurrency).toHaveBeenCalledWith('9', undefined);
|
|
s.remove('9', 1, 'user'); expect(tripSvc.deleteTrip).toHaveBeenCalledWith('9', 1, 'user');
|
|
s.deleteOldCover('/old.jpg'); expect(tripSvc.deleteOldCover).toHaveBeenCalledWith('/old.jpg');
|
|
s.updateCoverImage('9', '/n.jpg'); expect(tripSvc.updateCoverImage).toHaveBeenCalledWith('9', '/n.jpg');
|
|
s.copy('9', 1, 'C'); expect(tripSvc.copyTripById).toHaveBeenCalledWith('9', 1, 'C');
|
|
s.listMembers('9', 1); expect(tripSvc.listMembers).toHaveBeenCalledWith('9', 1);
|
|
s.addMember('9', 'b@x.y', 1, 1); expect(tripSvc.addMember).toHaveBeenCalledWith('9', 'b@x.y', 1, 1);
|
|
s.removeMember('9', 2); expect(tripSvc.removeMember).toHaveBeenCalledWith('9', 2);
|
|
s.transferOwnership('9', 2, 1); expect(tripSvc.transferOwnership).toHaveBeenCalledWith('9', 2, 1);
|
|
s.createGuest('9', 'Anna', 1); expect(tripSvc.createGuest).toHaveBeenCalledWith('9', 'Anna', 1);
|
|
s.renameGuest('9', 7, 'Bob'); expect(tripSvc.renameGuest).toHaveBeenCalledWith('9', 7, 'Bob');
|
|
s.deleteGuest('9', 7); expect(tripSvc.deleteGuest).toHaveBeenCalledWith('9', 7);
|
|
s.exportICS('9'); expect(tripSvc.exportICS).toHaveBeenCalledWith('9');
|
|
});
|
|
|
|
it('re-anchors the budget before the trip row leaves its old currency (#1543)', async () => {
|
|
const order: string[] = [];
|
|
(rebaseTripCurrency as ReturnType<typeof vi.fn>).mockImplementationOnce(async () => { order.push('rebase'); });
|
|
(tripSvc.updateTrip as ReturnType<typeof vi.fn>).mockImplementationOnce(() => { order.push('update'); return {} as never; });
|
|
|
|
await svc().update('9', 1, { currency: 'RUB' } as never, 'user');
|
|
|
|
// The rebase reads the outgoing currency off the trip row, so it has to run first.
|
|
expect(rebaseTripCurrency).toHaveBeenCalledWith('9', 'RUB');
|
|
expect(order).toEqual(['rebase', 'update']);
|
|
});
|
|
|
|
it('canAccessTrip delegates to the db helper', () => {
|
|
canAccessTrip.mockReturnValueOnce({ user_id: 7 });
|
|
expect(svc().canAccessTrip('9', 7)).toEqual({ user_id: 7 });
|
|
expect(canAccessTrip).toHaveBeenCalledWith('9', 7);
|
|
});
|
|
|
|
it('can() delegates to checkPermission; broadcast forwards', () => {
|
|
svc().can('trip_edit', 'user', 1, 1, false);
|
|
expect(checkPermission).toHaveBeenCalledWith('trip_edit', 'user', 1, 1, false);
|
|
svc().broadcast('9', 'trip:updated', { a: 1 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('9', 'trip:updated', { a: 1 }, 'sock');
|
|
});
|
|
|
|
it('getCopiedTrip re-reads via the TRIP_SELECT query', () => {
|
|
expect(svc().getCopiedTrip(42, 1)).toEqual({ id: 42 });
|
|
expect(dbMock.prepare).toHaveBeenCalledWith(expect.stringContaining('SELECT * FROM trips t'));
|
|
});
|
|
|
|
it('bundle aggregates every sub-collection + the member list, scoping packing to the viewer (#858)', () => {
|
|
const result = svc().bundle('9', { user_id: 1 }, 7);
|
|
expect(result).toMatchObject({ trip: { user_id: 1 }, days: [1], places: [], members: [{ id: 1 }] });
|
|
// The viewer id is forwarded so other members' private items stay out of the offline cache.
|
|
expect(packingListItems).toHaveBeenCalledWith('9', 7);
|
|
});
|
|
|
|
it('bundle tolerates a null member list', () => {
|
|
tripSvc.listMembers.mockReturnValueOnce({ owner: { id: 1 }, members: null });
|
|
const result = svc().bundle('9', { user_id: 1 }, 1);
|
|
expect(result).toMatchObject({ members: [{ id: 1 }] });
|
|
});
|
|
|
|
it('notifyInvite is fire-and-forget (no throw)', () => {
|
|
expect(() => svc().notifyInvite('9', { id: 1, email: 'a@b.c' } as never, 2, 'T', 'b@x.y')).not.toThrow();
|
|
});
|
|
});
|