fix(admin): an invite for a trip that does not exist answers 404 instead of an empty 201

createInvite returned { error: 'Trip not found', status: 404 } and the admin controller never looked at it: POST /api/admin/invites with an unknown trip_id answered 201 with an empty body and wrote an admin.invite_create audit row for resource 'undefined'. The service throws the DomainError now, so the route answers 404 { error: 'Trip not found' } and writes neither the invite nor the audit row.
This commit is contained in:
Maurice
2026-10-10 19:52:57 +02:00
parent 8fb888c8a3
commit 8187a423e2
5 changed files with 20 additions and 7 deletions
+1 -1
View File
@@ -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,
@@ -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,
@@ -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;
}
+14 -1
View File
@@ -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 });
@@ -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();