/** * Trips API integration tests. * Covers TRIP-001 through TRIP-022. */ import { buildApp } from '../../src/bootstrap'; import { db as testDb } from '../../src/db/database'; import { AppSettings } from '../../src/db/entities/AppSettings.entity'; import { BudgetCategoryOrder } from '../../src/db/entities/BudgetCategoryOrder.entity'; import { BudgetItems } from '../../src/db/entities/BudgetItems.entity'; import { DayAccommodations } from '../../src/db/entities/DayAccommodations.entity'; import { DayAssignments } from '../../src/db/entities/DayAssignments.entity'; import { DayNotes } from '../../src/db/entities/DayNotes.entity'; import { Days } from '../../src/db/entities/Days.entity'; import { PackingItems } from '../../src/db/entities/PackingItems.entity'; import { Places } from '../../src/db/entities/Places.entity'; import { Reservations } from '../../src/db/entities/Reservations.entity'; import { Tags } from '../../src/db/entities/Tags.entity'; import { TodoItems } from '../../src/db/entities/TodoItems.entity'; import { TripMembers } from '../../src/db/entities/TripMembers.entity'; import { Trips } from '../../src/db/entities/Trips.entity'; import { Users } from '../../src/db/entities/Users.entity'; import { invalidatePermissionsCache } from '../../src/nest/permissions/permissions-cache'; import { authCookie } from '../helpers/auth'; import { createUser, createAdmin, createTrip, addTripMember, createPlace, createReservation, createTag, createDayAccommodation, createBudgetItem, createPackingItem, createDayNote, createDayAssignment, } from '../helpers/factories'; import type { FactoryOrm } from '../helpers/factories/context'; import { tagPlace } from '../helpers/factories/places'; import { countRows, findRow, findRows, insertRow, insertRows, updateRows } from '../helpers/factories/rows'; import { setAppSetting } from '../helpers/factories/settings'; import { readTripDays } from '../helpers/factories/trips'; import { readUser } from '../helpers/factories/users'; import { resetTestDb, resetRateLimits } from '../helpers/test-db'; import { MikroORM } from '@mikro-orm/core'; import type { INestApplication } from '@nestjs/common'; import type { Application } from 'express'; import request from 'supertest'; import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest'; // ───────────────────────────────────────────────────────────────────────────── // Step 1: Bare in-memory DB — schema applied in beforeAll after mocks register // ───────────────────────────────────────────────────────────────────────────── vi.mock('../../src/db/database', async () => { const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock'); return buildDbMock(createSnapshotTestDb()); }); let nestApp: INestApplication; let app: Application; let orm: FactoryOrm; beforeAll(async () => { nestApp = await buildApp(); app = nestApp.getHttpAdapter().getInstance(); orm = nestApp.get(MikroORM); }); beforeEach(async () => { resetTestDb(testDb); await resetRateLimits(nestApp); await invalidatePermissionsCache(); }); afterAll(async () => { await nestApp.close(); testDb.close(); }); // ───────────────────────────────────────────────────────────────────────────── // Create trip (TRIP-001, TRIP-002, TRIP-003) // ───────────────────────────────────────────────────────────────────────────── describe('Create trip', () => { it('TRIP-001 — POST /api/trips with start_date/end_date returns 201 and auto-generates days', async () => { const { user } = createUser(testDb); const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) .send({ title: 'Paris Adventure', start_date: '2026-06-01', end_date: '2026-06-05' }); expect(res.status).toBe(201); expect(res.body.trip).toBeDefined(); expect(res.body.trip.title).toBe('Paris Adventure'); // Verify days were generated (5 days: Jun 1–5) const days = await findRows(orm, Days, { trip: res.body.trip.id }, { date: 'asc' }); expect(days).toHaveLength(5); expect(days[0].date).toBe('2026-06-01'); expect(days[4].date).toBe('2026-06-05'); }); it('TRIP-002 — POST /api/trips without dates returns 201 and creates 7 dateless placeholder days', async () => { const { user } = createUser(testDb); const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) .send({ title: 'Open-ended Trip' }); expect(res.status).toBe(201); expect(res.body.trip).toBeDefined(); expect(res.body.trip.start_date).toBeNull(); expect(res.body.trip.end_date).toBeNull(); // Should have 7 dateless placeholder days const days = await readTripDays(orm, res.body.trip.id); expect(days).toHaveLength(7); expect(days[0].date).toBeNull(); }); it('TRIP-002b — POST /api/trips with day_count creates correct number of dateless days', async () => { const { user } = createUser(testDb); const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) .send({ title: 'Custom Days Trip', day_count: 20 }); expect(res.status).toBe(201); expect(res.body.trip.start_date).toBeNull(); expect(res.body.trip.day_count).toBe(20); const days = await findRows(orm, Days, { trip: res.body.trip.id }); expect(days).toHaveLength(20); }); it('TRIP-001 — POST /api/trips requires a title, returns 400 without one', async () => { const { user } = createUser(testDb); const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) .send({ description: 'No title here' }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/title/i); }); it('TRIP-001 — POST /api/trips rejects end_date before start_date with 400', async () => { const { user } = createUser(testDb); const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) .send({ title: 'Bad Dates', start_date: '2026-06-10', end_date: '2026-06-05' }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/end date/i); }); it('TRIP-003 — trip_create permission set to admin blocks regular user with 403', async () => { const { user } = createUser(testDb); // Restrict trip creation to admins only await insertRow(orm, AppSettings, { key: 'perm_trip_create', value: 'admin' }); await invalidatePermissionsCache(); const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) .send({ title: 'Forbidden Trip' }); expect(res.status).toBe(403); expect(res.body.error).toMatch(/permission/i); }); it('TRIP-003 — trip_create permission set to admin allows admin user', async () => { const { user: admin } = createAdmin(testDb); await insertRow(orm, AppSettings, { key: 'perm_trip_create', value: 'admin' }); await invalidatePermissionsCache(); const res = await request(app).post('/api/trips').set('Cookie', authCookie(admin.id)).send({ title: 'Admin Trip' }); expect(res.status).toBe(201); }); it('TRIP-001 — unauthenticated POST /api/trips returns 401', async () => { const res = await request(app).post('/api/trips').send({ title: 'No Auth' }); expect(res.status).toBe(401); }); }); // ───────────────────────────────────────────────────────────────────────────── // List trips (TRIP-004, TRIP-005) // ───────────────────────────────────────────────────────────────────────────── describe('List trips', () => { it('TRIP-004 — GET /api/trips returns own trips and member trips, not other users trips', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const { user: stranger } = createUser(testDb); const ownTrip = createTrip(testDb, owner.id, { title: "Owner's Trip" }); const memberTrip = createTrip(testDb, stranger.id, { title: "Stranger's Trip (member)" }); createTrip(testDb, stranger.id, { title: "Stranger's Private Trip" }); // Add member to one of stranger's trips addTripMember(testDb, memberTrip.id, member.id); const ownerRes = await request(app).get('/api/trips').set('Cookie', authCookie(owner.id)); expect(ownerRes.status).toBe(200); const ownerTripIds = ownerRes.body.trips.map((t: any) => t.id); expect(ownerTripIds).toContain(ownTrip.id); expect(ownerTripIds).not.toContain(memberTrip.id); const memberRes = await request(app).get('/api/trips').set('Cookie', authCookie(member.id)); expect(memberRes.status).toBe(200); const memberTripIds = memberRes.body.trips.map((t: any) => t.id); expect(memberTripIds).toContain(memberTrip.id); expect(memberTripIds).not.toContain(ownTrip.id); }); it('TRIP-005 — GET /api/trips excludes archived trips by default', async () => { const { user } = createUser(testDb); const activeTrip = createTrip(testDb, user.id, { title: 'Active Trip' }); const archivedTrip = createTrip(testDb, user.id, { title: 'Archived Trip' }); // Archive the second trip directly in the DB await updateRows(orm, Trips, { id: archivedTrip.id }, { is_archived: 1 }); const res = await request(app).get('/api/trips').set('Cookie', authCookie(user.id)); expect(res.status).toBe(200); const tripIds = res.body.trips.map((t: any) => t.id); expect(tripIds).toContain(activeTrip.id); expect(tripIds).not.toContain(archivedTrip.id); }); it('TRIP-005 — GET /api/trips?archived=1 returns only archived trips', async () => { const { user } = createUser(testDb); const activeTrip = createTrip(testDb, user.id, { title: 'Active Trip' }); const archivedTrip = createTrip(testDb, user.id, { title: 'Archived Trip' }); await updateRows(orm, Trips, { id: archivedTrip.id }, { is_archived: 1 }); const res = await request(app).get('/api/trips?archived=1').set('Cookie', authCookie(user.id)); expect(res.status).toBe(200); const tripIds = res.body.trips.map((t: any) => t.id); expect(tripIds).toContain(archivedTrip.id); expect(tripIds).not.toContain(activeTrip.id); }); }); // ───────────────────────────────────────────────────────────────────────────── // Get trip (TRIP-006, TRIP-007, TRIP-016, TRIP-017) // ───────────────────────────────────────────────────────────────────────────── describe('Get trip', () => { it('TRIP-006 — GET /api/trips/:id for own trip returns 200 with full trip object', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'My Trip', description: 'A lovely trip' }); const res = await request(app).get(`/api/trips/${trip.id}`).set('Cookie', authCookie(user.id)); expect(res.status).toBe(200); expect(res.body.trip).toBeDefined(); expect(res.body.trip.id).toBe(trip.id); expect(res.body.trip.title).toBe('My Trip'); expect(res.body.trip.is_owner).toBe(1); }); it('TRIP-007 — GET /api/trips/:id for another users trip returns 404', async () => { const { user: owner } = createUser(testDb); const { user: other } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: "Owner's Trip" }); const res = await request(app).get(`/api/trips/${trip.id}`).set('Cookie', authCookie(other.id)); expect(res.status).toBe(404); expect(res.body.error).toMatch(/not found/i); }); it('TRIP-017 — Member can access trip → 200', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Shared Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app).get(`/api/trips/${trip.id}`).set('Cookie', authCookie(member.id)); expect(res.status).toBe(200); expect(res.body.trip.id).toBe(trip.id); expect(res.body.trip.is_owner).toBe(0); }); it('TRIP-006 — GET /api/trips/:id for non-existent trip returns 404', async () => { const { user } = createUser(testDb); const res = await request(app).get('/api/trips/999999').set('Cookie', authCookie(user.id)); expect(res.status).toBe(404); }); }); // ───────────────────────────────────────────────────────────────────────────── // Update trip (TRIP-008, TRIP-009, TRIP-010) // ───────────────────────────────────────────────────────────────────────────── describe('Update trip', () => { it('TRIP-008 — PUT /api/trips/:id updates title and description for owner → 200', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Original Title' }); const res = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(user.id)) .send({ title: 'Updated Title', description: 'New description' }); expect(res.status).toBe(200); expect(res.body.trip.title).toBe('Updated Title'); expect(res.body.trip.description).toBe('New description'); }); it('TRIP-009 — Archive trip (PUT with is_archived:true) removes it from normal list', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'To Archive' }); const archiveRes = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(user.id)) .send({ is_archived: true }); expect(archiveRes.status).toBe(200); expect(archiveRes.body.trip.is_archived).toBe(1); // Should not appear in the normal list const listRes = await request(app).get('/api/trips').set('Cookie', authCookie(user.id)); const tripIds = listRes.body.trips.map((t: any) => t.id); expect(tripIds).not.toContain(trip.id); }); it('TRIP-009 — Unarchive trip reappears in normal list', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Archived Trip' }); // Archive it first await updateRows(orm, Trips, { id: trip.id }, { is_archived: 1 }); // Unarchive via API const unarchiveRes = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(user.id)) .send({ is_archived: false }); expect(unarchiveRes.status).toBe(200); expect(unarchiveRes.body.trip.is_archived).toBe(0); // Should appear in the normal list again const listRes = await request(app).get('/api/trips').set('Cookie', authCookie(user.id)); const tripIds = listRes.body.trips.map((t: any) => t.id); expect(tripIds).toContain(trip.id); }); it('TRIP-010 — Archive by trip member is denied when trip_archive is set to trip_owner', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Members Trip' }); addTripMember(testDb, trip.id, member.id); // Restrict archiving to trip_owner only (this is actually the default, but set explicitly) await setAppSetting(orm, 'perm_trip_archive', 'trip_owner'); await invalidatePermissionsCache(); const res = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(member.id)) .send({ is_archived: true }); expect(res.status).toBe(403); expect(res.body.error).toMatch(/permission/i); }); it('TRIP-008 — Member cannot edit trip title when trip_edit is set to trip_owner', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Original' }); addTripMember(testDb, trip.id, member.id); // Default trip_edit is trip_owner — members should be blocked await setAppSetting(orm, 'perm_trip_edit', 'trip_owner'); await invalidatePermissionsCache(); const res = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(member.id)) .send({ title: 'Hacked Title' }); expect(res.status).toBe(403); }); it('TRIP-008 — PUT /api/trips/:id returns 404 for non-existent trip', async () => { const { user } = createUser(testDb); const res = await request(app) .put('/api/trips/999999') .set('Cookie', authCookie(user.id)) .send({ title: 'Ghost Update' }); expect(res.status).toBe(404); }); it('TRIP-023 — Shifting trip date range preserves day assignments positionally', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { start_date: '2026-08-01', end_date: '2026-08-05' }); const days = await readTripDays(orm, trip.id); expect(days).toHaveLength(5); const place = createPlace(testDb, trip.id); const assignment = createDayAssignment(testDb, days[0].id, place.id); const note = createDayNote(testDb, days[1].id, trip.id, { text: 'pack sunscreen' }); // Shift forward 10 days (zero overlap with original range) const res = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(user.id)) .send({ start_date: '2026-08-11', end_date: '2026-08-15' }); expect(res.status).toBe(200); const daysAfter = await readTripDays(orm, trip.id); expect(daysAfter).toHaveLength(5); expect(daysAfter.map((d) => d.date)).toEqual([ '2026-08-11', '2026-08-12', '2026-08-13', '2026-08-14', '2026-08-15', ]); const assignmentsAfter = await findRow(orm, DayAssignments, { id: assignment.id }); expect(assignmentsAfter).not.toBeNull(); expect(assignmentsAfter!.day_id).toBe(daysAfter[0].id); const notesAfter = await findRow(orm, DayNotes, { id: note.id }); expect(notesAfter).not.toBeNull(); expect(notesAfter!.day_id).toBe(daysAfter[1].id); }); it('TRIP-024 — Shrinking trip date range deletes overflow days and their content', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-05' }); const days = await readTripDays(orm, trip.id); const place = createPlace(testDb, trip.id); const a4 = createDayAssignment(testDb, days[3].id, place.id); const a5 = createDayAssignment(testDb, days[4].id, place.id); // Shrink from 5 to 3 days const res = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(user.id)) .send({ start_date: '2026-09-01', end_date: '2026-09-03' }); expect(res.status).toBe(200); const daysAfter = await readTripDays(orm, trip.id); expect(daysAfter).toHaveLength(3); expect(daysAfter.every((d) => d.date !== null)).toBe(true); // Overflow days and their assignments deleted const all = await findRows(orm, DayAssignments, { id: { $in: [a4.id, a5.id] } }); expect(all).toHaveLength(0); }); it('TRIP-028: An earlier end takes the last day with its stay, while the hotel booking of that stay remains', async () => { // Pins what the trip dialog warns about before such a save: the stay goes as // a whole, even with nights still inside the trip, and its booking does not. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-06' }); const days = await readTripDays(orm, trip.id); const place = createPlace(testDb, trip.id, { name: 'Harbour Hotel' }); const stay = createDayAccommodation(testDb, trip.id, place.id, days[3].id, days[5].id); const booking = createReservation(testDb, trip.id, { title: 'Harbour Hotel booking', type: 'hotel', day_id: days[3].id, }); await updateRows(orm, Reservations, { id: booking.id }, { accommodation_id: String(stay.id) }); const sightseeing = createDayAssignment(testDb, days[5].id, place.id); const res = await request(app) .put(`/api/trips/${trip.id}`) .set('Cookie', authCookie(user.id)) .send({ start_date: '2026-09-01', end_date: '2026-09-05' }); expect(res.status).toBe(200); const daysAfter = await readTripDays(orm, trip.id); expect(daysAfter.map((d) => d.id)).toEqual(days.slice(0, 5).map((d) => d.id)); expect(daysAfter.at(-1)!.date).toBe('2026-09-05'); expect(await findRow(orm, DayAccommodations, { id: stay.id })).toBeNull(); expect(await findRow(orm, DayAssignments, { id: sightseeing.id })).toBeNull(); expect((await findRow(orm, Reservations, { id: booking.id }))?.title).toBe('Harbour Hotel booking'); expect(await findRow(orm, Places, { id: place.id })).not.toBeNull(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Delete trip (TRIP-018, TRIP-019, TRIP-022) // ───────────────────────────────────────────────────────────────────────────── describe('Delete trip', () => { it('TRIP-018 — DELETE /api/trips/:id by owner returns 200 and trip is no longer accessible', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'To Delete' }); const deleteRes = await request(app).delete(`/api/trips/${trip.id}`).set('Cookie', authCookie(user.id)); expect(deleteRes.status).toBe(200); expect(deleteRes.body.success).toBe(true); // Trip should no longer be accessible const getRes = await request(app).get(`/api/trips/${trip.id}`).set('Cookie', authCookie(user.id)); expect(getRes.status).toBe(404); }); it('TRIP-019 — Regular user cannot delete another users trip → 404', async () => { const { user: owner } = createUser(testDb); const { user: other } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: "Owner's Trip" }); const res = await request(app).delete(`/api/trips/${trip.id}`).set('Cookie', authCookie(other.id)); // 404, not 403: someone with no access at all must not be able to tell an // existing trip from a missing one by walking sequential ids. A member who // simply lacks trip_delete still gets 403 — see the case below. expect(res.status).toBe(404); // Trip still exists const tripInDb = await findRow(orm, Trips, { id: trip.id }); expect(tripInDb).not.toBeNull(); }); it('TRIP-019 — Trip member cannot delete trip → 403', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Shared Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app).delete(`/api/trips/${trip.id}`).set('Cookie', authCookie(member.id)); expect(res.status).toBe(403); expect(res.body.error).toMatch(/permission/i); }); it('TRIP-022 — Trip with places and reservations can be deleted (cascade)', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Trip With Data' }); // Add associated data createPlace(testDb, trip.id, { name: 'Eiffel Tower' }); createReservation(testDb, trip.id, { title: 'Hotel Booking', type: 'hotel' }); const deleteRes = await request(app).delete(`/api/trips/${trip.id}`).set('Cookie', authCookie(user.id)); expect(deleteRes.status).toBe(200); expect(deleteRes.body.success).toBe(true); // Verify cascade: places and reservations should be gone const places = await findRows(orm, Places, { trip: trip.id }); expect(places).toHaveLength(0); const reservations = await findRows(orm, Reservations, { trip: trip.id }); expect(reservations).toHaveLength(0); }); it('TRIP-018 — Admin can delete another users trip', async () => { const { user: admin } = createAdmin(testDb); const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: "User's Trip" }); const res = await request(app).delete(`/api/trips/${trip.id}`).set('Cookie', authCookie(admin.id)); expect(res.status).toBe(200); expect(res.body.success).toBe(true); }); it('TRIP-018 — DELETE /api/trips/:id for non-existent trip returns 404', async () => { const { user } = createUser(testDb); const res = await request(app).delete('/api/trips/999999').set('Cookie', authCookie(user.id)); expect(res.status).toBe(404); }); }); // ───────────────────────────────────────────────────────────────────────────── // Members (TRIP-013, TRIP-014, TRIP-015) // ───────────────────────────────────────────────────────────────────────────── describe('Trip members', () => { it('TRIP-015 — GET /api/trips/:id/members returns owner and members list', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app).get(`/api/trips/${trip.id}/members`).set('Cookie', authCookie(owner.id)); expect(res.status).toBe(200); expect(res.body.owner).toBeDefined(); expect(res.body.owner.id).toBe(owner.id); expect(Array.isArray(res.body.members)).toBe(true); expect(res.body.members.some((m: any) => m.id === member.id)).toBe(true); expect(res.body.current_user_id).toBe(owner.id); }); it('TRIP-013 — POST /api/trips/:id/members adds a member by email → 201', async () => { const { user: owner } = createUser(testDb); const { user: invitee } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); const res = await request(app) .post(`/api/trips/${trip.id}/members`) .set('Cookie', authCookie(owner.id)) .send({ identifier: invitee.email }); expect(res.status).toBe(201); expect(res.body.member).toBeDefined(); expect(res.body.member.email).toBe(invitee.email); expect(res.body.member.role).toBe('member'); // Verify in DB const dbEntry = await findRow(orm, TripMembers, { trip: trip.id, user: invitee.id }); expect(dbEntry).not.toBeNull(); }); it('TRIP-013 — POST /api/trips/:id/members adds a member by username → 201', async () => { const { user: owner } = createUser(testDb); const { user: invitee } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); const res = await request(app) .post(`/api/trips/${trip.id}/members`) .set('Cookie', authCookie(owner.id)) .send({ identifier: invitee.username }); expect(res.status).toBe(201); expect(res.body.member.id).toBe(invitee.id); }); it('TRIP-013 — Adding a non-existent user returns 404', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); const res = await request(app) .post(`/api/trips/${trip.id}/members`) .set('Cookie', authCookie(owner.id)) .send({ identifier: 'nobody@nowhere.example.com' }); expect(res.status).toBe(404); expect(res.body.error).toMatch(/user not found/i); }); it('TRIP-013 — Adding a user who is already a member returns 400', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app) .post(`/api/trips/${trip.id}/members`) .set('Cookie', authCookie(owner.id)) .send({ identifier: member.email }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/already/i); }); it('TRIP-013 — Adding a member by whitespace-padded username resolves correctly → 201', async () => { const { user: owner } = createUser(testDb); const { user: invitee } = createUser(testDb, { username: 'paddeduser' }); const trip = createTrip(testDb, owner.id, { title: 'Padded Trip' }); const res = await request(app) .post(`/api/trips/${trip.id}/members`) .set('Cookie', authCookie(owner.id)) .send({ identifier: ' paddeduser ' }); expect(res.status).toBe(201); expect(res.body.member.id).toBe(invitee.id); }); it('TRIP-014 — DELETE /api/trips/:id/members/:userId removes a member → 200', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app) .delete(`/api/trips/${trip.id}/members/${member.id}`) .set('Cookie', authCookie(owner.id)); expect(res.status).toBe(200); expect(res.body.success).toBe(true); // Verify removal in DB const dbEntry = await findRow(orm, TripMembers, { trip: trip.id, user: member.id }); expect(dbEntry).toBeNull(); }); it('TRIP-014 — Member can remove themselves from a trip → 200', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app) .delete(`/api/trips/${trip.id}/members/${member.id}`) .set('Cookie', authCookie(member.id)); expect(res.status).toBe(200); expect(res.body.success).toBe(true); }); it('TRIP-TRANSFER-001 — owner hands the trip to a member; roles swap (#973)', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app) .post(`/api/trips/${trip.id}/transfer`) .set('Cookie', authCookie(owner.id)) .send({ newOwnerId: member.id }); expect(res.status).toBe(201); expect(res.body.success).toBe(true); const row = await findRow(orm, Trips, { id: trip.id }); expect(row?.user_id).toBe(member.id); const memberRows = (await findRows(orm, TripMembers, { trip: trip.id })).map((r) => r.user_id); expect(memberRows).toContain(owner.id); expect(memberRows).not.toContain(member.id); }); it('TRIP-TRANSFER-002 — a non-owner cannot transfer ownership → 403', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app) .post(`/api/trips/${trip.id}/transfer`) .set('Cookie', authCookie(member.id)) .send({ newOwnerId: member.id }); expect(res.status).toBe(403); }); it('TRIP-TRANSFER-003 — cannot transfer to a non-member → 400', async () => { const { user: owner } = createUser(testDb); const { user: stranger } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); const res = await request(app) .post(`/api/trips/${trip.id}/transfer`) .set('Cookie', authCookie(owner.id)) .send({ newOwnerId: stranger.id }); expect(res.status).toBe(400); }); it('TRIP-GUEST-001 — owner creates a guest; it appears as a member and is shielded from auth (#1362)', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Camping' }); const created = await request(app) .post(`/api/trips/${trip.id}/guests`) .set('Cookie', authCookie(owner.id)) .send({ name: 'Grandma' }); expect(created.status).toBe(201); expect(created.body.member.is_guest).toBe(true); expect(created.body.member.username).toBe('Grandma'); const guestId = created.body.member.id; // Surfaces in the members list every assignment picker consumes. const members = await request(app).get(`/api/trips/${trip.id}/members`).set('Cookie', authCookie(owner.id)); const guest = members.body.members.find((m: any) => m.id === guestId); expect(guest).toBeTruthy(); expect(guest.is_guest).toBe(true); // NOT in the global user directory (the member-add picker source). const dir = await request(app).get('/api/auth/users').set('Cookie', authCookie(owner.id)); expect(dir.body.users.some((u: any) => u.id === guestId)).toBe(false); // The synthetic email can never authenticate (resolves as an unknown email). const email = (await readUser(orm, guestId)).email; const login = await request(app).post('/api/auth/login').send({ email, password: 'anything' }); expect(login.status).toBe(401); }); it('TRIP-GUEST-004 — the same guest name is allowed on two trips (no "Name 2", #1446)', async () => { const { user: owner } = createUser(testDb); const tripA = createTrip(testDb, owner.id, { title: 'Trip A' }); const tripB = createTrip(testDb, owner.id, { title: 'Trip B' }); const addJake = (tripId: number) => request(app).post(`/api/trips/${tripId}/guests`).set('Cookie', authCookie(owner.id)).send({ name: 'Jake' }); const a = await addJake(tripA.id); const b = await addJake(tripB.id); expect(a.status).toBe(201); expect(b.status).toBe(201); // both keep the plain name — the second is NOT auto-renamed to "Jake 2" expect(a.body.member.username).toBe('Jake'); expect(b.body.member.username).toBe('Jake'); expect(b.body.member.id).not.toBe(a.body.member.id); // and each trip's member list shows "Jake" (not the internal uuid handle) const membersB = await request(app).get(`/api/trips/${tripB.id}/members`).set('Cookie', authCookie(owner.id)); expect(membersB.body.members.find((m: any) => m.id === b.body.member.id).username).toBe('Jake'); }); it('TRIP-GUEST-002 — guest CRUD is owner-only', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Camping' }); addTripMember(testDb, trip.id, member.id); // A non-owner member cannot create a guest. const denied = await request(app) .post(`/api/trips/${trip.id}/guests`) .set('Cookie', authCookie(member.id)) .send({ name: 'Nope' }); expect(denied.status).toBe(403); const created = await request(app) .post(`/api/trips/${trip.id}/guests`) .set('Cookie', authCookie(owner.id)) .send({ name: 'Kid' }); const guestId = created.body.member.id; // Rename + delete by the owner. const renamed = await request(app) .put(`/api/trips/${trip.id}/guests/${guestId}`) .set('Cookie', authCookie(owner.id)) .send({ name: 'Junior' }); expect(renamed.status).toBe(200); // #1446: the human name lives in display_name now (username is a non-shown uuid handle) expect((await readUser(orm, guestId)).display_name).toBe('Junior'); const removed = await request(app) .delete(`/api/trips/${trip.id}/guests/${guestId}`) .set('Cookie', authCookie(owner.id)); expect(removed.status).toBe(200); expect(await findRow(orm, Users, { id: guestId })).toBeNull(); }); it('TRIP-GUEST-003 — a guest cannot be invited as a member to any trip (#1362)', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Camping' }); const otherTrip = createTrip(testDb, owner.id, { title: 'Other' }); const created = await request(app) .post(`/api/trips/${trip.id}/guests`) .set('Cookie', authCookie(owner.id)) .send({ name: 'Eve' }); const email = (await readUser(orm, created.body.member.id)).email; const invite = await request(app) .post(`/api/trips/${otherTrip.id}/members`) .set('Cookie', authCookie(owner.id)) .send({ identifier: email }); expect(invite.status).toBe(404); }); it('TRIP-013 — Non-owner member cannot add other members when member_manage is trip_owner', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const { user: invitee } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Team Trip' }); addTripMember(testDb, trip.id, member.id); // Restrict member management to trip_owner (default) await setAppSetting(orm, 'perm_member_manage', 'trip_owner'); await invalidatePermissionsCache(); const res = await request(app) .post(`/api/trips/${trip.id}/members`) .set('Cookie', authCookie(member.id)) .send({ identifier: invitee.email }); expect(res.status).toBe(403); expect(res.body.error).toMatch(/permission/i); }); it('TRIP-015 — Non-member cannot list trip members → 404', async () => { const { user: owner } = createUser(testDb); const { user: stranger } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Private Trip' }); const res = await request(app).get(`/api/trips/${trip.id}/members`).set('Cookie', authCookie(stranger.id)); expect(res.status).toBe(404); }); }); // ───────────────────────────────────────────────────────────────────────────── // Copy trip (TRIP-023, TRIP-024) // ───────────────────────────────────────────────────────────────────────────── describe('Copy trip', () => { it('TRIP-023 — POST /api/trips/:id/copy creates a duplicate trip with 201', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Original Trip', description: 'Desc' }); const res = await request(app).post(`/api/trips/${trip.id}/copy`).set('Cookie', authCookie(user.id)).send({}); expect(res.status).toBe(201); expect(res.body.trip).toBeDefined(); expect(res.body.trip.id).not.toBe(trip.id); expect(res.body.trip.title).toBe('Original Trip'); }); it('TRIP-023 — copy accepts a custom title for the new trip', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Source' }); const res = await request(app) .post(`/api/trips/${trip.id}/copy`) .set('Cookie', authCookie(user.id)) .send({ title: 'Custom Copy' }); expect(res.status).toBe(201); expect(res.body.trip.title).toBe('Custom Copy'); }); it('TRIP-023 — copied trip belongs to the requesting user', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Shared Trip' }); addTripMember(testDb, trip.id, member.id); const res = await request(app).post(`/api/trips/${trip.id}/copy`).set('Cookie', authCookie(member.id)).send({}); expect(res.status).toBe(201); const newTrip = await findRow(orm, Trips, { id: res.body.trip.id }); expect(newTrip?.user_id).toBe(member.id); }); it('TRIP-024 — non-member cannot copy a trip → 404', async () => { const { user: owner } = createUser(testDb); const { user: stranger } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Private Trip' }); const res = await request(app).post(`/api/trips/${trip.id}/copy`).set('Cookie', authCookie(stranger.id)).send({}); expect(res.status).toBe(404); }); it('TRIP-024 — copy of non-existent trip returns 404', async () => { const { user } = createUser(testDb); const res = await request(app).post('/api/trips/999999/copy').set('Cookie', authCookie(user.id)).send({}); expect(res.status).toBe(404); }); }); // ───────────────────────────────────────────────────────────────────────────── // ICS export (TRIP-025) // ───────────────────────────────────────────────────────────────────────────── describe('ICS export', () => { it('TRIP-025 — GET /api/trips/:id/export.ics returns text/calendar content', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Calendar Trip' }); const res = await request(app).get(`/api/trips/${trip.id}/export.ics`).set('Cookie', authCookie(user.id)); expect(res.status).toBe(200); expect(res.headers['content-type']).toMatch(/text\/calendar/); expect(res.text).toContain('BEGIN:VCALENDAR'); expect(res.text).toContain('END:VCALENDAR'); }); it('TRIP-025 — non-member cannot export ICS → 404', async () => { const { user: owner } = createUser(testDb); const { user: stranger } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Private Trip' }); const res = await request(app).get(`/api/trips/${trip.id}/export.ics`).set('Cookie', authCookie(stranger.id)); expect(res.status).toBe(404); }); it('TRIP-025 — unauthenticated export returns 401', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Trip' }); const res = await request(app).get(`/api/trips/${trip.id}/export.ics`); expect(res.status).toBe(401); }); it('TRIP-025b — the trip payload never carries feed_token, not even for the owner', async () => { // feed_token is the sole credential for the anonymous ICS feed. TRIP_SELECT // reads `t.*`, so without the blanking it ships in every trip response and // gating /feed/token on share_manage would achieve nothing: any member // could read the value straight out of GET /api/trips/:id. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Trip' }); await updateRows(orm, Trips, { id: trip.id }, { feed_token: 'secret-feed-token' }); const one = await request(app).get(`/api/trips/${trip.id}`).set('Cookie', authCookie(user.id)); expect(one.status).toBe(200); expect(one.body.trip.feed_token ?? null).toBeNull(); expect(JSON.stringify(one.body)).not.toContain('secret-feed-token'); const list = await request(app).get('/api/trips').set('Cookie', authCookie(user.id)); expect(JSON.stringify(list.body)).not.toContain('secret-feed-token'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Copy trip with full data (covers loop bodies in the copy transaction) // ───────────────────────────────────────────────────────────────────────────── describe('Copy trip with data', () => { it('TRIP-026 — copy preserves days, places, tags, assignments, accommodations, reservations, budget, packing, notes', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Data-Rich Trip', start_date: '2025-09-01', end_date: '2025-09-03', }); const days = await readTripDays(orm, trip.id); expect(days.length).toBe(3); // Place with a tag const place = createPlace(testDb, trip.id, { name: 'Tower Bridge' }); const tag = createTag(testDb, user.id, { name: 'Landmark' }); await tagPlace(orm, place.id, [tag.id]); // Day assignment await insertRow(orm, DayAssignments, { day: days[0].id, place: place.id, order_index: 0, notes: 'Visit in morning', }); // Accommodation spanning days 0→1 createDayAccommodation(testDb, trip.id, place.id, days[0].id, days[1].id); // Reservation on day 0 createReservation(testDb, trip.id, { title: 'Flight Out', type: 'flight', day_id: days[0].id }); // Budget item createBudgetItem(testDb, trip.id, { name: 'Flights', total_price: 400 }); // Packing item createPackingItem(testDb, trip.id, { name: 'Toothbrush' }); // Day note createDayNote(testDb, days[0].id, trip.id, { text: 'Pack early!' }); const res = await request(app) .post(`/api/trips/${trip.id}/copy`) .set('Cookie', authCookie(user.id)) .send({ title: 'Data-Rich Trip (Copy)' }); expect(res.status).toBe(201); const newId = res.body.trip.id; expect(newId).not.toBe(trip.id); // Days copied const newDays = await readTripDays(orm, newId); expect(newDays).toHaveLength(3); // Place copied const newPlaces = await findRows(orm, Places, { trip: newId }); expect(newPlaces).toHaveLength(1); expect(newPlaces[0].name).toBe('Tower Bridge'); // Place tag copied // One place_tags row per (place, tag) pair on the copy's places. let newTags = 0; for (const p of newPlaces) newTags += await countRows(orm, Tags, { place_tags_inverse: p.id }); expect(newTags).toBe(1); // Assignment copied const newAssignments = await findRows(orm, DayAssignments, { day: { trip: newId } }); expect(newAssignments).toHaveLength(1); // Accommodation copied const newAccom = await findRows(orm, DayAccommodations, { trip: newId }); expect(newAccom).toHaveLength(1); // Reservation copied const newResv = await findRows(orm, Reservations, { trip: newId }); expect(newResv).toHaveLength(1); // Budget copied const newBudget = await findRows(orm, BudgetItems, { trip: newId }); expect(newBudget).toHaveLength(1); // Packing copied (checked reset to 0) const newPacking = await findRows(orm, PackingItems, { trip: newId }); expect(newPacking).toHaveLength(1); expect(newPacking[0].checked).toBe(0); // Day note copied const newNotes = await findRows(orm, DayNotes, { trip: newId }); expect(newNotes).toHaveLength(1); expect(newNotes[0].text).toBe('Pack early!'); }); it('TRIP-027 — copy preserves todos (unchecked, unassigned) and budget category order', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Todo Trip' }); // Two todos: one checked and assigned — both should arrive unchecked and unassigned await insertRow(orm, TodoItems, { trip: trip.id, name: 'Buy tickets', checked: 0, category: 'Transport', sort_order: 0, due_date: '2026-06-01', description: 'Check Ryanair', priority: 1, }); await insertRow(orm, TodoItems, { trip: trip.id, name: 'Book hotel', checked: 1, category: 'Accommodation', sort_order: 1, assignedUser: user.id, priority: 0, }); // Two budget category order rows await insertRows(orm, BudgetCategoryOrder, [ { trip: trip.id, category: 'Transport', sort_order: 0 }, { trip: trip.id, category: 'Accommodation', sort_order: 1 }, ]); const res = await request(app) .post(`/api/trips/${trip.id}/copy`) .set('Cookie', authCookie(user.id)) .send({ title: 'Todo Trip (Copy)' }); expect(res.status).toBe(201); const newId = res.body.trip.id; // Todos copied with checked reset and assigned_user_id nulled const newTodos = await findRows(orm, TodoItems, { trip: newId }, { sort_order: 'asc' }); expect(newTodos).toHaveLength(2); expect(newTodos[0].name).toBe('Buy tickets'); expect(newTodos[0].category).toBe('Transport'); expect(newTodos[0].checked).toBe(0); expect(newTodos[0].assigned_user_id).toBeNull(); expect(newTodos[0].due_date).toBe('2026-06-01'); expect(newTodos[0].description).toBe('Check Ryanair'); expect(newTodos[0].priority).toBe(1); expect(newTodos[1].name).toBe('Book hotel'); expect(newTodos[1].checked).toBe(0); expect(newTodos[1].assigned_user_id).toBeNull(); // Budget category order copied const newOrder = await findRows(orm, BudgetCategoryOrder, { trip: newId }, { sort_order: 'asc' }); expect(newOrder).toHaveLength(2); expect(newOrder[0]).toMatchObject({ category: 'Transport', sort_order: 0 }); expect(newOrder[1]).toMatchObject({ category: 'Accommodation', sort_order: 1 }); }); }); // ───────────────────────────────────────────────────────────────────────────── // Bundle endpoint — GET /api/trips/:id/bundle // ───────────────────────────────────────────────────────────────────────────── describe('Trip bundle', () => { it('BUNDLE-001 — returns all sub-collections for owned trip', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { start_date: '2026-07-01', end_date: '2026-07-03' }); const res = await request(app).get(`/api/trips/${trip.id}/bundle`).set('Cookie', authCookie(user.id)); expect(res.status).toBe(200); expect(res.body.trip).toBeDefined(); expect(res.body.trip.id).toBe(trip.id); expect(Array.isArray(res.body.days)).toBe(true); expect(res.body.days).toHaveLength(3); expect(Array.isArray(res.body.places)).toBe(true); expect(Array.isArray(res.body.packingItems)).toBe(true); expect(Array.isArray(res.body.todoItems)).toBe(true); expect(Array.isArray(res.body.budgetItems)).toBe(true); expect(Array.isArray(res.body.reservations)).toBe(true); expect(Array.isArray(res.body.files)).toBe(true); }); it('BUNDLE-002 — returns 404 for trip that does not exist', async () => { const { user } = createUser(testDb); const res = await request(app).get('/api/trips/999999/bundle').set('Cookie', authCookie(user.id)); expect(res.status).toBe(404); }); it('BUNDLE-003 — returns 404 when user has no access to trip', async () => { const { user: owner } = createUser(testDb); const { user: other } = createUser(testDb); const trip = createTrip(testDb, owner.id); const res = await request(app).get(`/api/trips/${trip.id}/bundle`).set('Cookie', authCookie(other.id)); expect(res.status).toBe(404); }); it('BUNDLE-004 — members can fetch bundle', async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id); await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); const res = await request(app).get(`/api/trips/${trip.id}/bundle`).set('Cookie', authCookie(member.id)); expect(res.status).toBe(200); expect(res.body.trip.id).toBe(trip.id); }); it('BUNDLE-005 — returns 401 when unauthenticated', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const res = await request(app).get(`/api/trips/${trip.id}/bundle`); expect(res.status).toBe(401); }); it("BUNDLE-006 — packingItems are scoped to the viewer: another member's private item stays out (#858)", async () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id); await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); await insertRow(orm, PackingItems, { trip: trip.id, name: 'Tent', checked: 0, sort_order: 0 }); await insertRow(orm, PackingItems, { trip: trip.id, name: 'Secret gift', checked: 0, sort_order: 1, is_private: 1, owner: owner.id, }); const ownerView = await request(app).get(`/api/trips/${trip.id}/bundle`).set('Cookie', authCookie(owner.id)); expect(ownerView.body.packingItems.map((i: { name: string }) => i.name).sort()).toEqual(['Secret gift', 'Tent']); const memberView = await request(app).get(`/api/trips/${trip.id}/bundle`).set('Cookie', authCookie(member.id)); expect(memberView.body.packingItems.map((i: { name: string }) => i.name)).toEqual(['Tent']); }); }); // ───────────────────────────────────────────────────────────────────────────── // Cover upload parity (TRIP-P01…P05) — written BEFORE the storage-upload swap. // ───────────────────────────────────────────────────────────────────────────── describe('Trip cover upload parity', () => { const fsMod = require('fs') as typeof import('fs'); const pathMod = require('path') as typeof import('path'); const FIXTURE_IMG = pathMod.join(__dirname, '../fixtures/small-image.jpg'); const coversDir = pathMod.join(__dirname, '../../uploads/covers'); afterAll(() => { fsMod.rmSync(coversDir, { recursive: true, force: true }); }); it('TRIP-P01 — cover upload returns /uploads/covers/ and writes the file', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const res = await request(app) .post(`/api/trips/${trip.id}/cover`) .set('Cookie', authCookie(user.id)) .attach('cover', FIXTURE_IMG, 'cover.png'); expect(res.status).toBe(201); expect(res.body.cover_image).toMatch(/^\/uploads\/covers\/[0-9a-f-]{36}\.png$/); const diskName = res.body.cover_image.replace('/uploads/covers/', ''); expect(fsMod.existsSync(pathMod.join(coversDir, diskName))).toBe(true); }); it('TRIP-P02 — re-upload deletes the previous cover file', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const first = await request(app) .post(`/api/trips/${trip.id}/cover`) .set('Cookie', authCookie(user.id)) .attach('cover', FIXTURE_IMG, 'one.jpg'); const firstName = first.body.cover_image.replace('/uploads/covers/', ''); expect(fsMod.existsSync(pathMod.join(coversDir, firstName))).toBe(true); const second = await request(app) .post(`/api/trips/${trip.id}/cover`) .set('Cookie', authCookie(user.id)) .attach('cover', FIXTURE_IMG, 'two.jpg'); expect(second.status).toBe(201); expect(fsMod.existsSync(pathMod.join(coversDir, firstName))).toBe(false); }); it('TRIP-P03 — no file → 400 "No image uploaded"', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const res = await request(app).post(`/api/trips/${trip.id}/cover`).set('Cookie', authCookie(user.id)); expect(res.status).toBe(400); expect(res.body.error).toBe('No image uploaded'); }); it('TRIP-P04 — non-image upload is 500 (plain-Error filter quirk — pinned, do not "fix")', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const res = await request(app) .post(`/api/trips/${trip.id}/cover`) .set('Cookie', authCookie(user.id)) .attach('cover', Buffer.from('plain text'), { filename: 'doc.txt', contentType: 'text/plain' }); expect(res.status).toBe(500); }); it('TRIP-P05 — unknown trip → 404 "Trip not found"', async () => { const { user } = createUser(testDb); const res = await request(app) .post('/api/trips/999999/cover') .set('Cookie', authCookie(user.id)) .attach('cover', FIXTURE_IMG); expect(res.status).toBe(404); expect(res.body.error).toBe('Trip not found'); }); });