Files
TREK/server/tests/unit/db/repositories/DemoRepository.test.ts
T

155 lines
9.4 KiB
TypeScript

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 { createUser, createAdmin } from '../../../helpers/factories';
import { DemoRepository, type NewDemoPlaceRow } from '../../../../src/db/repositories/DemoRepository';
const testDb = createSnapshotTestDb();
let t: TestOrm;
let demo: DemoRepository;
beforeAll(async () => {
t = await createTestOrm(testDb);
demo = new DemoRepository(t.em);
});
beforeEach(() => { resetTestDb(testDb); t.clear(); });
afterAll(async () => { await t.close(); testDb.close(); });
describe('DemoRepository', () => {
it('DEMOREPO-001: findUserByEmail — DMS1, case-sensitive, no guest filter', async () => {
const { user } = createUser(testDb, { email: 'admin@trek.app' });
await expect(demo.findUserByEmail('admin@trek.app')).resolves.toEqual({ id: user.id });
await expect(demo.findUserByEmail('nope@trek.app')).resolves.toBeNull();
});
it('DEMOREPO-002: createUser — DMS2, leaves first_seen_version/login_count/oidc/avatar to the entity default', async () => {
const id = await demo.createUser({ username: 'demo', email: 'demo@trek.app', password_hash: 'x', role: 'user' });
const row = testDb.prepare('SELECT username, email, role, first_seen_version, login_count FROM users WHERE id = ?').get(id) as {
username: string; email: string; role: string; first_seen_version: string; login_count: number;
};
expect(row).toEqual({ username: 'demo', email: 'demo@trek.app', role: 'user', first_seen_version: '0.0.0', login_count: 0 });
});
it('DEMOREPO-003: setAllowRegistrationFalse — DMS3', async () => {
await demo.setAllowRegistrationFalse();
const row = testDb.prepare("SELECT value FROM app_settings WHERE key = 'allow_registration'").get() as { value: string };
expect(row.value).toBe('false');
// Idempotent — a second call still lands 'false' (INSERT OR REPLACE dialect).
await demo.setAllowRegistrationFalse();
const row2 = testDb.prepare("SELECT value FROM app_settings WHERE key = 'allow_registration'").get() as { value: string };
expect(row2.value).toBe('false');
});
it('DEMOREPO-004: countTripsByUser / listTripIdsByUser — DMS4/DMS5', async () => {
const { user } = createAdmin(testDb);
expect(await demo.countTripsByUser(user.id)).toBe(0);
const t1 = await demo.insertTrip(user.id, 'A', 'desc', '2026-01-01', '2026-01-02', 'EUR');
const t2 = await demo.insertTrip(user.id, 'B', 'desc', '2026-02-01', '2026-02-02', 'EUR');
expect(await demo.countTripsByUser(user.id)).toBe(2);
expect((await demo.listTripIdsByUser(user.id)).sort()).toEqual([t1, t2].sort());
});
it('DEMOREPO-005: addTripMember — DMS6, INSERT OR IGNORE (a repeat call does not throw)', async () => {
const { user: owner } = createAdmin(testDb);
const { user: member } = createUser(testDb);
const tripId = await demo.insertTrip(owner.id, 'A', 'desc', '2026-01-01', '2026-01-02', 'EUR');
await demo.addTripMember(tripId, member.id, owner.id);
await demo.addTripMember(tripId, member.id, owner.id); // no UNIQUE-constraint throw
const rows = testDb.prepare('SELECT trip_id, user_id, invited_by FROM trip_members WHERE trip_id = ?').all(tripId);
expect(rows).toEqual([{ trip_id: tripId, user_id: member.id, invited_by: owner.id }]);
});
it('DEMOREPO-006: insertDay/insertPlace/insertDayAssignment/insertPackingItem/insertBudgetItem/insertReservation/insertDayNote — DMS7-14', async () => {
const { user } = createAdmin(testDb);
const tripId = await demo.insertTrip(user.id, 'Test Trip', 'd', '2026-04-15', '2026-04-16', 'JPY');
const dayId = await demo.insertDay(tripId, 1, '2026-04-15');
expect(testDb.prepare('SELECT trip_id, day_number, date FROM days WHERE id = ?').get(dayId)).toEqual({ trip_id: tripId, day_number: 1, date: '2026-04-15' });
const place: NewDemoPlaceRow = [tripId, 'Hotel X', 35.1, 139.1, 'Addr', 1, '15:00', 60, 'notes', null, 'gpid', null, null];
const placeId = await demo.insertPlace(place);
const placeRow = testDb.prepare('SELECT trip_id, name, category_id FROM places WHERE id = ?').get(placeId) as { trip_id: number; name: string; category_id: number };
expect(placeRow).toEqual({ trip_id: tripId, name: 'Hotel X', category_id: 1 });
await demo.insertDayAssignment(dayId, placeId, 0);
expect(testDb.prepare('SELECT day_id, place_id, order_index FROM day_assignments WHERE day_id = ?').get(dayId)).toEqual({ day_id: dayId, place_id: placeId, order_index: 0 });
await demo.insertPackingItem(tripId, 'Passport', 1, 'Documents', 0);
const packing = testDb.prepare('SELECT trip_id, name, checked, category, sort_order FROM packing_items WHERE trip_id = ?').get(tripId);
expect(packing).toEqual({ trip_id: tripId, name: 'Passport', checked: 1, category: 'Documents', sort_order: 0 });
await demo.insertBudgetItem(tripId, 'Food', 'Lunch', 1000, 2, null);
const budget = testDb.prepare('SELECT trip_id, category, name, total_price, persons, note FROM budget_items WHERE trip_id = ?').get(tripId);
expect(budget).toEqual({ trip_id: tripId, category: 'Food', name: 'Lunch', total_price: 1000, persons: 2, note: null });
await demo.insertReservation(tripId, dayId, 'Check-in', '2026-04-15T15:00', 'CONF-1', 'confirmed', 'hotel', 'Tokyo');
const reservation = testDb.prepare('SELECT trip_id, day_id, title, reservation_time, confirmation_number, status, type, location FROM reservations WHERE trip_id = ?').get(tripId);
expect(reservation).toEqual({ trip_id: tripId, day_id: dayId, title: 'Check-in', reservation_time: '2026-04-15T15:00', confirmation_number: 'CONF-1', status: 'confirmed', type: 'hotel', location: 'Tokyo' });
await demo.insertDayNote(dayId, tripId, 'Note text', '13:00', 'Info', 0.5);
const note = testDb.prepare('SELECT day_id, trip_id, text, time, icon, sort_order FROM day_notes WHERE day_id = ?').get(dayId);
expect(note).toEqual({ day_id: dayId, trip_id: tripId, text: 'Note text', time: '13:00', icon: 'Info', sort_order: 0.5 });
});
it('DEMOREPO-CRED-001 (DMR1/DMR5 round trip): getAdminCredentials reads exactly what restoreAdminCredentials wrote', async () => {
const { user } = createAdmin(testDb, { email: 'admin@nomad.app' });
testDb.prepare('UPDATE users SET maps_api_key = ?, openweather_api_key = ?, unsplash_api_key = ?, avatar = ? WHERE id = ?')
.run('maps-key', 'ow-key', 'un-key', 'avatar.png', user.id);
const before = await demo.getAdminCredentials('admin@nomad.app');
expect(before).toEqual({
password_hash: expect.any(String),
maps_api_key: 'maps-key',
openweather_api_key: 'ow-key',
unsplash_api_key: 'un-key',
avatar: 'avatar.png',
});
// Simulate a baseline restore clobbering the row, then restore the preserved values.
testDb.prepare('UPDATE users SET password_hash = ?, maps_api_key = NULL, openweather_api_key = NULL, unsplash_api_key = NULL, avatar = NULL WHERE id = ?')
.run('stale-hash-from-baseline', user.id);
await demo.restoreAdminCredentials('admin@nomad.app', before!);
const after = testDb.prepare('SELECT password_hash, maps_api_key, openweather_api_key, unsplash_api_key, avatar FROM users WHERE id = ?').get(user.id);
expect(after).toEqual(before);
});
it('DEMOREPO-CRED-002 (DMR5): writes exactly the 5 named columns, no updated_at bump', async () => {
const { user } = createAdmin(testDb, { email: 'admin@nomad.app' });
const before = testDb.prepare('SELECT updated_at FROM users WHERE id = ?').get(user.id) as { updated_at: string | null };
await demo.restoreAdminCredentials('admin@nomad.app', {
password_hash: 'h', maps_api_key: 'm', openweather_api_key: 'o', unsplash_api_key: 'u', avatar: 'a',
});
const after = testDb.prepare('SELECT updated_at FROM users WHERE id = ?').get(user.id) as { updated_at: string | null };
expect(after.updated_at).toBe(before.updated_at);
});
it('DEMOREPO-KEYS-001 (DMR2/DMR6 round trip): getInstanceApiKeys preserves a NULL value, restoreInstanceApiKeys writes it back as NULL', async () => {
testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('maps_api_key', 'a-maps-key')").run();
testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('unsplash_api_key', NULL)").run();
const rows = await demo.getInstanceApiKeys();
expect(rows.sort((a, b) => a.key.localeCompare(b.key))).toEqual([
{ key: 'maps_api_key', value: 'a-maps-key' },
{ key: 'unsplash_api_key', value: null },
]);
// Clobber, then restore from what was read.
testDb.prepare("UPDATE app_settings SET value = 'clobbered' WHERE key = 'maps_api_key'").run();
await demo.restoreInstanceApiKeys(rows);
const after = testDb.prepare("SELECT key, value FROM app_settings WHERE key IN ('maps_api_key', 'unsplash_api_key') ORDER BY key").all();
expect(after).toEqual([
{ key: 'maps_api_key', value: 'a-maps-key' },
{ key: 'unsplash_api_key', value: null },
]);
});
it('DEMOREPO-KEYS-002: restoreInstanceApiKeys upserts a row that did not previously exist', async () => {
await demo.restoreInstanceApiKeys([{ key: 'maps_api_key', value: 'brand-new' }]);
const row = testDb.prepare("SELECT value FROM app_settings WHERE key = 'maps_api_key'").get() as { value: string };
expect(row.value).toBe('brand-new');
});
});