From a4fe3241b2a6742a485b8b62b7c8915bdb0779a2 Mon Sep 17 00:00:00 2001 From: jubnl Date: Thu, 24 Sep 2026 06:49:24 +0200 Subject: [PATCH] =?UTF-8?q?test(server):=20plan=204=20=E2=80=94=20full-key?= =?UTF-8?q?=20parity=20tests=20for=20ShareTokensRepository=20(3h=20L4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3h L4's carry: ShareTokensRepository (SH1-SH6/SH17/UC6) had zero tests. 17 cases pinning every read against the legacy statement run raw on the same connection — findRawByTrip/findTokenByTrip with every nullable column both null and set, findValidByToken/findTripAndShareMapByToken/ findTripIdByToken's shared token+expiry predicate (valid, future-expiry, past-expiry, unknown token), plus insertNew/updateFlagsByTrip/deleteByTrip/ deleteByCreator's write correctness (deleteByCreator's cross-trip erasure kept distinct from deleteByTrip's single-trip scope, per the repository's own docstring). The listPublicForShare family (the other half of 3h L4) is a separate, larger carry — not in this file. --- .../ShareTokens.repository.test.ts | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 server/tests/unit/db/repositories/ShareTokens.repository.test.ts diff --git a/server/tests/unit/db/repositories/ShareTokens.repository.test.ts b/server/tests/unit/db/repositories/ShareTokens.repository.test.ts new file mode 100644 index 000000000..05f4d2999 --- /dev/null +++ b/server/tests/unit/db/repositories/ShareTokens.repository.test.ts @@ -0,0 +1,275 @@ +/** + * ShareTokensRepository — full-key parity (rule 19) for SH1-SH6/SH17/UC6, + * plan 4 Task 8b's 3h L4 carry ("3h L4 `ShareTokens` + the + * `listPublicForShare` family full-key parity tests" — this file is the + * `ShareTokens` half; the `listPublicForShare` family lives on its own + * repositories). Every read below is checked against the legacy statement + * run raw on the SAME better-sqlite3 connection the repository itself + * reads through, with every nullable column seeded both null and set, per + * the shape `InviteTokens.repository.test.ts` uses. + */ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { createSnapshotTestDb } from '../../../helpers/db-mock'; +import { resetTestDb } from '../../../helpers/test-db'; +import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; +import { createTrip, createUser } from '../../../helpers/factories'; +import { ShareTokens } from '../../../../src/db/entities/ShareTokens.entity'; +import type { ShareTokensRepository } from '../../../../src/db/repositories/ShareTokens.repository'; + +const testDb = createSnapshotTestDb(); +let t: TestOrm; +let repo: ShareTokensRepository; + +beforeAll(async () => { + t = await createTestOrm(testDb); + repo = t.repo(ShareTokens); +}); +beforeEach(() => { resetTestDb(testDb); t.clear(); }); +afterAll(async () => { await t.close(); testDb.close(); }); + +function rawShareToken(id: number): unknown { + return testDb.prepare('SELECT * FROM share_tokens WHERE id = ?').get(id); +} + +/** A raw `share_tokens` insert — no factory exists for this table yet. */ +function insertShareToken( + tripId: number, + createdBy: number, + overrides: Partial<{ + token: string; + share_map: number | null; + share_bookings: number | null; + share_packing: number | null; + share_budget: number | null; + share_collab: number | null; + expires_at: string | null; + }> = {}, +): number { + const result = testDb.prepare( + `INSERT INTO share_tokens + (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ).run( + tripId, + overrides.token ?? `token-${tripId}-${crypto.randomUUID().slice(0, 8)}`, + createdBy, + overrides.share_map === undefined ? 1 : overrides.share_map, + overrides.share_bookings === undefined ? 1 : overrides.share_bookings, + overrides.share_packing === undefined ? 0 : overrides.share_packing, + overrides.share_budget === undefined ? 0 : overrides.share_budget, + overrides.share_collab === undefined ? 0 : overrides.share_collab, + overrides.expires_at === undefined ? null : overrides.expires_at, + ); + return Number(result.lastInsertRowid); +} + +describe('ShareTokensRepository', () => { + describe('findRawByTrip (SH4) — full row, every nullable column both null and set', () => { + it('SHTOKREPO-001: a share_tokens row with every nullable column NULL matches SELECT * run raw', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + const id = insertShareToken(trip.id, user.id, { + share_map: null, share_bookings: null, share_packing: null, share_budget: null, share_collab: null, expires_at: null, + }); + + const row = await repo.findRawByTrip(trip.id); + + expect(row).toEqual(rawShareToken(id)); + }); + + it('SHTOKREPO-002: a share_tokens row with every nullable column SET matches SELECT * run raw', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + const id = insertShareToken(trip.id, user.id, { + share_map: 1, share_bookings: 0, share_packing: 1, share_budget: 0, share_collab: 1, expires_at: '2099-01-01 00:00:00', + }); + + const row = await repo.findRawByTrip(trip.id); + + expect(row).toEqual(rawShareToken(id)); + }); + + it('SHTOKREPO-003: no row for the trip is undefined', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + expect(await repo.findRawByTrip(trip.id)).toBeUndefined(); + }); + }); + + describe('findTokenByTrip (SH1)', () => { + it('SHTOKREPO-004: matches SELECT token FROM share_tokens WHERE trip_id = ? run raw', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + insertShareToken(trip.id, user.id, { token: 'find-me-by-trip' }); + + const legacy = testDb.prepare('SELECT token FROM share_tokens WHERE trip_id = ?').get(trip.id); + + expect(await repo.findTokenByTrip(trip.id)).toEqual(legacy); + }); + + it('SHTOKREPO-005: no row for the trip is undefined', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + expect(await repo.findTokenByTrip(trip.id)).toBeUndefined(); + }); + }); + + describe('findValidByToken (SH6) — the shared token/expiry predicate', () => { + it('SHTOKREPO-006: a token with expires_at NULL (never expires) matches the full row run raw', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + const id = insertShareToken(trip.id, user.id, { token: 'never-expires', expires_at: null }); + + const legacy = testDb + .prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") + .get('never-expires'); + + const row = await repo.findValidByToken('never-expires'); + expect(row).toEqual(legacy); + expect(row).toEqual(rawShareToken(id)); + }); + + it('SHTOKREPO-007: a token with a future expires_at still matches', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + insertShareToken(trip.id, user.id, { token: 'future-expiry', expires_at: '2099-01-01 00:00:00' }); + + const legacy = testDb + .prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") + .get('future-expiry'); + + expect(await repo.findValidByToken('future-expiry')).toEqual(legacy); + }); + + it('SHTOKREPO-008: an EXPIRED token (past expires_at) matches neither the repository read nor the legacy predicate — both undefined', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + insertShareToken(trip.id, user.id, { token: 'past-expiry', expires_at: '2020-01-01 00:00:00' }); + + const legacy = testDb + .prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") + .get('past-expiry'); + + expect(legacy).toBeUndefined(); + expect(await repo.findValidByToken('past-expiry')).toBeUndefined(); + }); + + it('SHTOKREPO-009: an unknown token is undefined', async () => { + expect(await repo.findValidByToken('does-not-exist')).toBeUndefined(); + }); + }); + + describe('findTripAndShareMapByToken (SH17) — narrow projection, same valid-token predicate', () => { + it('SHTOKREPO-010: matches the narrow legacy projection, share_map both set and NULL', async () => { + const { user } = createUser(testDb); + const tripA = createTrip(testDb, user.id); + const tripB = createTrip(testDb, user.id); + insertShareToken(tripA.id, user.id, { token: 'map-set', share_map: 1 }); + insertShareToken(tripB.id, user.id, { token: 'map-null', share_map: null }); + + const legacySet = testDb + .prepare("SELECT trip_id, share_map FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") + .get('map-set'); + const legacyNull = testDb + .prepare("SELECT trip_id, share_map FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") + .get('map-null'); + + expect(await repo.findTripAndShareMapByToken('map-set')).toEqual(legacySet); + expect(await repo.findTripAndShareMapByToken('map-null')).toEqual(legacyNull); + }); + + it('SHTOKREPO-011: an expired token is undefined, same as the legacy predicate', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + insertShareToken(trip.id, user.id, { token: 'expired-map', expires_at: '2020-01-01 00:00:00' }); + expect(await repo.findTripAndShareMapByToken('expired-map')).toBeUndefined(); + }); + }); + + describe('findTripIdByToken (R1, platform.routes.ts pre-init fallback) — same valid-token predicate', () => { + it('SHTOKREPO-012: matches the narrow legacy projection', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + insertShareToken(trip.id, user.id, { token: 'trip-id-only' }); + + const legacy = testDb + .prepare("SELECT trip_id FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") + .get('trip-id-only'); + + expect(await repo.findTripIdByToken('trip-id-only')).toEqual(legacy); + }); + + it('SHTOKREPO-013: an expired token is undefined', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + insertShareToken(trip.id, user.id, { token: 'trip-id-expired', expires_at: '2020-01-01 00:00:00' }); + expect(await repo.findTripIdByToken('trip-id-expired')).toBeUndefined(); + }); + }); + + describe('insertNew (SH3) / updateFlagsByTrip (SH2) / deleteByTrip (SH5) / deleteByCreator (UC6)', () => { + it('SHTOKREPO-014: insertNew writes exactly the given columns, re-readable raw', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + + await repo.insertNew({ + trip_id: trip.id, token: 'inserted-token', created_by: user.id, + share_map: 1, share_bookings: 0, share_packing: 1, share_budget: 0, share_collab: 1, expires_at: '2099-06-01 00:00:00', + }); + + const raw = testDb.prepare('SELECT * FROM share_tokens WHERE token = ?').get('inserted-token') as Record; + expect(raw).toMatchObject({ + trip_id: trip.id, token: 'inserted-token', created_by: user.id, + share_map: 1, share_bookings: 0, share_packing: 1, share_budget: 0, share_collab: 1, expires_at: '2099-06-01 00:00:00', + }); + }); + + it('SHTOKREPO-015: updateFlagsByTrip overwrites all six flag columns for that trip only', async () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + const other = createTrip(testDb, user.id); + const id = insertShareToken(trip.id, user.id, { token: 'to-update' }); + const otherId = insertShareToken(other.id, user.id, { token: 'untouched' }); + + await repo.updateFlagsByTrip(trip.id, { + share_map: 0, share_bookings: 0, share_packing: 0, share_budget: 0, share_collab: 0, expires_at: '2099-12-31 00:00:00', + }); + + expect(rawShareToken(id)).toMatchObject({ + share_map: 0, share_bookings: 0, share_packing: 0, share_budget: 0, share_collab: 0, expires_at: '2099-12-31 00:00:00', + }); + // The other trip's row is untouched — a different predicate, not a global update. + expect(rawShareToken(otherId)).toMatchObject({ share_map: 1, share_bookings: 1 }); + }); + + it('SHTOKREPO-016: deleteByTrip removes exactly that trip\'s row, leaving another trip\'s row alone', async () => { + const { user } = createUser(testDb); + const tripA = createTrip(testDb, user.id); + const tripB = createTrip(testDb, user.id); + insertShareToken(tripA.id, user.id, { token: 'delete-me' }); + const keepId = insertShareToken(tripB.id, user.id, { token: 'keep-me' }); + + await repo.deleteByTrip(tripA.id); + + expect(await repo.findRawByTrip(tripA.id)).toBeUndefined(); + expect(rawShareToken(keepId)).toBeDefined(); + }); + + it('SHTOKREPO-017: deleteByCreator removes every link that user created, across every trip, leaving another creator\'s links alone (DISTINCT from deleteByTrip)', async () => { + const { user: creator } = createUser(testDb); + const { user: otherCreator } = createUser(testDb); + const tripA = createTrip(testDb, creator.id); + const tripB = createTrip(testDb, creator.id); + const tripC = createTrip(testDb, otherCreator.id); + insertShareToken(tripA.id, creator.id, { token: 'creator-a' }); + insertShareToken(tripB.id, creator.id, { token: 'creator-b' }); + const otherId = insertShareToken(tripC.id, otherCreator.id, { token: 'other-creator' }); + + await repo.deleteByCreator(creator.id); + + expect(await repo.findRawByTrip(tripA.id)).toBeUndefined(); + expect(await repo.findRawByTrip(tripB.id)).toBeUndefined(); + expect(rawShareToken(otherId)).toBeDefined(); + }); + }); +});