mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
test(server): seed the trip read model, share token and audit log specs through the factories
Converts the raw prepare() fixtures and reads in the trip read model service spec and the share token and audit log repository specs to the ORM factories and readers. The trip read model spec drops the raw canAccessTrip and isOwner it still put on the database mock; nothing has read them since trip access moved onto the repositories. The share token spec now inserts through makeShareToken and reads what a write left behind through findRow, keeping the SELECT * row and the legacy statements it is held to as allowed oracles. The audit log spec keeps the raw row and the typeof() storage class probe behind allow markers, since both are about what SQLite stores.
This commit is contained in:
@@ -7,6 +7,8 @@ import { AuditLog } from '../../../../src/db/entities/AuditLog.entity';
|
||||
import { AppSettings } from '../../../../src/db/entities/AppSettings.entity';
|
||||
import type { AuditLogRepository } from '../../../../src/db/repositories/AuditLog.repository';
|
||||
import { DB_TIMESTAMP_RE } from '../../../../src/db/types';
|
||||
import { Users } from '../../../../src/db/entities/Users.entity';
|
||||
import { countRows, deleteRows, findRow } from '../../../helpers/factories/rows';
|
||||
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
@@ -23,6 +25,7 @@ describe('AuditLogRepository', () => {
|
||||
it('AUDITREPO-001: insert writes the legacy column set, matching the SELECT * row exactly', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'trip.create', resource: 'trip', details: '{"title":"Rome"}', ip: '1.2.3.4' });
|
||||
// test-sql-allow: the stored SELECT * row is the oracle for the legacy column set the insert must write.
|
||||
const row = testDb.prepare('SELECT * FROM audit_log').get() as { id: number; created_at: string };
|
||||
expect(row).toStrictEqual({
|
||||
id: expect.any(Number),
|
||||
@@ -38,6 +41,7 @@ describe('AuditLogRepository', () => {
|
||||
it('AUDITREPO-002: created_at is left to the column default, as text, in the YYYY-MM-DD HH:MM:SS shape', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null });
|
||||
// test-sql-allow: typeof() reports the storage class SQLite holds, which no entity maps.
|
||||
const stored = testDb.prepare('SELECT created_at, typeof(created_at) AS kind FROM audit_log').get() as { created_at: string; kind: string };
|
||||
expect(stored.kind).toBe('text');
|
||||
expect(stored.created_at).toMatch(DB_TIMESTAMP_RE);
|
||||
@@ -45,15 +49,15 @@ describe('AuditLogRepository', () => {
|
||||
|
||||
it('AUDITREPO-003: a null user writes a null user_id twin, not a foreign-key value', async () => {
|
||||
await auditLog.insertEntry({ user_id: null, action: 'user.login', resource: null, details: null, ip: null });
|
||||
const row = testDb.prepare('SELECT user_id FROM audit_log').get() as { user_id: number | null };
|
||||
expect(row.user_id).toBeNull();
|
||||
const row = await findRow(t, AuditLog, {});
|
||||
expect(row?.user_id).toBeNull();
|
||||
});
|
||||
|
||||
it('AUDITREPO-004: null resource/details/ip are stored as NULL, not the string "null"', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null });
|
||||
const row = testDb.prepare('SELECT resource, details, ip FROM audit_log').get();
|
||||
expect(row).toEqual({ resource: null, details: null, ip: null });
|
||||
const row = await findRow(t, AuditLog, {});
|
||||
expect(row).toMatchObject({ resource: null, details: null, ip: null });
|
||||
});
|
||||
|
||||
it('AUDITREPO-005: resolves without returning a value — no id or row is read back', async () => {
|
||||
@@ -69,11 +73,11 @@ describe('AuditLogRepository', () => {
|
||||
t.em.create(AppSettings, { key: 'pending-during-audit-insert', value: 'should-not-be-written' });
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'trip.create', resource: 'trip', details: null, ip: null });
|
||||
|
||||
const pendingCount = (testDb.prepare('SELECT COUNT(*) AS n FROM app_settings WHERE key = ?').get('pending-during-audit-insert') as { n: number }).n;
|
||||
const pendingCount = await countRows(t, AppSettings, { key: 'pending-during-audit-insert' });
|
||||
expect(pendingCount).toBe(0);
|
||||
|
||||
const auditRow = testDb.prepare('SELECT action FROM audit_log').get() as { action: string };
|
||||
expect(auditRow.action).toBe('trip.create');
|
||||
const auditRow = await findRow(t, AuditLog, {});
|
||||
expect(auditRow?.action).toBe('trip.create');
|
||||
});
|
||||
|
||||
// ── Plan 3i Task 0 — listPage/count (AD22/AD23) ────────────────────────────
|
||||
@@ -98,8 +102,9 @@ describe('AuditLogRepository', () => {
|
||||
// bob is deleted after the fact; `audit_log.user_id REFERENCES users(id)
|
||||
// ON DELETE SET NULL` fires — this row's own user_id becomes NULL, the
|
||||
// same shape a NULL-user_id row has, reached a different way.
|
||||
testDb.prepare('DELETE FROM users WHERE id = ?').run(bob.id);
|
||||
await deleteRows(t, Users, { id: bob.id });
|
||||
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
const legacy = testDb.prepare(LEGACY_PAGE_SQL).all(10, 0);
|
||||
const result = await auditLog.listPage(10, 0);
|
||||
expect(result).toEqual(legacy);
|
||||
@@ -118,6 +123,7 @@ describe('AuditLogRepository', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await auditLog.insertEntry({ user_id: user.id, action: `action.${i}`, resource: null, details: null, ip: null });
|
||||
}
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
const legacyPage = testDb.prepare(LEGACY_PAGE_SQL).all(2, 1);
|
||||
const result = await auditLog.listPage(2, 1);
|
||||
expect(result).toEqual(legacyPage);
|
||||
@@ -129,6 +135,7 @@ describe('AuditLogRepository', () => {
|
||||
await auditLog.insertEntry({ user_id: user.id, action: 'a', resource: null, details: null, ip: null });
|
||||
await auditLog.insertEntry({ user_id: null, action: 'b', resource: null, details: null, ip: null });
|
||||
|
||||
// test-sql-allow: the raw COUNT is the legacy oracle this parity test holds the repository to.
|
||||
const legacyCount = (testDb.prepare('SELECT COUNT(*) as c FROM audit_log').get() as { c: number }).c;
|
||||
const result = await auditLog.count();
|
||||
expect(result).toBe(legacyCount);
|
||||
|
||||
@@ -12,6 +12,8 @@ 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 { findRow } from '../../../helpers/factories/rows';
|
||||
import { makeShareToken } from '../../../helpers/factories/trips';
|
||||
import { createTrip, createUser } from '../../../helpers/factories';
|
||||
import { ShareTokens } from '../../../../src/db/entities/ShareTokens.entity';
|
||||
import type { ShareTokensRepository } from '../../../../src/db/repositories/ShareTokens.repository';
|
||||
@@ -27,12 +29,19 @@ beforeAll(async () => {
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
/** The SELECT * row as the database holds it: the oracle the repository's full rows are compared with. */
|
||||
function rawShareToken(id: number): unknown {
|
||||
// test-sql-allow: the raw row is the oracle the repository's full-row reads are held against.
|
||||
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(
|
||||
/** The stored row, read through the ORM, for the cases that check what a write left behind. */
|
||||
function storedShareToken(id: number) {
|
||||
return findRow(t, ShareTokens, { id });
|
||||
}
|
||||
|
||||
/** A `share_tokens` row with the legacy defaults this suite was written against. */
|
||||
async function insertShareToken(
|
||||
tripId: number,
|
||||
createdBy: number,
|
||||
overrides: Partial<{
|
||||
@@ -44,23 +53,17 @@ function insertShareToken(
|
||||
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);
|
||||
): Promise<number> {
|
||||
const row = await makeShareToken(t, tripId, createdBy, {
|
||||
token: overrides.token ?? `token-${tripId}-${crypto.randomUUID().slice(0, 8)}`,
|
||||
share_map: overrides.share_map === undefined ? 1 : overrides.share_map,
|
||||
share_bookings: overrides.share_bookings === undefined ? 1 : overrides.share_bookings,
|
||||
share_packing: overrides.share_packing === undefined ? 0 : overrides.share_packing,
|
||||
share_budget: overrides.share_budget === undefined ? 0 : overrides.share_budget,
|
||||
share_collab: overrides.share_collab === undefined ? 0 : overrides.share_collab,
|
||||
expires_at: overrides.expires_at === undefined ? null : overrides.expires_at,
|
||||
});
|
||||
return row.id;
|
||||
}
|
||||
|
||||
describe('ShareTokensRepository', () => {
|
||||
@@ -68,7 +71,7 @@ describe('ShareTokensRepository', () => {
|
||||
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, {
|
||||
const id = await insertShareToken(trip.id, user.id, {
|
||||
share_map: null, share_bookings: null, share_packing: null, share_budget: null, share_collab: null, expires_at: null,
|
||||
});
|
||||
|
||||
@@ -80,7 +83,7 @@ describe('ShareTokensRepository', () => {
|
||||
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, {
|
||||
const id = await 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',
|
||||
});
|
||||
|
||||
@@ -100,8 +103,9 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
await insertShareToken(trip.id, user.id, { token: 'find-me-by-trip' });
|
||||
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
const legacy = testDb.prepare('SELECT token FROM share_tokens WHERE trip_id = ?').get(trip.id);
|
||||
|
||||
expect(await repo.findTokenByTrip(trip.id)).toEqual(legacy);
|
||||
@@ -118,9 +122,10 @@ describe('ShareTokensRepository', () => {
|
||||
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 id = await insertShareToken(trip.id, user.id, { token: 'never-expires', expires_at: null });
|
||||
|
||||
const legacy = testDb
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
.prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))")
|
||||
.get('never-expires');
|
||||
|
||||
@@ -132,9 +137,10 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
await insertShareToken(trip.id, user.id, { token: 'future-expiry', expires_at: '2099-01-01 00:00:00' });
|
||||
|
||||
const legacy = testDb
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
.prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))")
|
||||
.get('future-expiry');
|
||||
|
||||
@@ -144,9 +150,10 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
await insertShareToken(trip.id, user.id, { token: 'past-expiry', expires_at: '2020-01-01 00:00:00' });
|
||||
|
||||
const legacy = testDb
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
.prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))")
|
||||
.get('past-expiry');
|
||||
|
||||
@@ -164,13 +171,15 @@ describe('ShareTokensRepository', () => {
|
||||
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 });
|
||||
await insertShareToken(tripA.id, user.id, { token: 'map-set', share_map: 1 });
|
||||
await insertShareToken(tripB.id, user.id, { token: 'map-null', share_map: null });
|
||||
|
||||
const legacySet = testDb
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
.prepare("SELECT trip_id, share_map, share_hide_images FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))")
|
||||
.get('map-set');
|
||||
const legacyNull = testDb
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
.prepare("SELECT trip_id, share_map, share_hide_images FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))")
|
||||
.get('map-null');
|
||||
|
||||
@@ -181,7 +190,7 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
await insertShareToken(trip.id, user.id, { token: 'expired-map', expires_at: '2020-01-01 00:00:00' });
|
||||
expect(await repo.findTripAndShareMapByToken('expired-map')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -190,9 +199,10 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
await insertShareToken(trip.id, user.id, { token: 'trip-id-only' });
|
||||
|
||||
const legacy = testDb
|
||||
// test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to.
|
||||
.prepare("SELECT trip_id FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))")
|
||||
.get('trip-id-only');
|
||||
|
||||
@@ -202,7 +212,7 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
await 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();
|
||||
});
|
||||
});
|
||||
@@ -218,7 +228,7 @@ describe('ShareTokensRepository', () => {
|
||||
share_travel_only: 1, share_hide_images: 0, expires_at: '2099-06-01 00:00:00',
|
||||
});
|
||||
|
||||
const raw = testDb.prepare('SELECT * FROM share_tokens WHERE token = ?').get('inserted-token') as Record<string, unknown>;
|
||||
const raw = await findRow(t, ShareTokens, { token: 'inserted-token' });
|
||||
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,
|
||||
@@ -230,33 +240,33 @@ describe('ShareTokensRepository', () => {
|
||||
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' });
|
||||
const id = await insertShareToken(trip.id, user.id, { token: 'to-update' });
|
||||
const otherId = await 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,
|
||||
share_travel_only: 1, share_hide_images: 1, expires_at: '2099-12-31 00:00:00',
|
||||
});
|
||||
|
||||
expect(rawShareToken(id)).toMatchObject({
|
||||
expect(await storedShareToken(id)).toMatchObject({
|
||||
share_map: 0, share_bookings: 0, share_packing: 0, share_budget: 0, share_collab: 0,
|
||||
share_travel_only: 1, share_hide_images: 1, 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, share_travel_only: 0, share_hide_images: 0 });
|
||||
expect(await storedShareToken(otherId)).toMatchObject({ share_map: 1, share_bookings: 1, share_travel_only: 0, share_hide_images: 0 });
|
||||
});
|
||||
|
||||
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 insertShareToken(tripA.id, user.id, { token: 'delete-me' });
|
||||
const keepId = await insertShareToken(tripB.id, user.id, { token: 'keep-me' });
|
||||
|
||||
await repo.deleteByTrip(tripA.id);
|
||||
|
||||
expect(await repo.findRawByTrip(tripA.id)).toBeUndefined();
|
||||
expect(rawShareToken(keepId)).toBeDefined();
|
||||
expect(await storedShareToken(keepId)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('SHTOKREPO-017: deleteByCreator removes every link that user created, across every trip, leaving another creator\'s links alone (DISTINCT from deleteByTrip)', async () => {
|
||||
@@ -265,15 +275,15 @@ describe('ShareTokensRepository', () => {
|
||||
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 insertShareToken(tripA.id, creator.id, { token: 'creator-a' });
|
||||
await insertShareToken(tripB.id, creator.id, { token: 'creator-b' });
|
||||
const otherId = await 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();
|
||||
expect(await storedShareToken(otherId)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,14 +21,6 @@ vi.mock('../../../src/db/database', async () => {
|
||||
closeDb: () => {},
|
||||
reinitialize: () => {},
|
||||
getPlaceWithTags: () => null,
|
||||
canAccessTrip: (tripId: any, userId: number) =>
|
||||
db.prepare(`
|
||||
SELECT t.id, t.user_id FROM trips t
|
||||
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ?
|
||||
WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)
|
||||
`).get(userId, tripId, userId),
|
||||
isOwner: (tripId: any, userId: number) =>
|
||||
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
|
||||
};
|
||||
return mock;
|
||||
});
|
||||
@@ -76,6 +68,10 @@ import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.s
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photo-registration.service';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { BudgetItems } from '../../../src/db/entities/BudgetItems.entity';
|
||||
import { PackingItems } from '../../../src/db/entities/PackingItems.entity';
|
||||
import { Trips } from '../../../src/db/entities/Trips.entity';
|
||||
import { insertRow, updateRows } from '../../helpers/factories/rows';
|
||||
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
|
||||
import {
|
||||
createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, sharedTestOrm,
|
||||
@@ -245,13 +241,13 @@ afterAll(() => {
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const addBudgetItem = (tripId: number, name: string, totalPrice: number) =>
|
||||
testDb.prepare("INSERT INTO budget_items (trip_id, category, name, total_price) VALUES (?, 'food', ?, ?)")
|
||||
.run(tripId, name, totalPrice);
|
||||
const orm = () => sharedTestOrm(testDb);
|
||||
|
||||
const addPackingItem = (tripId: number, name: string, checked: number) =>
|
||||
testDb.prepare('INSERT INTO packing_items (trip_id, name, checked) VALUES (?, ?, ?)')
|
||||
.run(tripId, name, checked);
|
||||
const addBudgetItem = async (tripId: number, name: string, totalPrice: number) =>
|
||||
insertRow(await orm(), BudgetItems, { trip: tripId, category: 'food', name, total_price: totalPrice });
|
||||
|
||||
const addPackingItem = async (tripId: number, name: string, checked: number) =>
|
||||
insertRow(await orm(), PackingItems, { trip: tripId, name, checked });
|
||||
|
||||
// R8 (Plan 3c program brief item 8): the SQL-text-keyed Proxy fault injection
|
||||
// this file used to build (`ownerlessDbs`, keyed on the literal
|
||||
@@ -297,7 +293,7 @@ describe('getTripSummary — feed_token never reaches the wire (TR-B)', () => {
|
||||
it('TRIP-READ-002b: withoutFeedToken strips feed_token even though findRaw hands it back intact', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
testDb.prepare('UPDATE trips SET feed_token = ? WHERE id = ?').run('secret-anon-feed-credential', trip.id);
|
||||
await updateRows(await orm(), Trips, { id: trip.id }, { feed_token: 'secret-anon-feed-credential' });
|
||||
|
||||
// TripsRepository.findRaw is a plain `SELECT *` — it hands feed_token back
|
||||
// intact (proven directly, bypassing the service, so this test would fail
|
||||
@@ -317,13 +313,13 @@ describe('getTripSummary shaping', () => {
|
||||
it('TRIP-READ-003: folds a falsy total_price into the budget total instead of poisoning it', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
addBudgetItem(trip.id, 'Dinner', 40);
|
||||
await addBudgetItem(trip.id, 'Dinner', 40);
|
||||
// total_price is NOT NULL DEFAULT 0, so 0 is the reachable falsy price: a free
|
||||
// entry someone logged to keep it on the list. The `|| 0` in the reduce is what
|
||||
// keeps any such value out of the sum — drop it and a price that arrives
|
||||
// non-numeric turns the whole trip total into NaN, which the MCP summary and
|
||||
// the offline clients both render as an empty budget.
|
||||
addBudgetItem(trip.id, 'Free walking tour', 0);
|
||||
await addBudgetItem(trip.id, 'Free walking tour', 0);
|
||||
|
||||
const summary = (await svc.getTripSummary(trip.id, owner.id))!;
|
||||
expect(summary.budget.item_count).toBe(2);
|
||||
@@ -334,11 +330,12 @@ describe('getTripSummary shaping', () => {
|
||||
it('TRIP-READ-008: totals a foreign-currency bill in the trip currency, at its booked rate (#2525)', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
addBudgetItem(trip.id, 'Dinner', 100);
|
||||
await addBudgetItem(trip.id, 'Dinner', 100);
|
||||
// 801.76 USD booked when a euro bought 1.17 dollars: 685.26 EUR of trip money. The
|
||||
// summary used to add the 801.76 to the euros and report 901.76 EUR.
|
||||
testDb.prepare("INSERT INTO budget_items (trip_id, category, name, total_price, currency, exchange_rate) VALUES (?, 'accommodation', 'Aparthotel Silver', 801.76, 'USD', 1.17)")
|
||||
.run(trip.id);
|
||||
await insertRow(await orm(), BudgetItems, {
|
||||
trip: trip.id, category: 'accommodation', name: 'Aparthotel Silver', total_price: 801.76, currency: 'USD', exchange_rate: 1.17,
|
||||
});
|
||||
|
||||
const summary = (await svc.getTripSummary(trip.id, owner.id))!;
|
||||
expect(summary.budget.total).toBe(785.26);
|
||||
@@ -351,11 +348,12 @@ describe('getTripSummary shaping', () => {
|
||||
it('TRIP-READ-009: totals a trip saved without a currency in euros, the default the rest of the app reads it in (#2525)', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
testDb.prepare('UPDATE trips SET currency = NULL WHERE id = ?').run(trip.id);
|
||||
addBudgetItem(trip.id, 'Dinner', 100);
|
||||
await updateRows(await orm(), Trips, { id: trip.id }, { currency: null });
|
||||
await addBudgetItem(trip.id, 'Dinner', 100);
|
||||
// 117.33 USD booked at 1.1733 dollars to the euro is 100 EUR of trip money.
|
||||
testDb.prepare("INSERT INTO budget_items (trip_id, category, name, total_price, currency, exchange_rate) VALUES (?, 'transport', 'Taxi', 117.33, 'USD', 1.1733)")
|
||||
.run(trip.id);
|
||||
await insertRow(await orm(), BudgetItems, {
|
||||
trip: trip.id, category: 'transport', name: 'Taxi', total_price: 117.33, currency: 'USD', exchange_rate: 1.1733,
|
||||
});
|
||||
|
||||
const summary = (await svc.getTripSummary(trip.id, owner.id))!;
|
||||
expect(summary.budget.total).toBe(200);
|
||||
@@ -366,9 +364,9 @@ describe('getTripSummary shaping', () => {
|
||||
it('TRIP-READ-004: counts only checked packing items, not the whole list', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
addPackingItem(trip.id, 'Socks', 1);
|
||||
addPackingItem(trip.id, 'Charger', 1);
|
||||
addPackingItem(trip.id, 'Passport', 0);
|
||||
await addPackingItem(trip.id, 'Socks', 1);
|
||||
await addPackingItem(trip.id, 'Charger', 1);
|
||||
await addPackingItem(trip.id, 'Passport', 0);
|
||||
|
||||
// total and checked come from the same array; if the filter is ever widened the
|
||||
// packing progress the summary reports jumps to 100% while items are still open.
|
||||
@@ -423,9 +421,8 @@ describe('private packing items stay viewer-scoped (#858)', () => {
|
||||
const { user: viewer } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id, { start_date: '2025-06-01', end_date: '2025-06-02' });
|
||||
addTripMember(testDb, trip.id, viewer.id);
|
||||
testDb.prepare("INSERT INTO packing_items (trip_id, name, is_private, owner_id) VALUES (?, 'Ring', 1, ?)")
|
||||
.run(trip.id, owner.id);
|
||||
testDb.prepare("INSERT INTO packing_items (trip_id, name) VALUES (?, 'Tent')").run(trip.id);
|
||||
await insertRow(await orm(), PackingItems, { trip: trip.id, name: 'Ring', is_private: 1, owner: owner.id });
|
||||
await insertRow(await orm(), PackingItems, { trip: trip.id, name: 'Tent' });
|
||||
|
||||
// Both read paths pass the viewer into packing.listItems; that argument is the
|
||||
// ONLY thing filtering the list. If either call site loses it, listItems falls
|
||||
|
||||
Reference in New Issue
Block a user