mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
151 lines
8.3 KiB
TypeScript
151 lines
8.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { dbMock } = vi.hoisted(() => {
|
|
const stmt = { get: vi.fn(), all: vi.fn(() => []), run: vi.fn() };
|
|
return { dbMock: { prepare: vi.fn(() => stmt), _stmt: stmt } };
|
|
});
|
|
vi.mock('../../../src/db/database', () => ({ db: dbMock, closeDb: () => {}, reinitialize: () => {} }));
|
|
import { db as dbConn } from '../../../src/db/database';
|
|
import { DatabaseService } from '../../../src/nest/database/database.service';
|
|
|
|
const { broadcast } = vi.hoisted(() => ({ broadcast: vi.fn() }));
|
|
vi.mock('../../../src/websocket', () => ({ broadcast }));
|
|
|
|
const { checkPermission } = vi.hoisted(() => ({ checkPermission: vi.fn(() => true) }));
|
|
vi.mock('../../../src/services/permissions', () => ({ checkPermission }));
|
|
|
|
const { pk } = vi.hoisted(() => ({
|
|
pk: {
|
|
verifyTripAccess: vi.fn(), listItems: vi.fn(), createItem: vi.fn(), updateItem: vi.fn(), deleteItem: vi.fn(),
|
|
bulkImport: vi.fn(), listBags: vi.fn(), createBag: vi.fn(), updateBag: vi.fn(), deleteBag: vi.fn(),
|
|
listTemplates: vi.fn(), applyTemplate: vi.fn(), saveAsTemplate: vi.fn(), setBagMembers: vi.fn(), getCategoryAssignees: vi.fn(),
|
|
updateCategoryAssignees: vi.fn(), reorderItems: vi.fn(),
|
|
setItemSharing: vi.fn(), addContributor: vi.fn(), removeContributor: vi.fn(), cloneItem: vi.fn(),
|
|
},
|
|
}));
|
|
vi.mock('../../../src/services/packingService', () => pk);
|
|
|
|
const { send } = vi.hoisted(() => ({ send: vi.fn(() => Promise.resolve()) }));
|
|
vi.mock('../../../src/services/notificationService', () => ({ send }));
|
|
|
|
import { PackingService } from '../../../src/nest/packing/packing.service';
|
|
|
|
function svc() {
|
|
return new PackingService(new DatabaseService(dbConn));
|
|
}
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
describe('PackingService (wrapper delegation + helpers)', () => {
|
|
it('canEdit delegates to checkPermission with packing_edit', () => {
|
|
svc().canEdit({ user_id: 2 } as never, { id: 1, role: 'user' } as never);
|
|
expect(checkPermission).toHaveBeenCalledWith('packing_edit', 'user', 2, 1, true);
|
|
});
|
|
|
|
it('broadcast forwards to the websocket helper', () => {
|
|
svc().broadcast('5', 'packing:created', { item: 1 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'packing:created', { item: 1 }, 'sock');
|
|
});
|
|
|
|
describe('broadcastItem (private-item scoping, #858)', () => {
|
|
it('broadcasts a shared item to the whole room (no onlyUserId)', () => {
|
|
svc().broadcastItem('5', 'packing:created', { item: 1 }, { is_private: 0, owner_id: 7 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'packing:created', { item: 1 }, 'sock', undefined);
|
|
});
|
|
|
|
it('scopes a private item to its owner', () => {
|
|
svc().broadcastItem('5', 'packing:created', { item: 1 }, { is_private: 1, owner_id: 7 }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'packing:created', { item: 1 }, 'sock', 7);
|
|
});
|
|
|
|
it('falls back to a room broadcast when the private item has no owner', () => {
|
|
svc().broadcastItem('5', 'packing:created', { item: 1 }, { is_private: 1, owner_id: null }, 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'packing:created', { item: 1 }, 'sock', undefined);
|
|
});
|
|
});
|
|
|
|
it('getItemPrivacy reads the privacy fields for an item', () => {
|
|
dbMock._stmt.get.mockReturnValueOnce({ is_private: 1, owner_id: 3 });
|
|
expect(svc().getItemPrivacy('5', '9')).toEqual({ is_private: 1, owner_id: 3 });
|
|
expect(dbMock.prepare).toHaveBeenCalledWith(expect.stringContaining('is_private, owner_id'));
|
|
});
|
|
|
|
it('forwards every item/bag/template/assignee call to the legacy service', () => {
|
|
const s = svc();
|
|
s.verifyTripAccess('5', 1); expect(pk.verifyTripAccess).toHaveBeenCalledWith('5', 1);
|
|
s.listItems('5'); expect(pk.listItems).toHaveBeenCalledWith('5', undefined);
|
|
s.createItem('5', { name: 'a' }); expect(pk.createItem).toHaveBeenCalledWith('5', { name: 'a' }, undefined);
|
|
s.updateItem('5', '2', { name: 'b' } as never, ['name']); expect(pk.updateItem).toHaveBeenCalledWith('5', '2', { name: 'b' }, ['name'], undefined, undefined);
|
|
s.deleteItem('5', '2'); expect(pk.deleteItem).toHaveBeenCalledWith('5', '2');
|
|
s.bulkImport('5', [{ name: 'x' }] as never); expect(pk.bulkImport).toHaveBeenCalledWith('5', [{ name: 'x' }], undefined);
|
|
s.reorderItems('5', [3, 1] as never); expect(pk.reorderItems).toHaveBeenCalledWith('5', [3, 1]);
|
|
s.listBags('5'); expect(pk.listBags).toHaveBeenCalledWith('5');
|
|
s.createBag('5', { name: 'Bag' }); expect(pk.createBag).toHaveBeenCalledWith('5', { name: 'Bag' });
|
|
s.updateBag('5', '2', { name: 'B' } as never, ['name']); expect(pk.updateBag).toHaveBeenCalledWith('5', '2', { name: 'B' }, ['name']);
|
|
s.deleteBag('5', '2'); expect(pk.deleteBag).toHaveBeenCalledWith('5', '2');
|
|
s.setBagMembers('5', '2', [1, 2]); expect(pk.setBagMembers).toHaveBeenCalledWith('5', '2', [1, 2]);
|
|
s.listTemplates(); expect(pk.listTemplates).toHaveBeenCalled();
|
|
s.applyTemplate('5', 't1', 'personal', 1); expect(pk.applyTemplate).toHaveBeenCalledWith('5', 't1', 'personal', 1);
|
|
s.saveAsTemplate('5', 1, 'Tpl'); expect(pk.saveAsTemplate).toHaveBeenCalledWith('5', 1, 'Tpl');
|
|
s.getCategoryAssignees('5'); expect(pk.getCategoryAssignees).toHaveBeenCalledWith('5');
|
|
s.updateCategoryAssignees('5', 'Clothes', [2]); expect(pk.updateCategoryAssignees).toHaveBeenCalledWith('5', 'Clothes', [2]);
|
|
s.setItemSharing('5', '2', 1, 'shared', [3]); expect(pk.setItemSharing).toHaveBeenCalledWith('5', '2', 1, 'shared', [3]);
|
|
s.addContributor('5', '2', 3); expect(pk.addContributor).toHaveBeenCalledWith('5', '2', 3);
|
|
s.removeContributor('5', '2', 3); expect(pk.removeContributor).toHaveBeenCalledWith('5', '2', 3);
|
|
s.cloneItem('5', '2', 7); expect(pk.cloneItem).toHaveBeenCalledWith('5', '2', 7);
|
|
});
|
|
|
|
describe('viewersOf + broadcastToViewers (#858 three-tier)', () => {
|
|
it('viewersOf: Common → null (whole room); restricted → owner + recipients', () => {
|
|
expect(svc().viewersOf({ is_private: 0, owner_id: 1 })).toBeNull();
|
|
expect(svc().viewersOf(null)).toBeNull();
|
|
expect(svc().viewersOf({ is_private: 1, owner_id: 1, recipients: [{ user_id: 2 }, { user_id: 3 }] })).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('broadcastToViewers delivers to each viewer (deduped) via onlyUserId', () => {
|
|
svc().broadcastToViewers('5', 'packing:created', { item: 1 }, [1, 2, 2], 'sock');
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'packing:created', { item: 1 }, 'sock', 1);
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'packing:created', { item: 1 }, 'sock', 2);
|
|
expect(broadcast).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('notifyTagged', () => {
|
|
it('does nothing when no users are tagged', () => {
|
|
svc().notifyTagged('5', { id: 1, email: 'a@b.c' } as never, 'Clothes', []);
|
|
svc().notifyTagged('5', { id: 1, email: 'a@b.c' } as never, 'Clothes', 'nope');
|
|
expect(dbMock.prepare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fires the notification when users are tagged (fire-and-forget, no throw)', () => {
|
|
expect(() => svc().notifyTagged('5', { id: 1, email: 'a@b.c' } as never, 'Clothes', [2, 3])).not.toThrow();
|
|
});
|
|
|
|
it('queries the trip title and dispatches the notification with the resolved title', async () => {
|
|
dbMock._stmt.get.mockReturnValue({ title: 'Iceland 2026' });
|
|
svc().notifyTagged('5', { id: 1, email: 'a@b.c' } as never, 'Clothes', [2, 3]);
|
|
// Flush the dynamic import().then microtask chain.
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
expect(dbMock.prepare).toHaveBeenCalledWith('SELECT title FROM trips WHERE id = ?');
|
|
expect(send).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
event: 'packing_tagged',
|
|
actorId: 1,
|
|
scope: 'trip',
|
|
targetId: 5,
|
|
params: expect.objectContaining({ trip: 'Iceland 2026', actor: 'a@b.c', category: 'Clothes', tripId: '5' }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('falls back to "Untitled" when the trip row is missing (?? / default branch)', async () => {
|
|
dbMock._stmt.get.mockReturnValue(undefined);
|
|
svc().notifyTagged('5', { id: 1, email: 'a@b.c' } as never, 'Clothes', [2]);
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
expect(send).toHaveBeenCalledWith(
|
|
expect.objectContaining({ params: expect.objectContaining({ trip: 'Untitled' }) }),
|
|
);
|
|
});
|
|
});
|
|
});
|