diff --git a/server/scripts/eslint-baseline.json b/server/scripts/eslint-baseline.json index f357ce8a8..bf60443dd 100644 --- a/server/scripts/eslint-baseline.json +++ b/server/scripts/eslint-baseline.json @@ -16,7 +16,7 @@ "no-misleading-character-class": 1 }, "tests": { - "@typescript-eslint/no-explicit-any": 1569, + "@typescript-eslint/no-explicit-any": 1567, "@typescript-eslint/no-non-null-assertion": 1757, "@typescript-eslint/no-require-imports": 25, "@typescript-eslint/no-unsafe-function-type": 7, diff --git a/server/scripts/service-http-baseline.json b/server/scripts/service-http-baseline.json index f122288a3..741483c4e 100644 --- a/server/scripts/service-http-baseline.json +++ b/server/scripts/service-http-baseline.json @@ -4,7 +4,6 @@ "src/nest/auth/auth.service.ts": 5, "src/nest/auth/passkey.controller.ts": 1, "src/nest/auth/passkey.service.ts": 2, - "src/nest/auth/registration-invites.service.ts": 1, "src/nest/backup/backup-archive.ts": 8, "src/nest/backup/backup.impl.ts": 1, "src/nest/collab/collab.controller.ts": 7, diff --git a/server/src/nest/auth/registration-invites.service.ts b/server/src/nest/auth/registration-invites.service.ts index b4dc5f597..32cd03d54 100644 --- a/server/src/nest/auth/registration-invites.service.ts +++ b/server/src/nest/auth/registration-invites.service.ts @@ -70,8 +70,9 @@ export class RegistrationInvitesService { const parsed = Number.parseInt(String(data.trip_id)); if (!Number.isInteger(parsed) || !(await this.trips.existsById(parsed))) { // Used to bind null silently, handing back a plain registration invite - // the admin never asked for. - return { error: 'Trip not found', status: 404 }; + // the admin never asked for. Thrown, so the controller writes neither + // the invite nor its audit row and the admin sees the 404. + throw new DomainError(404, 'Trip not found'); } tripId = parsed; } diff --git a/server/tests/integration/admin.test.ts b/server/tests/integration/admin.test.ts index 5dde1b122..80c7bf76d 100644 --- a/server/tests/integration/admin.test.ts +++ b/server/tests/integration/admin.test.ts @@ -19,7 +19,7 @@ import { resetTestDb, resetRateLimits } from '../helpers/test-db'; import type { FactoryOrm } from '../helpers/factories/context'; import { createUser, createAdmin, createInviteToken, createTrip, createBudgetItem, createJourney, createJourneyEntry, addJourneyContributor, addTripPhoto, createCategory, createTag, createTodoItem, createMcpToken, createBucketListItem, createVisitedCountry, createCollabNote, addTripMember } from '../helpers/factories'; import { authCookie } from '../helpers/auth'; -import { findRow, insertRow, insertRowIgnoringConflict, updateRows } from '../helpers/factories/rows'; +import { countRows, findRow, insertRow, insertRowIgnoringConflict, updateRows } from '../helpers/factories/rows'; import { makeShareToken } from '../helpers/factories/trips'; import { makeVacayPlan } from '../helpers/factories/vacay'; import { readUser } from '../helpers/factories/users'; @@ -548,6 +548,19 @@ describe('Invite token management', () => { expect(res.body.invite.token).toBeDefined(); }); + it('ADMIN-013b — POST /admin/invites with a trip that does not exist answers 404 and writes nothing', async () => { + const { user: admin } = createAdmin(testDb); + + const res = await request(app) + .post('/api/admin/invites') + .set('Cookie', authCookie(admin.id)) + .send({ max_uses: 1, trip_id: 99999 }); + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: 'Trip not found' }); + expect(await countRows(orm, InviteTokens, {})).toBe(0); + expect(await countRows(orm, AuditLog, { action: 'admin.invite_create' })).toBe(0); + }); + it('ADMIN-014 — DELETE /admin/invites/:id removes invite', async () => { const { user: admin } = createAdmin(testDb); const invite = createInviteToken(testDb, { created_by: admin.id }); diff --git a/server/tests/unit/nest/registration-invites.service.test.ts b/server/tests/unit/nest/registration-invites.service.test.ts index 3af28c3c7..cac28e32c 100644 --- a/server/tests/unit/nest/registration-invites.service.test.ts +++ b/server/tests/unit/nest/registration-invites.service.test.ts @@ -84,8 +84,8 @@ describe('Invites', () => { describe('Invites — trip binding', () => { it('ADMIN-SVC-073 — createInvite 404s on a trip_id that does not resolve', async () => { const { user: admin } = createAdmin(testDb); - expect(await svc.createInvite(admin.id, { trip_id: 99999 }) as any).toMatchObject({ status: 404, error: 'Trip not found' }); - expect(await svc.createInvite(admin.id, { trip_id: 'not-a-number' }) as any).toMatchObject({ status: 404 }); + await expect(svc.createInvite(admin.id, { trip_id: 99999 })).rejects.toMatchObject({ status: 404, publicMessage: 'Trip not found' }); + await expect(svc.createInvite(admin.id, { trip_id: 'not-a-number' })).rejects.toMatchObject({ status: 404 }); expect(await countRows(await sharedTestOrm(testDb), InviteTokens)).toBe(0); // An absent/blank binding is still a plain registration invite. expect((await svc.createInvite(admin.id, {}) as any).tripId).toBeNull();