mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
818021908f
Four of the fixes already on this branch were wrong, and an adversarial read of the diff is what turned them up. The shared merge helper claimed to implement the server's COALESCE(assignment_time, place_time) and did not: it pinned the embedded times to whatever the assignment already showed, ignoring the override column entirely. Sharing that with the WS applier traded one bug for another - a collaborator moving a place from 08:00 to 10:00 stopped reaching the day card for every assignment WITHOUT an override, which is most of them. It merges the way the server projects now, and the test that was supposed to pin this seeded a fixture the server could never produce (embedded times that differ from the pool row with no override to explain the difference). The per-iteration auth gate went into every loop in tripSyncManager except the one that matters most: the mutation-queue replay, which writes server responses straight into Dexie. After a logout that is the shared anonymous database. The fire-and-forget tags/categories writes two lines below a gate added in the same commit were ungated for the same reason - they resolve later, and the check has to happen then. safeHttpUrl is backed by placeWebsiteSchema: http(s) only, capped at 500 characters. A booking url is deliberately free-form - shared's own contract says so in as many words, because people paste bare hosts and long provider deep links - so guarding it with the stricter schema turned a link somebody saved last year into dead text. safeExternalHref refuses the schemes that execute in this origin and gives a scheme-less value https://, which is what an address bar does with the same input. And the memories timeout was 20s while the server's own budget for a Synology call is 30s, doubled again when the session has expired and it re-authenticates: a slow NAS that answered before this branch would have started reading as disconnected. Those calls had no client timeout at all before; they have none again, and the ones that need to give up early carry an AbortSignal. The notes thumbnail also loses its NUL byte cache separator - it made git treat the whole file as binary, so a security fix landed in it unreviewable - and gains the same cache cap its chat sibling got.
154 lines
6.2 KiB
TypeScript
154 lines
6.2 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { useTripStore } from '../../../src/store/tripStore';
|
|
import { resetAllStores } from '../../helpers/store';
|
|
import { buildPlace, buildAssignment } from '../../helpers/factories';
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
});
|
|
|
|
describe('remoteEventHandler > places', () => {
|
|
const seedData = () => {
|
|
const place = buildPlace({ id: 1, name: 'Original' });
|
|
const assignment = buildAssignment({ id: 100, place, day_id: 10 });
|
|
useTripStore.setState({
|
|
places: [place],
|
|
assignments: { '10': [assignment] },
|
|
});
|
|
};
|
|
|
|
it('FE-WSEVT-PLACE-001: place:created prepends new place to places array', () => {
|
|
seedData();
|
|
const newPlace = buildPlace({ id: 99, name: 'New Place' });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:created', place: newPlace });
|
|
const { places } = useTripStore.getState();
|
|
expect(places[0].id).toBe(99);
|
|
expect(places).toHaveLength(2);
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-002: place:created is idempotent — no duplicate if same ID', () => {
|
|
seedData();
|
|
const duplicate = buildPlace({ id: 1, name: 'Duplicate' });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:created', place: duplicate });
|
|
const { places } = useTripStore.getState();
|
|
expect(places).toHaveLength(1);
|
|
expect(places[0].name).toBe('Original');
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-003: place:updated updates place in places array', () => {
|
|
seedData();
|
|
const updated = buildPlace({ id: 1, name: 'Updated Name' });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: updated });
|
|
const { places } = useTripStore.getState();
|
|
expect(places[0].name).toBe('Updated Name');
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-004: place:updated cascades into assignments nested place', () => {
|
|
seedData();
|
|
const updated = buildPlace({ id: 1, name: 'Cascaded Update' });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: updated });
|
|
const { assignments } = useTripStore.getState();
|
|
expect(assignments['10'][0].place?.name).toBe('Cascaded Update');
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-004b: place:updated keeps a per-stop override', () => {
|
|
// The day view renders COALESCE(assignment_time, place_time), so an
|
|
// assignment that carries its own time keeps it when the pool row changes
|
|
// (the local reducer merges the same way).
|
|
const place = buildPlace({ id: 1, name: 'Original', place_time: '08:00', end_time: '09:00' });
|
|
const assignment = buildAssignment({
|
|
id: 100,
|
|
day_id: 10,
|
|
assignment_time: '14:00',
|
|
assignment_end_time: '15:30',
|
|
place: { ...place, place_time: '14:00', end_time: '15:30' },
|
|
});
|
|
useTripStore.setState({ places: [place], assignments: { '10': [assignment] } });
|
|
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'place:updated',
|
|
place: buildPlace({ id: 1, name: 'Renamed', place_time: '08:00', end_time: '09:00' }),
|
|
});
|
|
|
|
const embedded = useTripStore.getState().assignments['10'][0].place!;
|
|
expect(embedded.name).toBe('Renamed');
|
|
expect(embedded.place_time).toBe('14:00');
|
|
expect(embedded.end_time).toBe('15:30');
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-004c: without an override the pool time reaches the day card', () => {
|
|
// The other half of the COALESCE. An assignment with no assignment_time
|
|
// shows whatever the pool row says, so a collaborator moving the place from
|
|
// 08:00 to 10:00 has to land here - freezing the embedded copy would leave
|
|
// the day plan wrong until the next reload.
|
|
const place = buildPlace({ id: 1, name: 'Original', place_time: '08:00', end_time: '09:00' });
|
|
const assignment = buildAssignment({
|
|
id: 100,
|
|
day_id: 10,
|
|
assignment_time: null,
|
|
assignment_end_time: null,
|
|
place: { ...place },
|
|
});
|
|
useTripStore.setState({ places: [place], assignments: { '10': [assignment] } });
|
|
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'place:updated',
|
|
place: buildPlace({ id: 1, name: 'Original', place_time: '10:00', end_time: '11:00' }),
|
|
});
|
|
|
|
const embedded = useTripStore.getState().assignments['10'][0].place!;
|
|
expect(embedded.place_time).toBe('10:00');
|
|
expect(embedded.end_time).toBe('11:00');
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-005: place:deleted removes place from places array', () => {
|
|
seedData();
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 });
|
|
const { places } = useTripStore.getState();
|
|
expect(places).toHaveLength(0);
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-006: place:deleted cascades — assignments referencing that place are removed', () => {
|
|
seedData();
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 });
|
|
const { assignments } = useTripStore.getState();
|
|
expect(assignments['10']).toHaveLength(0);
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-007: place:updated leaves other places and their assignments untouched', () => {
|
|
const one = buildPlace({ id: 1, name: 'One' });
|
|
const two = buildPlace({ id: 2, name: 'Two' });
|
|
useTripStore.setState({
|
|
places: [one, two],
|
|
assignments: {
|
|
'10': [
|
|
buildAssignment({ id: 100, place: one, day_id: 10 }),
|
|
buildAssignment({ id: 200, place: two, day_id: 10 }),
|
|
],
|
|
},
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: buildPlace({ id: 2, name: 'Two renamed' }) });
|
|
const { places, assignments } = useTripStore.getState();
|
|
expect(places.map(p => p.name)).toEqual(['One', 'Two renamed']);
|
|
expect(assignments['10'].map(a => a.place?.name)).toEqual(['One', 'Two renamed']);
|
|
});
|
|
|
|
it('FE-WSEVT-PLACE-008: place:deleted keeps assignments of the other places', () => {
|
|
const one = buildPlace({ id: 1 });
|
|
const two = buildPlace({ id: 2 });
|
|
useTripStore.setState({
|
|
places: [one, two],
|
|
assignments: {
|
|
'10': [
|
|
buildAssignment({ id: 100, place: one, day_id: 10 }),
|
|
buildAssignment({ id: 200, place: two, day_id: 10 }),
|
|
],
|
|
},
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 });
|
|
const { places, assignments } = useTripStore.getState();
|
|
expect(places.map(p => p.id)).toEqual([2]);
|
|
expect(assignments['10'].map(a => a.id)).toEqual([200]);
|
|
});
|
|
});
|