mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
125 lines
7.2 KiB
TypeScript
125 lines
7.2 KiB
TypeScript
/**
|
|
* pluginEventMeta (#plugins, event enrichment): derives the { entity, entityId } a
|
|
* core event carries for subscribed plugins. entity = the family; entityId comes
|
|
* ONLY from a per-family whitelist, so a non-entity id (e.g. a userId) can never
|
|
* leak, and reorder/bulk/sub-entity payloads yield no id.
|
|
*/
|
|
import { pluginEventMeta } from '../../../src/plugin-event-sink';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
describe('pluginEventMeta', () => {
|
|
it('reads the nested object id and the flat *Id key per family', () => {
|
|
// nested-object payloads additionally yield a snapshot (covered below)
|
|
expect(pluginEventMeta('place:created', { place: { id: 7 } })).toMatchObject({ entity: 'place', entityId: 7 });
|
|
expect(pluginEventMeta('place:deleted', { placeId: 12 })).toEqual({ entity: 'place', entityId: 12 });
|
|
expect(pluginEventMeta('reservation:updated', { reservation: { id: 40 } })).toMatchObject({
|
|
entity: 'reservation',
|
|
entityId: 40,
|
|
});
|
|
expect(pluginEventMeta('reservation:deleted', { reservationId: 40 })).toEqual({
|
|
entity: 'reservation',
|
|
entityId: 40,
|
|
});
|
|
expect(pluginEventMeta('day:updated', { day: { id: 3 } })).toMatchObject({ entity: 'day', entityId: 3 });
|
|
expect(pluginEventMeta('accommodation:deleted', { accommodationId: 9 })).toEqual({
|
|
entity: 'accommodation',
|
|
entityId: 9,
|
|
});
|
|
expect(pluginEventMeta('file:created', { file: { id: 5 } })).toMatchObject({ entity: 'file', entityId: 5 });
|
|
expect(pluginEventMeta('assignment:created', { assignment: { id: 30 } })).toMatchObject({
|
|
entity: 'assignment',
|
|
entityId: 30,
|
|
});
|
|
expect(pluginEventMeta('packing:deleted', { itemId: 2 })).toEqual({ entity: 'packing', entityId: 2 });
|
|
expect(pluginEventMeta('budget:updated', { item: { id: 8 } })).toMatchObject({ entity: 'budget', entityId: 8 });
|
|
});
|
|
|
|
it('picks the ENTITY id, not a parent id and never a non-entity id', () => {
|
|
// A day-note's own id is the note, not its parent day.
|
|
expect(pluginEventMeta('dayNote:created', { dayId: 3, note: { id: 50 } })).toMatchObject({
|
|
entity: 'dayNote',
|
|
entityId: 50,
|
|
});
|
|
expect(pluginEventMeta('dayNote:deleted', { noteId: 50, dayId: 3 })).toEqual({ entity: 'dayNote', entityId: 50 });
|
|
// THE LEAK TEST: this event carries a userId — the mapper must surface itemId, NEVER userId.
|
|
expect(pluginEventMeta('budget:member-paid-updated', { itemId: 8, userId: 99, paid: 1 })).toEqual({
|
|
entity: 'budget',
|
|
entityId: 8,
|
|
});
|
|
});
|
|
|
|
it('reads trip via `id` / `trip.id`', () => {
|
|
expect(pluginEventMeta('trip:deleted', { id: 1 })).toEqual({ entity: 'trip', entityId: 1 });
|
|
expect(pluginEventMeta('trip:updated', { trip: { id: 1 } })).toMatchObject({ entity: 'trip', entityId: 1 });
|
|
});
|
|
|
|
it('yields the family only (no entityId) for bare, reorder, bulk and sub-entity payloads', () => {
|
|
expect(pluginEventMeta('reservation:created', {})).toEqual({ entity: 'reservation' }); // bare sibling (accommodation create)
|
|
expect(pluginEventMeta('day:reordered', { orderedIds: [1, 2, 3] })).toEqual({ entity: 'day' });
|
|
expect(pluginEventMeta('budget:reordered', { orderedCategories: ['a'] })).toEqual({ entity: 'budget' });
|
|
expect(pluginEventMeta('reservation:positions', { positions: [], day_id: 3 })).toEqual({ entity: 'reservation' });
|
|
expect(pluginEventMeta('budget:settlement-created', { settlement: { id: 2 } })).toEqual({ entity: 'budget' }); // sub-entity, not surfaced
|
|
expect(pluginEventMeta('packing:bag-deleted', { bagId: 4 })).toEqual({ entity: 'packing' });
|
|
});
|
|
|
|
it('is pure + defensive: unknown family, empty/odd payloads never throw', () => {
|
|
expect(pluginEventMeta('mystery:thing', { id: 5 })).toEqual({ entity: 'mystery' }); // family not whitelisted -> no id even if present
|
|
expect(pluginEventMeta('place:created', {})).toEqual({ entity: 'place' });
|
|
expect(pluginEventMeta('place:created', { place: null } as never)).toEqual({ entity: 'place' });
|
|
expect(pluginEventMeta('place:created', { placeId: 'not-a-number' })).toEqual({ entity: 'place' });
|
|
expect(pluginEventMeta('place:created', { placeId: '15' })).toEqual({ entity: 'place', entityId: 15 }); // numeric string coerced
|
|
expect(pluginEventMeta('', {})).toBeUndefined();
|
|
});
|
|
|
|
describe('snapshot (wave 9)', () => {
|
|
it('whitelists the entity fields of created/updated payloads', () => {
|
|
const m = pluginEventMeta('day:updated', {
|
|
day: { id: 3, trip_id: 7, day_number: 1, date: '2027-01-01', title: 'Kyoto', notes: 'x' },
|
|
});
|
|
expect(m?.snapshot).toEqual({ id: 3, trip_id: 7, day_number: 1, date: '2027-01-01', title: 'Kyoto', notes: 'x' });
|
|
// reservations keep their endpoints (transport legs carry no user data)
|
|
const r = pluginEventMeta('reservation:created', {
|
|
reservation: { id: 40, title: 'Flight', endpoints: [{ role: 'from', name: 'HND' }] },
|
|
});
|
|
expect(r?.snapshot).toMatchObject({ id: 40, title: 'Flight', endpoints: [{ role: 'from', name: 'HND' }] });
|
|
});
|
|
|
|
it('never surfaces a user id, a foreign whitelist field or trips.feed_token', () => {
|
|
// budget rows may arrive hydrated (paid_by/members/payers) — none of it travels
|
|
const b = pluginEventMeta('budget:created', {
|
|
item: { id: 8, name: 'Hotel', total_price: 100, paid_by: 99, members: [{ user_id: 99 }], payers: [99] },
|
|
});
|
|
expect(b?.snapshot).toEqual({ id: 8, name: 'Hotel', total_price: 100 });
|
|
// the trip owner + the secret calendar feed token stay host-side
|
|
const t = pluginEventMeta('trip:updated', { trip: { id: 1, title: 'Japan', user_id: 5, feed_token: 'sekret' } });
|
|
expect(t?.snapshot).toEqual({ id: 1, title: 'Japan' });
|
|
// files: uploaded_by / disk filename are not in the whitelist
|
|
const f = pluginEventMeta('file:created', {
|
|
file: { id: 5, original_name: 'visa.pdf', filename: 'ab12.pdf', uploaded_by: 9 },
|
|
});
|
|
expect(f?.snapshot).toEqual({ id: 5, original_name: 'visa.pdf' });
|
|
// assignments: participants (user ids) never travel
|
|
const a = pluginEventMeta('assignment:created', {
|
|
assignment: { id: 30, day_id: 3, place_id: 7, participants: [{ user_id: 9 }] },
|
|
});
|
|
expect(a?.snapshot).toEqual({ id: 30, day_id: 3, place_id: 7 });
|
|
});
|
|
|
|
it('yields NO snapshot for a private packing item (#858) but keeps the id hint', () => {
|
|
const priv = pluginEventMeta('packing:created', { item: { id: 70, name: 'Meds', is_private: 1, owner_id: 5 } });
|
|
expect(priv).toEqual({ entity: 'packing', entityId: 70 });
|
|
const pub = pluginEventMeta('packing:created', { item: { id: 71, name: 'Socks', is_private: 0 } });
|
|
expect(pub?.snapshot).toEqual({ id: 71, name: 'Socks' }); // is_private itself isn't whitelisted either
|
|
});
|
|
|
|
it('yields no snapshot for deletes, reorders and unknown families', () => {
|
|
expect(pluginEventMeta('place:deleted', { placeId: 12 })?.snapshot).toBeUndefined();
|
|
expect(pluginEventMeta('day:reordered', { orderedIds: [1, 2] })?.snapshot).toBeUndefined();
|
|
expect(pluginEventMeta('mystery:thing', { mystery: { id: 5 } })?.snapshot).toBeUndefined();
|
|
// an array at the entity key is not an entity object
|
|
expect(pluginEventMeta('place:created', { place: [1, 2] } as never)?.snapshot).toBeUndefined();
|
|
});
|
|
});
|
|
});
|