2026-04-03 19:17:53 +08:00
/ * *
* Collab ( notes , polls , messages , reactions ) integration tests .
* Covers COLLAB - 001 to COLLAB - 027 .
*
* Note : File upload to collab notes ( COLLAB - 005 / 006 / 007 ) requires physical file I / O .
* Link preview ( COLLAB - 025 / 026 ) would need fetch mocking — skipped here .
* /
import { describe , it , expect , vi , beforeAll , beforeEach , afterAll } from 'vitest' ;
import request from 'supertest' ;
import type { Application } from 'express' ;
import path from 'path' ;
import fs from 'fs' ;
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 : ( ) = > { } ,
} ) ) ;
import { createApp } from '../../src/app' ;
import { createTables } from '../../src/db/schema' ;
import { runMigrations } from '../../src/db/migrations' ;
import { resetTestDb } from '../helpers/test-db' ;
import { createUser , createTrip , addTripMember } from '../helpers/factories' ;
2026-04-03 20:10:38 +08:00
import { authCookie , generateToken } from '../helpers/auth' ;
2026-04-03 19:17:53 +08:00
import { loginAttempts , mfaAttempts } from '../../src/routes/auth' ;
const app : Application = createApp ( ) ;
const FIXTURE_PDF = path . join ( __dirname , '../fixtures/test.pdf' ) ;
// Ensure uploads/files dir exists for collab file uploads
const uploadsDir = path . join ( __dirname , '../../uploads/files' ) ;
beforeAll ( ( ) = > {
createTables ( testDb ) ;
runMigrations ( testDb ) ;
if ( ! fs . existsSync ( uploadsDir ) ) fs . mkdirSync ( uploadsDir , { recursive : true } ) ;
} ) ;
beforeEach ( ( ) = > {
resetTestDb ( testDb ) ;
loginAttempts . clear ( ) ;
mfaAttempts . clear ( ) ;
} ) ;
afterAll ( ( ) = > {
testDb . close ( ) ;
} ) ;
// ─────────────────────────────────────────────────────────────────────────────
// Collab Notes
// ─────────────────────────────────────────────────────────────────────────────
describe ( 'Collab notes' , ( ) = > {
it ( 'COLLAB-001 — POST /collab/notes creates a note' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Packing Ideas' , content : 'Bring sunscreen' , category : 'Planning' } ) ;
expect ( res . status ) . toBe ( 201 ) ;
expect ( res . body . note . title ) . toBe ( 'Packing Ideas' ) ;
expect ( res . body . note . content ) . toBe ( 'Bring sunscreen' ) ;
} ) ;
it ( 'COLLAB-001 — POST without title returns 400' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { content : 'No title' } ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( 'COLLAB-001 — non-member cannot create collab note' , async ( ) = > {
const { user : owner } = createUser ( testDb ) ;
const { user : other } = createUser ( testDb ) ;
const trip = createTrip ( testDb , owner . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( other . id ) )
. send ( { title : 'Sneaky note' } ) ;
expect ( res . status ) . toBe ( 404 ) ;
} ) ;
it ( 'COLLAB-002 — GET /collab/notes returns all notes' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Note A' } ) ;
await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Note B' } ) ;
const res = await request ( app )
. get ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( res . status ) . toBe ( 200 ) ;
expect ( res . body . notes ) . toHaveLength ( 2 ) ;
} ) ;
it ( 'COLLAB-003 — PUT /collab/notes/:id updates a note' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Old Title' , content : 'Old content' } ) ;
const noteId = create . body . note . id ;
const res = await request ( app )
. put ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'New Title' , content : 'New content' , pinned : true } ) ;
expect ( res . status ) . toBe ( 200 ) ;
expect ( res . body . note . title ) . toBe ( 'New Title' ) ;
expect ( res . body . note . pinned ) . toBe ( 1 ) ;
} ) ;
it ( 'COLLAB-003 — PUT non-existent note returns 404' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. put ( ` /api/trips/ ${ trip . id } /collab/notes/99999 ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Updated' } ) ;
expect ( res . status ) . toBe ( 404 ) ;
} ) ;
it ( 'COLLAB-004 — DELETE /collab/notes/:id removes note' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'To Delete' } ) ;
const noteId = create . body . note . id ;
const del = await request ( app )
. delete ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( del . status ) . toBe ( 200 ) ;
expect ( del . body . success ) . toBe ( true ) ;
const list = await request ( app )
. get ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( list . body . notes ) . toHaveLength ( 0 ) ;
} ) ;
it ( 'COLLAB-005 — POST /collab/notes/:id/files uploads a file to a note' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Note with file' } ) ;
const noteId = create . body . note . id ;
const upload = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , FIXTURE_PDF ) ;
expect ( upload . status ) . toBe ( 201 ) ;
expect ( upload . body . file ) . toBeDefined ( ) ;
} ) ;
it ( 'COLLAB-006 — uploading blocked extension to note is rejected' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Note' } ) ;
const noteId = create . body . note . id ;
// Create a temp .svg file
const svgPath = path . join ( uploadsDir , 'collab_blocked.svg' ) ;
fs . writeFileSync ( svgPath , '<svg></svg>' ) ;
try {
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , svgPath ) ;
expect ( res . status ) . toBe ( 400 ) ;
} finally {
if ( fs . existsSync ( svgPath ) ) fs . unlinkSync ( svgPath ) ;
}
} ) ;
it ( 'COLLAB-007 — DELETE /collab/notes/:noteId/files/:fileId removes file from note' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Note with file' } ) ;
const noteId = create . body . note . id ;
const upload = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , FIXTURE_PDF ) ;
const fileId = upload . body . file . id ;
const del = await request ( app )
. delete ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files/ ${ fileId } ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( del . status ) . toBe ( 200 ) ;
expect ( del . body . success ) . toBe ( true ) ;
} ) ;
2026-04-03 20:10:38 +08:00
it ( 'COLLAB-028 — uploaded note file URL uses authenticated download path, not /uploads/' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'URL check' } ) ;
const noteId = create . body . note . id ;
const upload = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , FIXTURE_PDF ) ;
expect ( upload . status ) . toBe ( 201 ) ;
const fileUrl = upload . body . file . url ;
expect ( fileUrl ) . toMatch ( /^\/api\/trips\/\d+\/files\/\d+\/download$/ ) ;
expect ( fileUrl ) . not . toContain ( '/uploads/' ) ;
} ) ;
it ( 'COLLAB-029 — note attachments in listing use authenticated download URLs' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'List URL check' } ) ;
const noteId = create . body . note . id ;
await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , FIXTURE_PDF ) ;
const list = await request ( app )
. get ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( list . status ) . toBe ( 200 ) ;
const note = list . body . notes . find ( ( n : any ) = > n . id === noteId ) ;
expect ( note . attachments . length ) . toBe ( 1 ) ;
expect ( note . attachments [ 0 ] . url ) . toMatch ( /^\/api\/trips\/\d+\/files\/\d+\/download$/ ) ;
expect ( note . attachments [ 0 ] . url ) . not . toContain ( '/uploads/' ) ;
} ) ;
it ( 'COLLAB-030 — note file is downloadable via files endpoint with ephemeral token' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Downloadable note' } ) ;
const noteId = create . body . note . id ;
const upload = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , FIXTURE_PDF ) ;
const fileUrl = upload . body . file . url ;
// Obtain an ephemeral resource token (same flow as getAuthUrl on the client)
const tokenRes = await request ( app )
. post ( '/api/auth/resource-token' )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { purpose : 'download' } ) ;
expect ( tokenRes . status ) . toBe ( 200 ) ;
const { token } = tokenRes . body ;
// Download with ?token= should succeed
const dl = await request ( app ) . get ( ` ${ fileUrl } ?token= ${ token } ` ) ;
expect ( dl . status ) . toBe ( 200 ) ;
} ) ;
it ( 'COLLAB-031 — note file download without auth returns 401' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { title : 'Auth required note' } ) ;
const noteId = create . body . note . id ;
const upload = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/notes/ ${ noteId } /files ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. attach ( 'file' , FIXTURE_PDF ) ;
const fileUrl = upload . body . file . url ;
// Download without any auth should fail
const dl = await request ( app ) . get ( fileUrl ) ;
expect ( dl . status ) . toBe ( 401 ) ;
} ) ;
2026-04-03 19:17:53 +08:00
} ) ;
// ─────────────────────────────────────────────────────────────────────────────
// Polls
// ─────────────────────────────────────────────────────────────────────────────
describe ( 'Polls' , ( ) = > {
it ( 'COLLAB-008 — POST /collab/polls creates a poll' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { question : 'Where to eat?' , options : [ 'Pizza' , 'Sushi' , 'Tacos' ] } ) ;
expect ( res . status ) . toBe ( 201 ) ;
expect ( res . body . poll . question ) . toBe ( 'Where to eat?' ) ;
expect ( res . body . poll . options ) . toHaveLength ( 3 ) ;
} ) ;
it ( 'COLLAB-008 — POST without question returns 400' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { options : [ 'A' , 'B' ] } ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( 'COLLAB-009 — GET /collab/polls returns polls' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { question : 'Beach or mountains?' , options : [ 'Beach' , 'Mountains' ] } ) ;
const res = await request ( app )
. get ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( res . status ) . toBe ( 200 ) ;
expect ( res . body . polls ) . toHaveLength ( 1 ) ;
} ) ;
it ( 'COLLAB-010 — POST /collab/polls/:id/vote casts a vote' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { question : 'Restaurant?' , options : [ 'Italian' , 'French' ] } ) ;
const pollId = create . body . poll . id ;
const vote = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls/ ${ pollId } /vote ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { option_index : 0 } ) ;
expect ( vote . status ) . toBe ( 200 ) ;
expect ( vote . body . poll ) . toBeDefined ( ) ;
} ) ;
it ( 'COLLAB-011 — PUT /collab/polls/:id/close closes a poll' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { question : 'Hotel?' , options : [ 'Budget' , 'Luxury' ] } ) ;
const pollId = create . body . poll . id ;
const close = await request ( app )
. put ( ` /api/trips/ ${ trip . id } /collab/polls/ ${ pollId } /close ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( close . status ) . toBe ( 200 ) ;
expect ( close . body . poll . is_closed ) . toBe ( true ) ;
} ) ;
it ( 'COLLAB-012 — cannot vote on closed poll' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { question : 'Closed?' , options : [ 'Yes' , 'No' ] } ) ;
const pollId = create . body . poll . id ;
await request ( app )
. put ( ` /api/trips/ ${ trip . id } /collab/polls/ ${ pollId } /close ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
const vote = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls/ ${ pollId } /vote ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { option_index : 0 } ) ;
expect ( vote . status ) . toBe ( 400 ) ;
} ) ;
it ( 'COLLAB-013 — DELETE /collab/polls/:id removes poll' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const create = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/polls ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { question : 'Delete me?' , options : [ 'Yes' , 'No' ] } ) ;
const pollId = create . body . poll . id ;
const del = await request ( app )
. delete ( ` /api/trips/ ${ trip . id } /collab/polls/ ${ pollId } ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( del . status ) . toBe ( 200 ) ;
expect ( del . body . success ) . toBe ( true ) ;
} ) ;
} ) ;
// ─────────────────────────────────────────────────────────────────────────────
// Messages
// ─────────────────────────────────────────────────────────────────────────────
describe ( 'Messages' , ( ) = > {
it ( 'COLLAB-014 — POST /collab/messages sends a message' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'Hello, team!' } ) ;
expect ( res . status ) . toBe ( 201 ) ;
expect ( res . body . message . text ) . toBe ( 'Hello, team!' ) ;
} ) ;
it ( 'COLLAB-014 — POST without text returns 400' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : '' } ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( 'COLLAB-014 — non-member cannot send message' , async ( ) = > {
const { user : owner } = createUser ( testDb ) ;
const { user : other } = createUser ( testDb ) ;
const trip = createTrip ( testDb , owner . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( other . id ) )
. send ( { text : 'Unauthorized' } ) ;
expect ( res . status ) . toBe ( 404 ) ;
} ) ;
it ( 'COLLAB-015 — GET /collab/messages returns messages in order' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'First message' } ) ;
await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'Second message' } ) ;
const res = await request ( app )
. get ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( res . status ) . toBe ( 200 ) ;
expect ( res . body . messages . length ) . toBeGreaterThanOrEqual ( 2 ) ;
} ) ;
it ( 'COLLAB-016 — POST /collab/messages with reply_to links reply' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const parent = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'Original' } ) ;
const parentId = parent . body . message . id ;
const reply = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'Reply here' , reply_to : parentId } ) ;
expect ( reply . status ) . toBe ( 201 ) ;
expect ( reply . body . message . reply_to ) . toBe ( parentId ) ;
} ) ;
it ( 'COLLAB-017 — DELETE /collab/messages/:id removes own message' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const msg = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'Delete me' } ) ;
const msgId = msg . body . message . id ;
const del = await request ( app )
. delete ( ` /api/trips/ ${ trip . id } /collab/messages/ ${ msgId } ` )
. set ( 'Cookie' , authCookie ( user . id ) ) ;
expect ( del . status ) . toBe ( 200 ) ;
expect ( del . body . success ) . toBe ( true ) ;
} ) ;
it ( 'COLLAB-017 — cannot delete another user\'s message' , async ( ) = > {
const { user : owner } = createUser ( testDb ) ;
const { user : member } = createUser ( testDb ) ;
const trip = createTrip ( testDb , owner . id ) ;
addTripMember ( testDb , trip . id , member . id ) ;
const msg = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( owner . id ) )
. send ( { text : 'Owner message' } ) ;
const msgId = msg . body . message . id ;
const del = await request ( app )
. delete ( ` /api/trips/ ${ trip . id } /collab/messages/ ${ msgId } ` )
. set ( 'Cookie' , authCookie ( member . id ) ) ;
expect ( del . status ) . toBe ( 403 ) ;
} ) ;
} ) ;
// ─────────────────────────────────────────────────────────────────────────────
// Reactions
// ─────────────────────────────────────────────────────────────────────────────
describe ( 'Message reactions' , ( ) = > {
it ( 'COLLAB-018 — POST /collab/messages/:id/react adds a reaction' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const msg = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'React to me' } ) ;
const msgId = msg . body . message . id ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages/ ${ msgId } /react ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { emoji : '👍' } ) ;
expect ( res . status ) . toBe ( 200 ) ;
expect ( res . body . reactions ) . toBeDefined ( ) ;
} ) ;
it ( 'COLLAB-018 — POST react without emoji returns 400' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const msg = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'Test' } ) ;
const msgId = msg . body . message . id ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages/ ${ msgId } /react ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { } ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
} ) ;
// ─────────────────────────────────────────────────────────────────────────────
// Long text validation
// ─────────────────────────────────────────────────────────────────────────────
describe ( 'Collab validation' , ( ) = > {
it ( 'COLLAB-018 — message text exceeding 5000 chars is rejected' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const trip = createTrip ( testDb , user . id ) ;
const res = await request ( app )
. post ( ` /api/trips/ ${ trip . id } /collab/messages ` )
. set ( 'Cookie' , authCookie ( user . id ) )
. send ( { text : 'A' . repeat ( 5001 ) } ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
} ) ;