Spaces:
Sleeping
Sleeping
| /** | |
| * Trips API integration tests. | |
| * Covers TRIP-001 through TRIP-022. | |
| */ | |
| import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest'; | |
| import request from 'supertest'; | |
| import type { Application } from 'express'; | |
| import type { INestApplication } from '@nestjs/common'; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Step 1: Bare in-memory DB β schema applied in beforeAll after mocks register | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const { testDb, dbMock } = vi.hoisted(() => { | |
| const Database = require('better-sqlite3'); | |
| const db = new Database(':memory:'); | |
| db.exec('PRAGMA journal_mode = WAL'); | |
| db.exec('PRAGMA foreign_keys = ON'); | |
| db.exec('PRAGMA busy_timeout = 5000'); | |
| const mock = { | |
| db, | |
| closeDb: () => {}, | |
| reinitialize: () => {}, | |
| getPlaceWithTags: (placeId: number) => { | |
| const place: any = db.prepare(` | |
| SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon | |
| FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ? | |
| `).get(placeId); | |
| if (!place) return null; | |
| const tags = db.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`).all(placeId); | |
| return { ...place, category: place.category_id ? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon } : null, tags }; | |
| }, | |
| canAccessTrip: (tripId: any, userId: number) => | |
| db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId), | |
| isOwner: (tripId: any, userId: number) => | |
| !!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId), | |
| }; | |
| return { testDb: db, dbMock: mock }; | |
| }); | |
| vi.mock('../../src/db/database', () => dbMock); | |
| vi.mock('../../src/config', () => ({ | |
| JWT_SECRET: 'test-jwt-secret-for-trek-testing-only', | |
| ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2', | |
| updateJwtSecret: () => {}, | |
| SESSION_DURATION: '24h', | |
| SESSION_DURATION_MS: 86400000, | |
| SESSION_DURATION_SECONDS: 86400, | |
| DEFAULT_LANGUAGE: 'en', | |
| })); | |
| vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() })); | |
| import { buildApp } from '../../src/bootstrap'; | |
| import { createTables } from '../../src/db/schema'; | |
| import { runMigrations } from '../../src/db/migrations'; | |
| import { resetTestDb, resetRateLimits } from '../helpers/test-db'; | |
| import { createUser, createAdmin, createTrip, addTripMember, createPlace, createReservation, createTag, createDayAccommodation, createBudgetItem, createPackingItem, createDayNote, createDayAssignment } from '../helpers/factories'; | |
| import { authCookie } from '../helpers/auth'; | |
| import { invalidatePermissionsCache } from '../../src/services/permissions'; | |
| let nestApp: INestApplication; | |
| let app: Application; | |
| beforeAll(async () => { | |
| createTables(testDb); | |
| runMigrations(testDb); | |
| nestApp = await buildApp(); | |
| app = nestApp.getHttpAdapter().getInstance(); | |
| }); | |
| beforeEach(() => { | |
| resetTestDb(testDb); | |
| resetRateLimits(nestApp); | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY date').all(res.body.trip.id) as any[]; | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(res.body.trip.id) as any[]; | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ?').all(res.body.trip.id) as any[]; | |
| 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 | |
| testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('perm_trip_create', 'admin')").run(); | |
| 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); | |
| testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('perm_trip_create', 'admin')").run(); | |
| 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 | |
| testDb.prepare('UPDATE trips SET is_archived = 1 WHERE id = ?').run(archivedTrip.id); | |
| 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' }); | |
| testDb.prepare('UPDATE trips SET is_archived = 1 WHERE id = ?').run(archivedTrip.id); | |
| 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 | |
| testDb.prepare('UPDATE trips SET is_archived = 1 WHERE id = ?').run(trip.id); | |
| // 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) | |
| testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_trip_archive', 'trip_owner')").run(); | |
| 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 | |
| testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_trip_edit', 'trip_owner')").run(); | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string }[]; | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string | null }[]; | |
| 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 = testDb.prepare('SELECT * FROM day_assignments WHERE id = ?').get(assignment.id) as { day_id: number } | undefined; | |
| expect(assignmentsAfter).toBeDefined(); | |
| expect(assignmentsAfter!.day_id).toBe(daysAfter[0].id); | |
| const notesAfter = testDb.prepare('SELECT * FROM day_notes WHERE id = ?').get(note.id) as { day_id: number } | undefined; | |
| expect(notesAfter).toBeDefined(); | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number }[]; | |
| 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string | null }[]; | |
| expect(daysAfter).toHaveLength(3); | |
| expect(daysAfter.every(d => d.date !== null)).toBe(true); | |
| // Overflow days and their assignments deleted | |
| const all = testDb.prepare('SELECT * FROM day_assignments WHERE id IN (?, ?)').all(a4.id, a5.id) as { id: number }[]; | |
| expect(all).toHaveLength(0); | |
| }); | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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 β 403', 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)); | |
| // getTripOwner finds the trip (it exists); checkPermission fails for non-members β 403 | |
| expect(res.status).toBe(403); | |
| // Trip still exists | |
| const tripInDb = testDb.prepare('SELECT id FROM trips WHERE id = ?').get(trip.id); | |
| expect(tripInDb).toBeDefined(); | |
| }); | |
| 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 = testDb.prepare('SELECT id FROM places WHERE trip_id = ?').all(trip.id); | |
| expect(places).toHaveLength(0); | |
| const reservations = testDb.prepare('SELECT id FROM reservations WHERE trip_id = ?').all(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 = testDb.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?').get(trip.id, invitee.id); | |
| expect(dbEntry).toBeDefined(); | |
| }); | |
| 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 = testDb.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?').get(trip.id, member.id); | |
| expect(dbEntry).toBeUndefined(); | |
| }); | |
| 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-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) | |
| testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_member_manage', 'trip_owner')").run(); | |
| 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 = testDb.prepare('SELECT * FROM trips WHERE id = ?').get(res.body.trip.id) as any; | |
| 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); | |
| }); | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as any[]; | |
| 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' }); | |
| testDb.prepare('INSERT INTO place_tags (place_id, tag_id) VALUES (?, ?)').run(place.id, tag.id); | |
| // Day assignment | |
| testDb.prepare( | |
| 'INSERT INTO day_assignments (day_id, place_id, order_index, notes) VALUES (?, ?, 0, ?)' | |
| ).run(days[0].id, place.id, '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 = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(newId) as any[]; | |
| expect(newDays).toHaveLength(3); | |
| // Place copied | |
| const newPlaces = testDb.prepare('SELECT * FROM places WHERE trip_id = ?').all(newId) as any[]; | |
| expect(newPlaces).toHaveLength(1); | |
| expect(newPlaces[0].name).toBe('Tower Bridge'); | |
| // Place tag copied | |
| const newTags = testDb.prepare( | |
| 'SELECT pt.* FROM place_tags pt JOIN places p ON p.id = pt.place_id WHERE p.trip_id = ?' | |
| ).all(newId) as any[]; | |
| expect(newTags).toHaveLength(1); | |
| // Assignment copied | |
| const newAssignments = testDb.prepare( | |
| 'SELECT da.* FROM day_assignments da JOIN days d ON d.id = da.day_id WHERE d.trip_id = ?' | |
| ).all(newId) as any[]; | |
| expect(newAssignments).toHaveLength(1); | |
| // Accommodation copied | |
| const newAccom = testDb.prepare('SELECT * FROM day_accommodations WHERE trip_id = ?').all(newId) as any[]; | |
| expect(newAccom).toHaveLength(1); | |
| // Reservation copied | |
| const newResv = testDb.prepare('SELECT * FROM reservations WHERE trip_id = ?').all(newId) as any[]; | |
| expect(newResv).toHaveLength(1); | |
| // Budget copied | |
| const newBudget = testDb.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(newId) as any[]; | |
| expect(newBudget).toHaveLength(1); | |
| // Packing copied (checked reset to 0) | |
| const newPacking = testDb.prepare('SELECT * FROM packing_items WHERE trip_id = ?').all(newId) as any[]; | |
| expect(newPacking).toHaveLength(1); | |
| expect(newPacking[0].checked).toBe(0); | |
| // Day note copied | |
| const newNotes = testDb.prepare('SELECT * FROM day_notes WHERE trip_id = ?').all(newId) as any[]; | |
| 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 | |
| testDb.prepare( | |
| 'INSERT INTO todo_items (trip_id, name, checked, category, sort_order, due_date, description, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' | |
| ).run(trip.id, 'Buy tickets', 0, 'Transport', 0, '2026-06-01', 'Check Ryanair', 1); | |
| testDb.prepare( | |
| 'INSERT INTO todo_items (trip_id, name, checked, category, sort_order, assigned_user_id, priority) VALUES (?, ?, ?, ?, ?, ?, ?)' | |
| ).run(trip.id, 'Book hotel', 1, 'Accommodation', 1, user.id, 0); | |
| // Two budget category order rows | |
| const insOrder = testDb.prepare('INSERT INTO budget_category_order (trip_id, category, sort_order) VALUES (?, ?, ?)'); | |
| insOrder.run(trip.id, 'Transport', 0); | |
| insOrder.run(trip.id, 'Accommodation', 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 = testDb.prepare('SELECT * FROM todo_items WHERE trip_id = ? ORDER BY sort_order').all(newId) as any[]; | |
| 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 = testDb.prepare('SELECT category, sort_order FROM budget_category_order WHERE trip_id = ? ORDER BY sort_order').all(newId) as any[]; | |
| 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); | |
| testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, 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); | |
| }); | |
| }); | |