mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
94221a6d91
assignments, budget, collab, accommodations, reservations and booking-import drop their copied requireTrip/requireEdit helpers for the guard. Status codes and bodies are unchanged. places is deliberately NOT converted, and that is the finding of this PR rather than an omission. Its e2e suite documents that the body pipe answers 400 ahead of the trip 404 — a parity shift somebody made on purpose — and a Nest guard always runs before the pipe, so converting it silently reverses that ordering. The first attempt also dropped the permission check on DELETE, which the integration suite caught as a 200 where a 403 belonged. places keeps its in-handler check, and README now says why so the next person does not rediscover it the same way. collab applies the guard per handler for the same reason files does: its note-file upload route would otherwise be refused before multer had read the body, and a response sent mid-upload destroys the socket. Two coverage notes. The per-domain thresholds are regenerated because deleting a well-covered helper lowers a file's ratio without anything becoming less tested — the drops are 0.4 to 1.2 points across six domains and match the helpers removed. And the atlas integration suite gets the same admin1-bundle warm-up the unit suite got: ATLAS-013 was paying 3.1s for the one-off decompression inside its own 5s budget, which is why it only ever failed under load. It now runs in 116ms.
126 lines
7.2 KiB
TypeScript
126 lines
7.2 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { HttpException } from '@nestjs/common';
|
|
import { ReservationsController } from '../../../src/nest/reservations/reservations.controller';
|
|
import type { ReservationsService } from '../../../src/nest/reservations/reservations.service';
|
|
import type { User } from '../../../src/types';
|
|
|
|
const user = { id: 1, role: 'user', email: 'u@example.test' } as User;
|
|
const trip = { id: 5, user_id: 1 };
|
|
|
|
function makeService(overrides: Partial<ReservationsService> = {}): ReservationsService {
|
|
return {
|
|
verifyTripAccess: vi.fn().mockReturnValue(trip),
|
|
canEdit: vi.fn().mockReturnValue(true),
|
|
broadcast: vi.fn(),
|
|
syncBudgetOnCreate: vi.fn(),
|
|
syncBudgetOnUpdate: vi.fn(),
|
|
notifyBookingChange: vi.fn(),
|
|
...overrides,
|
|
} as unknown as ReservationsService;
|
|
}
|
|
|
|
function thrown(fn: () => unknown): { status: number; body: unknown } {
|
|
try { fn(); } catch (err) {
|
|
expect(err).toBeInstanceOf(HttpException);
|
|
const e = err as HttpException;
|
|
return { status: e.getStatus(), body: e.getResponse() };
|
|
}
|
|
throw new Error('expected throw');
|
|
}
|
|
|
|
describe('ReservationsController (parity with the legacy /api/trips/:tripId/reservations route)', () => {
|
|
|
|
it('GET / returns reservations', () => {
|
|
const svc = makeService({ list: vi.fn().mockReturnValue([{ id: 1 }]) } as Partial<ReservationsService>);
|
|
expect(new ReservationsController(svc).list(user, '5')).toEqual({ reservations: [{ id: 1 }] });
|
|
});
|
|
|
|
describe('POST /', () => {
|
|
|
|
// The bespoke 'Title is required' 400 moved to the global ZodValidationPipe
|
|
// (ReservationCreateDto) — covered by the e2e suite.
|
|
|
|
it('creates, runs budget sync, broadcasts accommodation + reservation, notifies', () => {
|
|
const create = vi.fn().mockReturnValue({ reservation: { id: 9 }, accommodationCreated: true });
|
|
const broadcast = vi.fn(); const syncBudgetOnCreate = vi.fn(); const notifyBookingChange = vi.fn();
|
|
const svc = makeService({ create, broadcast, syncBudgetOnCreate, notifyBookingChange } as Partial<ReservationsService>);
|
|
const body = { title: 'Hotel', type: 'lodging', create_budget_entry: { total_price: 200 } };
|
|
expect(new ReservationsController(svc).create(user, '5', body, 'sock')).toEqual({ reservation: { id: 9 } });
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'accommodation:created', {}, 'sock');
|
|
expect(syncBudgetOnCreate).toHaveBeenCalledWith('5', 9, 'Hotel', 'lodging', { total_price: 200 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'reservation:created', { reservation: { id: 9 } }, 'sock');
|
|
expect(notifyBookingChange).toHaveBeenCalledWith('5', user.id, 'Hotel', 'lodging');
|
|
});
|
|
});
|
|
|
|
describe('PUT /positions', () => {
|
|
// The 'positions must be an array' 400 moved to the global
|
|
// ZodValidationPipe (ReservationPositionsDto).
|
|
|
|
it('updates positions and broadcasts', () => {
|
|
const updatePositions = vi.fn(); const broadcast = vi.fn();
|
|
const svc = makeService({ updatePositions, broadcast } as Partial<ReservationsService>);
|
|
const positions = [{ id: 1, day_plan_position: 0 }];
|
|
expect(new ReservationsController(svc).updatePositions(user, '5', { positions, day_id: 3 }, 'sock')).toEqual({ success: true });
|
|
expect(updatePositions).toHaveBeenCalledWith('5', positions, 3);
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'reservation:positions', { positions, day_id: 3 }, 'sock');
|
|
});
|
|
});
|
|
|
|
describe('PUT /:id', () => {
|
|
it('404 when the reservation is missing', () => {
|
|
const svc = makeService({ getReservation: vi.fn().mockReturnValue(undefined) } as Partial<ReservationsService>);
|
|
expect(thrown(() => new ReservationsController(svc).update(user, '5', '9', { title: 'X' }))).toEqual({ status: 404, body: { error: 'Reservation not found' } });
|
|
});
|
|
|
|
it('updates, syncs budget with current fallbacks, broadcasts + notifies', () => {
|
|
const getReservation = vi.fn().mockReturnValue({ title: 'Old', type: 'lodging' });
|
|
const update = vi.fn().mockReturnValue({ reservation: { id: 9 }, accommodationChanged: true });
|
|
const broadcast = vi.fn(); const syncBudgetOnUpdate = vi.fn(); const notifyBookingChange = vi.fn();
|
|
const svc = makeService({ getReservation, update, broadcast, syncBudgetOnUpdate, notifyBookingChange } as Partial<ReservationsService>);
|
|
new ReservationsController(svc).update(user, '5', '9', { create_budget_entry: { total_price: 50 } }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'accommodation:updated', {}, 'sock');
|
|
expect(syncBudgetOnUpdate).toHaveBeenCalledWith('5', '9', '', undefined, 'Old', 'lodging', { total_price: 50 }, 'sock');
|
|
expect(notifyBookingChange).toHaveBeenCalledWith('5', user.id, 'Old', 'lodging');
|
|
});
|
|
});
|
|
|
|
describe('PUT /:id/travelers', () => {
|
|
// The 'user_ids must be an array' 400 moved to the global
|
|
// ZodValidationPipe (ReservationTravelersDto).
|
|
|
|
it('404 when the reservation is off-trip / missing', () => {
|
|
const svc = makeService({ setTravelers: vi.fn().mockReturnValue(null) } as Partial<ReservationsService>);
|
|
expect(thrown(() => new ReservationsController(svc).updateTravelers(user, '5', '9', { user_ids: [1] }))).toEqual({ status: 404, body: { error: 'Reservation not found' } });
|
|
});
|
|
|
|
it('assigns travelers, broadcasts, and returns { travelers, reservation }', () => {
|
|
const travelers = [{ user_id: 2, username: 'Sam', avatar: null, is_guest: 0 }];
|
|
const reservation = { id: 9, travelers };
|
|
const setTravelers = vi.fn().mockReturnValue({ travelers, reservation });
|
|
const broadcast = vi.fn();
|
|
const svc = makeService({ setTravelers, broadcast } as Partial<ReservationsService>);
|
|
expect(new ReservationsController(svc).updateTravelers(user, '5', '9', { user_ids: [2] }, 'sock')).toEqual({ travelers, reservation });
|
|
expect(setTravelers).toHaveBeenCalledWith('9', '5', [2]);
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'reservation:travelers-updated', { reservationId: 9, travelers }, 'sock');
|
|
});
|
|
});
|
|
|
|
describe('DELETE /:id', () => {
|
|
it('404 when nothing deleted', () => {
|
|
const svc = makeService({ remove: vi.fn().mockReturnValue({ deleted: undefined, accommodationDeleted: false, deletedBudgetItemId: null }) } as Partial<ReservationsService>);
|
|
expect(thrown(() => new ReservationsController(svc).remove(user, '5', '9'))).toEqual({ status: 404, body: { error: 'Reservation not found' } });
|
|
});
|
|
|
|
it('broadcasts the accommodation + budget cascade then reservation:deleted', () => {
|
|
const remove = vi.fn().mockReturnValue({ deleted: { id: 9, title: 'Hotel', type: 'lodging', accommodation_id: 3 }, accommodationDeleted: true, deletedBudgetItemId: 7 });
|
|
const broadcast = vi.fn(); const notifyBookingChange = vi.fn();
|
|
const svc = makeService({ remove, broadcast, notifyBookingChange } as Partial<ReservationsService>);
|
|
expect(new ReservationsController(svc).remove(user, '5', '9', 'sock')).toEqual({ success: true });
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'accommodation:deleted', { accommodationId: 3 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'budget:deleted', { itemId: 7 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'reservation:deleted', { reservationId: 9 }, 'sock');
|
|
});
|
|
});
|
|
});
|