mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
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();
|
|
});
|
|
});
|
|
});
|