mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
743b724cbc
Rework the private-packing flag into a full sharing model. Every item is now Common (the group pool — where all existing items live, so nothing breaks), Personal (private to its owner) or Shared with specific people (it shows up on those travelers' own lists, marked "by <bringer>"). is_private discriminates restricted from common; a new packing_item_recipients table holds who a shared item covers, and packing_item_contributors records "I can bring that too" pledges on Common items. The panel gains a Gemeinsam / Meine Liste view switch, each item a sharing control (owner sets the tier + the people it covers), and Common items can be co-brought or cloned onto your personal list. Visibility is enforced server-side in listItems and the WebSocket broadcasts are scoped to exactly who can see an item across every tier transition. All 22 locales stay in parity.
643 lines
29 KiB
TypeScript
643 lines
29 KiB
TypeScript
/**
|
|
* Packing List integration tests.
|
|
* Covers PACK-001 to PACK-014.
|
|
*/
|
|
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';
|
|
|
|
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, createTrip, createPackingItem, addTripMember } from '../helpers/factories';
|
|
import { authCookie } from '../helpers/auth';
|
|
|
|
let nestApp: INestApplication;
|
|
let app: Application;
|
|
|
|
beforeAll(async () => {
|
|
createTables(testDb);
|
|
runMigrations(testDb);
|
|
nestApp = await buildApp();
|
|
app = nestApp.getHttpAdapter().getInstance();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
resetRateLimits(nestApp);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await nestApp.close();
|
|
testDb.close();
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Create packing item
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Create packing item', () => {
|
|
it('PACK-001 — POST creates a packing item', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Passport', category: 'Documents' });
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.item.name).toBe('Passport');
|
|
expect(res.body.item.category).toBe('Documents');
|
|
expect(res.body.item.checked).toBe(0);
|
|
});
|
|
|
|
it('PACK-001 — POST without name returns 400', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ category: 'Clothing' });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('PACK-014 — non-member cannot create packing item', 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}/packing`)
|
|
.set('Cookie', authCookie(other.id))
|
|
.send({ name: 'Sunscreen' });
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// List packing items
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('List packing items', () => {
|
|
it('PACK-002 — GET /api/trips/:tripId/packing returns all items', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
createPackingItem(testDb, trip.id, { name: 'Toothbrush', category: 'Toiletries' });
|
|
createPackingItem(testDb, trip.id, { name: 'Shirt', category: 'Clothing' });
|
|
|
|
const res = await request(app)
|
|
.get(`/api/trips/${trip.id}/packing`)
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.items).toHaveLength(2);
|
|
});
|
|
|
|
it('PACK-002 — member can list packing items', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
createPackingItem(testDb, trip.id, { name: 'Jacket' });
|
|
|
|
const res = await request(app)
|
|
.get(`/api/trips/${trip.id}/packing`)
|
|
.set('Cookie', authCookie(member.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.items).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Private items (#858)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Private packing items (#858)', () => {
|
|
it('PACK-PRIV-001 — a private item is hidden from other members but visible to its owner', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
|
|
// Owner creates one shared and one private item.
|
|
await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id)).send({ name: 'Shared tent' });
|
|
const priv = await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id)).send({ name: 'Surprise gift', is_private: true });
|
|
expect(priv.body.item.is_private).toBe(1);
|
|
expect(priv.body.item.owner_id).toBe(owner.id);
|
|
|
|
const ownerView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id));
|
|
const memberView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(member.id));
|
|
|
|
expect(ownerView.body.items.map((i: any) => i.name).sort()).toEqual(['Shared tent', 'Surprise gift']);
|
|
expect(memberView.body.items.map((i: any) => i.name)).toEqual(['Shared tent']);
|
|
});
|
|
|
|
it('PACK-PRIV-002 — toggling an item private hides it from other members', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
|
|
const created = await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id)).send({ name: 'Diary' });
|
|
const id = created.body.item.id;
|
|
|
|
await request(app).put(`/api/trips/${trip.id}/packing/${id}`).set('Cookie', authCookie(owner.id)).send({ is_private: true });
|
|
|
|
const memberView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(member.id));
|
|
expect(memberView.body.items).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Three-tier sharing (#858)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Three-tier packing sharing (#858)', () => {
|
|
it('PACK-3T-001 — existing items stay Common (visible to all) — non-breaking', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
// A pre-existing row (default is_private=0) must remain visible to everyone.
|
|
createPackingItem(testDb, trip.id, { name: 'Group tent' });
|
|
|
|
const memberView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(member.id));
|
|
expect(memberView.body.items.map((i: any) => i.name)).toContain('Group tent');
|
|
});
|
|
|
|
it('PACK-3T-002 — a Shared item reaches the recipient (with the bringer) but no one else', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: friend } = createUser(testDb);
|
|
const { user: stranger } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, friend.id);
|
|
addTripMember(testDb, trip.id, stranger.id);
|
|
|
|
const created = await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id))
|
|
.send({ name: 'Power bank', visibility: 'shared', recipient_ids: [friend.id] });
|
|
expect(created.body.item.recipients.map((r: any) => r.user_id)).toEqual([friend.id]);
|
|
expect(created.body.item.owner_username).toBeTruthy();
|
|
|
|
const friendView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(friend.id));
|
|
const strangerView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(stranger.id));
|
|
expect(friendView.body.items.map((i: any) => i.name)).toContain('Power bank');
|
|
expect(strangerView.body.items.map((i: any) => i.name)).not.toContain('Power bank');
|
|
});
|
|
|
|
it('PACK-3T-003 — clone copies a Common item onto the caller\'s personal list', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
const created = await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id)).send({ name: 'Adapter', visibility: 'common' });
|
|
|
|
const clone = await request(app).post(`/api/trips/${trip.id}/packing/${created.body.item.id}/clone`).set('Cookie', authCookie(member.id));
|
|
expect(clone.status).toBe(201);
|
|
expect(clone.body.item.is_private).toBe(1);
|
|
expect(clone.body.item.owner_id).toBe(member.id);
|
|
// The owner does not see the member's private clone.
|
|
const ownerView = await request(app).get(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id));
|
|
expect(ownerView.body.items.filter((i: any) => i.name === 'Adapter')).toHaveLength(1);
|
|
});
|
|
|
|
it('PACK-3T-004 — "I can bring that too" adds the caller as a contributor on a Common item', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: helper } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, helper.id);
|
|
const created = await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id)).send({ name: 'Sunscreen', visibility: 'common' });
|
|
|
|
const res = await request(app).post(`/api/trips/${trip.id}/packing/${created.body.item.id}/contributors`).set('Cookie', authCookie(helper.id));
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.item.contributors.map((c: any) => c.user_id)).toContain(helper.id);
|
|
});
|
|
|
|
it('PACK-3T-005 — sharing can only be changed by the owner', async () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
const created = await request(app).post(`/api/trips/${trip.id}/packing`).set('Cookie', authCookie(owner.id)).send({ name: 'Tent', visibility: 'personal' });
|
|
|
|
const denied = await request(app).put(`/api/trips/${trip.id}/packing/${created.body.item.id}/sharing`).set('Cookie', authCookie(member.id)).send({ visibility: 'common' });
|
|
expect(denied.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Update packing item
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Update packing item', () => {
|
|
it('PACK-003 — PUT updates packing item (toggle checked)', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const item = createPackingItem(testDb, trip.id, { name: 'Camera' });
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/${item.id}`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ checked: true });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.item.checked).toBe(1);
|
|
});
|
|
|
|
it('PACK-003 — PUT returns 404 for non-existent item', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/99999`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Updated' });
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Delete packing item
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Delete packing item', () => {
|
|
it('PACK-004 — DELETE removes packing item', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const item = createPackingItem(testDb, trip.id, { name: 'Sunglasses' });
|
|
|
|
const del = await request(app)
|
|
.delete(`/api/trips/${trip.id}/packing/${item.id}`)
|
|
.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}/packing`)
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(list.body.items).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Bulk import
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Bulk import packing items', () => {
|
|
it('PACK-005 — POST /import creates multiple items at once', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/import`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({
|
|
items: [
|
|
{ name: 'Toothbrush', category: 'Toiletries' },
|
|
{ name: 'Shampoo', category: 'Toiletries' },
|
|
{ name: 'Socks', category: 'Clothing' },
|
|
],
|
|
});
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.items).toHaveLength(3);
|
|
expect(res.body.count).toBe(3);
|
|
});
|
|
|
|
it('PACK-005 — POST /import with empty array returns 400', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/import`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ items: [] });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Reorder
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Reorder packing items', () => {
|
|
it('PACK-006 — PUT /reorder reorders items', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const i1 = createPackingItem(testDb, trip.id, { name: 'Item A' });
|
|
const i2 = createPackingItem(testDb, trip.id, { name: 'Item B' });
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/reorder`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ orderedIds: [i2.id, i1.id] });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.success).toBe(true);
|
|
|
|
const rows = testDb
|
|
.prepare('SELECT id, sort_order FROM packing_items WHERE trip_id = ? ORDER BY sort_order')
|
|
.all(trip.id) as Array<{ id: number; sort_order: number }>;
|
|
expect(rows[0].id).toBe(i2.id);
|
|
expect(rows[1].id).toBe(i1.id);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Bags
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Bags', () => {
|
|
it('PACK-008 — POST /bags creates a bag', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Carry-on', color: '#3b82f6' });
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.bag.name).toBe('Carry-on');
|
|
});
|
|
|
|
it('PACK-008 — POST /bags without name returns 400', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ color: '#ff0000' });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('PACK-011 — GET /bags returns bags list', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
// Create a bag
|
|
await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Main Bag' });
|
|
|
|
const res = await request(app)
|
|
.get(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.bags).toHaveLength(1);
|
|
});
|
|
|
|
it('PACK-009 — PUT /bags/:bagId updates bag', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const createRes = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Old Name' });
|
|
const bagId = createRes.body.bag.id;
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/bags/${bagId}`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'New Name' });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.bag.name).toBe('New Name');
|
|
});
|
|
|
|
it('PACK-010 — DELETE /bags/:bagId removes bag', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
const createRes = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Temp Bag' });
|
|
const bagId = createRes.body.bag.id;
|
|
|
|
const del = await request(app)
|
|
.delete(`/api/trips/${trip.id}/packing/bags/${bagId}`)
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(del.status).toBe(200);
|
|
expect(del.body.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Category assignees
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('Category assignees', () => {
|
|
it('PACK-012 — PUT /category-assignees/:category sets assignees', async () => {
|
|
const { user } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/category-assignees/Clothing`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ user_ids: [user.id, member.id] });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.assignees).toBeDefined();
|
|
});
|
|
|
|
it('PACK-013 — GET /category-assignees returns all category assignments', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
// Set an assignee first
|
|
await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/category-assignees/Electronics`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ user_ids: [user.id] });
|
|
|
|
const res = await request(app)
|
|
.get(`/api/trips/${trip.id}/packing/category-assignees`)
|
|
.set('Cookie', authCookie(user.id));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.assignees).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Packing — apply-template, bag members, save-as-template', () => {
|
|
it('PACK-015 — POST /apply-template/:templateId applies template items to trip', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const tpl = testDb.prepare("INSERT INTO packing_templates (name, created_by) VALUES ('Beach', ?)").run(user.id);
|
|
const cat = testDb.prepare("INSERT INTO packing_template_categories (template_id, name, sort_order) VALUES (?, 'Essentials', 0)").run(tpl.lastInsertRowid);
|
|
testDb.prepare("INSERT INTO packing_template_items (category_id, name, sort_order) VALUES (?, 'Sunscreen', 0)").run(cat.lastInsertRowid);
|
|
const templateId = tpl.lastInsertRowid;
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/apply-template/${templateId}`)
|
|
.set('Cookie', authCookie(user.id));
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.items)).toBe(true);
|
|
expect(res.body.items.length).toBeGreaterThan(0);
|
|
expect(res.body.count).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('PACK-015b — POST /apply-template/:id for empty template returns 404', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
// Template with no items
|
|
const tpl = testDb.prepare("INSERT INTO packing_templates (name, created_by) VALUES ('Empty', ?)").run(user.id);
|
|
const emptyTemplateId = tpl.lastInsertRowid;
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/apply-template/${emptyTemplateId}`)
|
|
.set('Cookie', authCookie(user.id));
|
|
|
|
expect(res.status).toBe(404);
|
|
expect(res.body.error).toBeDefined();
|
|
});
|
|
|
|
it('PACK-016 — PUT /bags/:bagId/members sets bag members', async () => {
|
|
const { user } = createUser(testDb);
|
|
const { user: member } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
addTripMember(testDb, trip.id, member.id);
|
|
|
|
// Create a bag first
|
|
const bagRes = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/bags`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Carry-on' });
|
|
expect(bagRes.status).toBe(201);
|
|
const bagId = bagRes.body.bag.id;
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/bags/${bagId}/members`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ user_ids: [user.id, member.id] });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.members)).toBe(true);
|
|
expect(res.body.members.length).toBe(2);
|
|
});
|
|
|
|
it('PACK-016b — PUT /bags/:bagId/members for non-existent bag returns 404', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.put(`/api/trips/${trip.id}/packing/bags/999999/members`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ user_ids: [user.id] });
|
|
|
|
expect(res.status).toBe(404);
|
|
expect(res.body.error).toBeDefined();
|
|
});
|
|
|
|
it('PACK-017 — POST /save-as-template saves packing list as a template (admin)', async () => {
|
|
const { user } = createUser(testDb, { role: 'admin' });
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
// Add an item so the trip has something to save
|
|
createPackingItem(testDb, trip.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/save-as-template`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'My Summer Template' });
|
|
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.template).toBeDefined();
|
|
expect(res.body.template.name).toBe('My Summer Template');
|
|
});
|
|
|
|
it('PACK-017b — POST /save-as-template without name returns 400 (admin)', async () => {
|
|
const { user } = createUser(testDb, { role: 'admin' });
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/save-as-template`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({});
|
|
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toBeDefined();
|
|
});
|
|
|
|
it('PACK-017c — POST /save-as-template when trip has no items returns 400 (admin)', async () => {
|
|
const { user } = createUser(testDb, { role: 'admin' });
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/save-as-template`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'Empty Trip Template' });
|
|
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toBeDefined();
|
|
});
|
|
|
|
it('PACK-017d — POST /save-as-template is forbidden for non-admins (403)', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
createPackingItem(testDb, trip.id);
|
|
|
|
const res = await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/save-as-template`)
|
|
.set('Cookie', authCookie(user.id))
|
|
.send({ name: 'My Summer Template' });
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.error).toBe('Admin access required');
|
|
});
|
|
|
|
it('PACK-017e — GET /packing/templates lists templates for a trip member', async () => {
|
|
const { user: admin } = createUser(testDb, { role: 'admin' });
|
|
const trip = createTrip(testDb, admin.id);
|
|
createPackingItem(testDb, trip.id);
|
|
await request(app)
|
|
.post(`/api/trips/${trip.id}/packing/save-as-template`)
|
|
.set('Cookie', authCookie(admin.id))
|
|
.send({ name: 'Shared Template' });
|
|
|
|
const res = await request(app)
|
|
.get(`/api/trips/${trip.id}/packing/templates`)
|
|
.set('Cookie', authCookie(admin.id));
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.templates)).toBe(true);
|
|
expect(res.body.templates.some((t: { name: string }) => t.name === 'Shared Template')).toBe(true);
|
|
expect(res.body.templates[0]).toHaveProperty('item_count');
|
|
});
|
|
});
|