mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
test(server): seed the demo repository, transports MCP and immich specs through the factories
Converts the raw prepare() fixtures and reads in the demo repository spec, the transports MCP spec and the immich integration suite to the ORM factories and readers. Where a case compared a narrow SELECT with toEqual, it now picks the same columns off the row the reader returns, so the comparison stays exact rather than loosening to a partial match. The immich album link cases seed their photos through addTripPhoto and addAlbumLink, which register the provider asset the same way the raw INSERT OR IGNORE did.
This commit is contained in:
@@ -50,17 +50,28 @@ vi.mock('../../src/utils/ssrfGuard', async () => {
|
||||
});
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
import { resetTestDb, resetRateLimits, setAddonEnabled } from '../helpers/test-db';
|
||||
import type { FactoryOrm } from '../helpers/factories/context';
|
||||
import { createUser } from '../helpers/factories';
|
||||
import { authCookie } from '../helpers/auth';
|
||||
import { findRow, findRows, updateRows } from '../helpers/factories/rows';
|
||||
import { makeTrip } from '../helpers/factories/trips';
|
||||
import { addAlbumLink, addTripPhoto } from '../helpers/factories/photos';
|
||||
import { PhotoProviders } from '../../src/db/entities/PhotoProviders.entity';
|
||||
import { TrekPhotos } from '../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripAlbumLinks } from '../../src/db/entities/TripAlbumLinks.entity';
|
||||
import { TripPhotos } from '../../src/db/entities/TripPhotos.entity';
|
||||
|
||||
let nestApp: INestApplication;
|
||||
let app: Application;
|
||||
let orm: FactoryOrm;
|
||||
|
||||
beforeAll(async () => {
|
||||
nestApp = await buildApp();
|
||||
app = nestApp.getHttpAdapter().getInstance();
|
||||
orm = nestApp.get(MikroORM);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -72,7 +83,7 @@ beforeEach(async () => {
|
||||
// configure it before it's usable in production); the legacy test helper always
|
||||
// seeded it enabled, which is what these tests assume. Same convention
|
||||
// memories-synology.test.ts already uses for its own provider.
|
||||
testDb.prepare("UPDATE photo_providers SET enabled = 1 WHERE id = 'immich'").run();
|
||||
await updateRows(orm, PhotoProviders, { id: 'immich' }, { enabled: 1 });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -141,7 +152,7 @@ describe('Immich authentication', () => {
|
||||
describe('Immich album links', () => {
|
||||
it('IMMICH-020 — POST album-links creates a link', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = testDb.prepare('INSERT INTO trips (user_id, title) VALUES (?, ?) RETURNING *').get(user.id, 'Test Trip') as any;
|
||||
const trip = await makeTrip(orm, user.id, { title: 'Test Trip' });
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/integrations/memories/unified/trips/${trip.id}/album-links`)
|
||||
@@ -151,16 +162,16 @@ describe('Immich album links', () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
|
||||
const link = testDb.prepare('SELECT * FROM trip_album_links WHERE trip_id = ? AND user_id = ?').get(trip.id, user.id) as any;
|
||||
expect(link).toBeDefined();
|
||||
expect(link.album_id).toBe('album-uuid-123');
|
||||
expect(link.album_name).toBe('Vacation 2024');
|
||||
const link = await findRow(orm, TripAlbumLinks, { trip: trip.id, user: user.id });
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.album_id).toBe('album-uuid-123');
|
||||
expect(link?.album_name).toBe('Vacation 2024');
|
||||
});
|
||||
|
||||
it('IMMICH-021 — GET album-links returns linked albums', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = testDb.prepare('INSERT INTO trips (user_id, title) VALUES (?, ?) RETURNING *').get(user.id, 'Test Trip') as any;
|
||||
testDb.prepare('INSERT INTO trip_album_links (trip_id, user_id, album_id, album_name, provider) VALUES (?, ?, ?, ?, ?)').run(trip.id, user.id, 'album-abc', 'My Album', 'immich');
|
||||
const trip = await makeTrip(orm, user.id, { title: 'Test Trip' });
|
||||
await addAlbumLink(orm, trip.id, user.id, 'immich', 'album-abc', 'My Album');
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/api/integrations/memories/unified/trips/${trip.id}/album-links`)
|
||||
@@ -174,23 +185,18 @@ describe('Immich album links', () => {
|
||||
|
||||
it('IMMICH-022 — DELETE album-links removes associated photos but not individually-added ones', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = testDb.prepare('INSERT INTO trips (user_id, title) VALUES (?, ?) RETURNING *').get(user.id, 'Test Trip') as any;
|
||||
const trip = await makeTrip(orm, user.id, { title: 'Test Trip' });
|
||||
|
||||
// Create album link
|
||||
const linkResult = testDb.prepare('INSERT INTO trip_album_links (trip_id, user_id, album_id, album_name, provider) VALUES (?, ?, ?, ?, ?) RETURNING *')
|
||||
.get(trip.id, user.id, 'album-xyz', 'Album XYZ', 'immich') as any;
|
||||
const linkResult = await addAlbumLink(orm, trip.id, user.id, 'immich', 'album-xyz', 'Album XYZ');
|
||||
|
||||
// Insert photos synced from the album
|
||||
for (const assetId of ['asset-001', 'asset-002']) {
|
||||
testDb.prepare('INSERT OR IGNORE INTO trek_photos (provider, asset_id, owner_id) VALUES (?, ?, ?)').run('immich', assetId, user.id);
|
||||
const tkp = testDb.prepare('SELECT id FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?').get('immich', assetId, user.id) as any;
|
||||
testDb.prepare('INSERT INTO trip_photos (trip_id, user_id, photo_id, shared, album_link_id) VALUES (?, ?, ?, 1, ?)').run(trip.id, user.id, tkp.id, linkResult.id);
|
||||
await addTripPhoto(orm, trip.id, user.id, assetId, 'immich', { shared: true, albumLinkId: linkResult.id });
|
||||
}
|
||||
|
||||
// Insert an individually-added photo (no album_link_id)
|
||||
testDb.prepare('INSERT OR IGNORE INTO trek_photos (provider, asset_id, owner_id) VALUES (?, ?, ?)').run('immich', 'asset-manual', user.id);
|
||||
const tkpManual = testDb.prepare('SELECT id FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?').get('immich', 'asset-manual', user.id) as any;
|
||||
testDb.prepare('INSERT INTO trip_photos (trip_id, user_id, photo_id, shared) VALUES (?, ?, ?, 1)').run(trip.id, user.id, tkpManual.id);
|
||||
await addTripPhoto(orm, trip.id, user.id, 'asset-manual', 'immich', { shared: true });
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/api/integrations/memories/unified/trips/${trip.id}/album-links/${linkResult.id}`)
|
||||
@@ -200,29 +206,26 @@ describe('Immich album links', () => {
|
||||
expect(res.body.success).toBe(true);
|
||||
|
||||
// Album-linked photos should be gone
|
||||
const remainingPhotos = testDb.prepare(`
|
||||
SELECT tp.*, tkp.asset_id FROM trip_photos tp
|
||||
JOIN trek_photos tkp ON tkp.id = tp.photo_id
|
||||
WHERE tp.trip_id = ?
|
||||
`).all(trip.id) as any[];
|
||||
const remainingPhotos = [];
|
||||
for (const tp of await findRows(orm, TripPhotos, { trip: trip.id })) {
|
||||
const tkp = await findRow(orm, TrekPhotos, { id: tp.photo_id });
|
||||
if (tkp) remainingPhotos.push({ ...tp, asset_id: tkp.asset_id });
|
||||
}
|
||||
expect(remainingPhotos.length).toBe(1);
|
||||
expect(remainingPhotos[0].asset_id).toBe('asset-manual');
|
||||
|
||||
// Album link itself should be gone
|
||||
const link = testDb.prepare('SELECT * FROM trip_album_links WHERE id = ?').get(linkResult.id);
|
||||
expect(link).toBeUndefined();
|
||||
const link = await findRow(orm, TripAlbumLinks, { id: linkResult.id });
|
||||
expect(link).toBeNull();
|
||||
});
|
||||
|
||||
it('IMMICH-023 — DELETE album-link by non-member returns 404', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: other } = createUser(testDb);
|
||||
const trip = testDb.prepare('INSERT INTO trips (user_id, title) VALUES (?, ?) RETURNING *').get(owner.id, 'Test Trip') as any;
|
||||
const trip = await makeTrip(orm, owner.id, { title: 'Test Trip' });
|
||||
|
||||
const linkResult = testDb.prepare('INSERT INTO trip_album_links (trip_id, user_id, album_id, album_name, provider) VALUES (?, ?, ?, ?, ?) RETURNING *')
|
||||
.get(trip.id, owner.id, 'album-secret', 'Secret Album', 'immich') as any;
|
||||
testDb.prepare('INSERT OR IGNORE INTO trek_photos (provider, asset_id, owner_id) VALUES (?, ?, ?)').run('immich', 'asset-owned', owner.id);
|
||||
const tkpOwned = testDb.prepare('SELECT id FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?').get('immich', 'asset-owned', owner.id) as any;
|
||||
testDb.prepare('INSERT INTO trip_photos (trip_id, user_id, photo_id, shared, album_link_id) VALUES (?, ?, ?, 1, ?)').run(trip.id, owner.id, tkpOwned.id, linkResult.id);
|
||||
const linkResult = await addAlbumLink(orm, trip.id, owner.id, 'immich', 'album-secret', 'Secret Album');
|
||||
await addTripPhoto(orm, trip.id, owner.id, 'asset-owned', 'immich', { shared: true, albumLinkId: linkResult.id });
|
||||
|
||||
// Non-member tries to delete owner's album link — should be denied
|
||||
const res = await request(app)
|
||||
@@ -232,14 +235,11 @@ describe('Immich album links', () => {
|
||||
expect(res.status).toBe(404);
|
||||
|
||||
// Link and photos should still exist
|
||||
const link = testDb.prepare('SELECT * FROM trip_album_links WHERE id = ?').get(linkResult.id);
|
||||
expect(link).toBeDefined();
|
||||
const photo = testDb.prepare(`
|
||||
SELECT tp.* FROM trip_photos tp
|
||||
JOIN trek_photos tkp ON tkp.id = tp.photo_id
|
||||
WHERE tkp.asset_id = ?
|
||||
`).get('asset-owned');
|
||||
expect(photo).toBeDefined();
|
||||
const link = await findRow(orm, TripAlbumLinks, { id: linkResult.id });
|
||||
expect(link).not.toBeNull();
|
||||
const owned = await findRow(orm, TrekPhotos, { asset_id: 'asset-owned' });
|
||||
const photo = owned ? await findRow(orm, TripPhotos, { photo: owned.id }) : null;
|
||||
expect(photo).not.toBeNull();
|
||||
});
|
||||
|
||||
it('IMMICH-024 — DELETE album-link without auth returns 401', async () => {
|
||||
|
||||
@@ -4,6 +4,27 @@ 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';
|
||||
import { findRow, findRows, insertRows, updateRows } from '../../../helpers/factories/rows';
|
||||
import { readAppSetting } from '../../../helpers/factories/settings';
|
||||
import { readUser } from '../../../helpers/factories/users';
|
||||
import { AppSettings } from '../../../../src/db/entities/AppSettings.entity';
|
||||
import { BudgetItems } from '../../../../src/db/entities/BudgetItems.entity';
|
||||
import { DayAssignments } from '../../../../src/db/entities/DayAssignments.entity';
|
||||
import { DayNotes } from '../../../../src/db/entities/DayNotes.entity';
|
||||
import { Days } from '../../../../src/db/entities/Days.entity';
|
||||
import { PackingItems } from '../../../../src/db/entities/PackingItems.entity';
|
||||
import { Places } from '../../../../src/db/entities/Places.entity';
|
||||
import { Reservations } from '../../../../src/db/entities/Reservations.entity';
|
||||
import { TripMembers } from '../../../../src/db/entities/TripMembers.entity';
|
||||
import { Users } from '../../../../src/db/entities/Users.entity';
|
||||
|
||||
/** The given columns of a row, the shape a narrow SELECT returned. */
|
||||
function pick<T extends object, K extends keyof T>(row: T | null, keys: K[]): Pick<T, K> | null {
|
||||
if (!row) return null;
|
||||
const out = {} as Pick<T, K>;
|
||||
for (const key of keys) out[key] = row[key];
|
||||
return out;
|
||||
}
|
||||
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
@@ -25,20 +46,16 @@ describe('DemoRepository', () => {
|
||||
|
||||
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;
|
||||
};
|
||||
const row = pick(await readUser(t, id), ['username', 'email', 'role', 'first_seen_version', 'login_count']);
|
||||
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');
|
||||
expect(await readAppSetting(t, 'allow_registration')).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');
|
||||
expect(await readAppSetting(t, 'allow_registration')).toBe('false');
|
||||
});
|
||||
|
||||
it('DEMOREPO-004: countTripsByUser / listTripIdsByUser — DMS4/DMS5', async () => {
|
||||
@@ -56,7 +73,7 @@ describe('DemoRepository', () => {
|
||||
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);
|
||||
const rows = (await findRows(t, TripMembers, { trip: tripId })).map((r) => pick(r, ['trip_id', 'user_id', 'invited_by']));
|
||||
expect(rows).toEqual([{ trip_id: tripId, user_id: member.id, invited_by: owner.id }]);
|
||||
});
|
||||
|
||||
@@ -64,30 +81,32 @@ describe('DemoRepository', () => {
|
||||
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' });
|
||||
expect(pick(await findRow(t, Days, { id: dayId }), ['trip_id', 'day_number', 'date'])).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 };
|
||||
const placeRow = pick(await findRow(t, Places, { id: placeId }), ['trip_id', 'name', 'category_id']);
|
||||
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 });
|
||||
expect(pick(await findRow(t, DayAssignments, { day: dayId }), ['day_id', 'place_id', 'order_index'])).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);
|
||||
const packing = pick(await findRow(t, PackingItems, { trip: tripId }), ['trip_id', 'name', 'checked', 'category', 'sort_order']);
|
||||
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);
|
||||
const budget = pick(await findRow(t, BudgetItems, { trip: tripId }), ['trip_id', 'category', 'name', 'total_price', 'persons', 'note']);
|
||||
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);
|
||||
const reservation = pick(await findRow(t, Reservations, { trip: tripId }), [
|
||||
'trip_id', 'day_id', 'title', 'reservation_time', 'confirmation_number', 'status', 'type', 'location',
|
||||
]);
|
||||
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);
|
||||
const note = pick(await findRow(t, DayNotes, { day: dayId }), ['day_id', 'trip_id', 'text', 'time', 'icon', 'sort_order']);
|
||||
expect(note).toEqual({ day_id: dayId, trip_id: tripId, text: 'Note text', time: '13:00', icon: 'Info', sort_order: 0.5 });
|
||||
});
|
||||
|
||||
@@ -109,8 +128,9 @@ describe('DemoRepository', () => {
|
||||
|
||||
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);
|
||||
await updateRows(t, Users, { id: user.id }, {
|
||||
maps_api_key: 'maps-key', openweather_api_key: 'ow-key', unsplash_api_key: 'un-key', avatar: 'avatar.png',
|
||||
});
|
||||
|
||||
const before = await demo.getAdminCredentials('admin@nomad.app');
|
||||
expect(before).toEqual({
|
||||
@@ -122,28 +142,31 @@ describe('DemoRepository', () => {
|
||||
});
|
||||
|
||||
// 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 updateRows(t, Users, { id: user.id }, {
|
||||
password_hash: 'stale-hash-from-baseline', maps_api_key: null, openweather_api_key: null, unsplash_api_key: null, avatar: null,
|
||||
});
|
||||
|
||||
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);
|
||||
const after = pick(await readUser(t, user.id), ['password_hash', 'maps_api_key', 'openweather_api_key', 'unsplash_api_key', 'avatar']);
|
||||
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 };
|
||||
const before = await readUser(t, user.id);
|
||||
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 };
|
||||
const after = await readUser(t, user.id);
|
||||
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();
|
||||
await insertRows(t, AppSettings, [
|
||||
{ key: 'maps_api_key', value: 'a-maps-key' },
|
||||
{ key: 'unsplash_api_key', value: null },
|
||||
]);
|
||||
|
||||
const rows = await demo.getInstanceApiKeys();
|
||||
expect(rows.sort((a, b) => a.key.localeCompare(b.key))).toEqual([
|
||||
@@ -152,10 +175,11 @@ describe('DemoRepository', () => {
|
||||
]);
|
||||
|
||||
// Clobber, then restore from what was read.
|
||||
testDb.prepare("UPDATE app_settings SET value = 'clobbered' WHERE key = 'maps_api_key'").run();
|
||||
await updateRows(t, AppSettings, { key: 'maps_api_key' }, { value: 'clobbered' });
|
||||
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();
|
||||
const after = (await findRows(t, AppSettings, { key: { $in: ['maps_api_key', 'unsplash_api_key'] } }, { key: 'asc' }))
|
||||
.map((r) => pick(r, ['key', 'value']));
|
||||
expect(after).toEqual([
|
||||
{ key: 'maps_api_key', value: 'a-maps-key' },
|
||||
{ key: 'unsplash_api_key', value: null },
|
||||
@@ -164,7 +188,6 @@ describe('DemoRepository', () => {
|
||||
|
||||
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');
|
||||
expect(await readAppSetting(t, 'maps_api_key')).toBe('brand-new');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,18 @@ vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, createDay } from '../../helpers/factories';
|
||||
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { countRows, findRow, findRows, insertRow, updateRows } from '../../helpers/factories/rows';
|
||||
import { makeDay } from '../../helpers/factories/trips';
|
||||
import { makeBudgetItem } from '../../helpers/factories/budget';
|
||||
import { BudgetItems } from '../../../src/db/entities/BudgetItems.entity';
|
||||
import { ReservationEndpoints } from '../../../src/db/entities/ReservationEndpoints.entity';
|
||||
import { Reservations } from '../../../src/db/entities/Reservations.entity';
|
||||
|
||||
/** Overwrites the booking's stored metadata text, the way an import or a legacy row leaves it. */
|
||||
async function setMetadata(metadata: string, reservationId: number): Promise<void> {
|
||||
await updateRows(orm, Reservations, { id: reservationId }, { metadata });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetTestDb(testDb);
|
||||
@@ -31,7 +43,14 @@ beforeEach(() => {
|
||||
delete process.env.DEMO_MODE;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
let orm: TestOrm;
|
||||
|
||||
beforeAll(async () => {
|
||||
orm = await createTestOrm(testDb);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await orm.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
@@ -53,9 +72,15 @@ const stopoverEndpoints = [
|
||||
];
|
||||
|
||||
const errorText = (result: unknown) => (result as { content: { text: string }[] }).content[0].text;
|
||||
const storedMetadata = (reservationId: number) => JSON.parse(
|
||||
(testDb.prepare('SELECT metadata FROM reservations WHERE id = ?').get(reservationId) as { metadata: string }).metadata
|
||||
);
|
||||
const storedMetadata = async (reservationId: number) =>
|
||||
JSON.parse(String((await findRow(orm, Reservations, { id: reservationId }))?.metadata));
|
||||
|
||||
/** The ids of the booking's stored endpoints, in sequence order. */
|
||||
const endpointIds = async (reservationId: number) =>
|
||||
(await findRows(orm, ReservationEndpoints, { reservation: reservationId }, { sequence: 'asc' })).map((r) => r.id);
|
||||
|
||||
/** A second trip's day, which the tools must refuse as a foreign day. */
|
||||
const foreignDayOn = async (tripId: number, date: string) => (await makeDay(orm, tripId, { date, day_number: 1 })).id;
|
||||
|
||||
describe('Tool: create_transport', () => {
|
||||
it('backfills lat/lng/timezone for code-only flight endpoints', async () => {
|
||||
@@ -74,7 +99,7 @@ describe('Tool: create_transport', () => {
|
||||
expect(typeof from.lng).toBe('number');
|
||||
expect(from.timezone).toBe('Europe/Zurich');
|
||||
// persisted NOT NULL columns are populated
|
||||
const rows = testDb.prepare('SELECT lat, lng FROM reservation_endpoints WHERE reservation_id = ?').all(data.reservation.id) as any[];
|
||||
const rows = await findRows(orm, ReservationEndpoints, { reservation: data.reservation.id });
|
||||
expect(rows.every(r => r.lat != null && r.lng != null)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -250,7 +275,7 @@ describe('Transport tools: access and validation', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const otherTrip = createTrip(testDb, user.id);
|
||||
const foreignDay = Number(testDb.prepare("INSERT INTO days (trip_id, date, day_number) VALUES (?, '2026-07-01', 1)").run(otherTrip.id).lastInsertRowid);
|
||||
const foreignDay = await foreignDayOn(otherTrip.id, '2026-07-01');
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'create_transport',
|
||||
@@ -265,7 +290,7 @@ describe('Transport tools: access and validation', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const otherTrip = createTrip(testDb, user.id);
|
||||
const foreignDay = Number(testDb.prepare("INSERT INTO days (trip_id, date, day_number) VALUES (?, '2026-07-02', 1)").run(otherTrip.id).lastInsertRowid);
|
||||
const foreignDay = await foreignDayOn(otherTrip.id, '2026-07-02');
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'create_transport',
|
||||
@@ -288,7 +313,7 @@ describe('Transport tools: the price link', () => {
|
||||
});
|
||||
const data = parseToolResult(result) as { reservation: { id: number } };
|
||||
|
||||
const item = testDb.prepare('SELECT name, category, total_price FROM budget_items WHERE reservation_id = ?').get(data.reservation.id) as { name: string; category: string; total_price: number };
|
||||
const item = await findRow(orm, BudgetItems, { reservation: data.reservation.id });
|
||||
expect(item).toMatchObject({ name: 'ZRH → CDG', category: 'Flights', total_price: 240 });
|
||||
// The price also rides along in the reservation metadata, as the REST path does.
|
||||
expect(broadcastMock.mock.calls.some(c => c[1] === 'budget:created')).toBe(true);
|
||||
@@ -304,8 +329,8 @@ describe('Transport tools: the price link', () => {
|
||||
arguments: { tripId: trip.id, type: 'train', title: 'ICE 599', price: 89 },
|
||||
});
|
||||
const data = parseToolResult(result) as { reservation: { id: number } };
|
||||
const item = testDb.prepare('SELECT category FROM budget_items WHERE reservation_id = ?').get(data.reservation.id) as { category: string };
|
||||
expect(item.category).toBe('train');
|
||||
const item = await findRow(orm, BudgetItems, { reservation: data.reservation.id });
|
||||
expect(item?.category).toBe('train');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,7 +343,7 @@ describe('Transport tools: the price link', () => {
|
||||
arguments: { tripId: trip.id, type: 'car', title: 'Rental', price: 0 },
|
||||
});
|
||||
const data = parseToolResult(result) as { reservation: { id: number } };
|
||||
expect(testDb.prepare('SELECT id FROM budget_items WHERE reservation_id = ?').get(data.reservation.id)).toBeUndefined();
|
||||
expect(await findRow(orm, BudgetItems, { reservation: data.reservation.id })).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -360,7 +385,7 @@ describe('Tool: update_transport (guards)', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const otherTrip = createTrip(testDb, user.id);
|
||||
const foreignDay = Number(testDb.prepare("INSERT INTO days (trip_id, date, day_number) VALUES (?, '2026-08-01', 1)").run(otherTrip.id).lastInsertRowid);
|
||||
const foreignDay = await foreignDayOn(otherTrip.id, '2026-08-01');
|
||||
await withHarness(user.id, async (h) => {
|
||||
const created = await h.client.callTool({
|
||||
name: 'create_transport',
|
||||
@@ -418,7 +443,7 @@ describe('Tool: delete_transport', () => {
|
||||
arguments: { tripId: trip.id, reservationId: reservation.id },
|
||||
});
|
||||
expect(parseToolResult(result)).toEqual({ success: true });
|
||||
expect(testDb.prepare('SELECT id FROM reservations WHERE id = ?').get(reservation.id)).toBeUndefined();
|
||||
expect(await findRow(orm, Reservations, { id: reservation.id })).toBeNull();
|
||||
expect(broadcastMock.mock.calls.some(c => c[1] === 'reservation:deleted')).toBe(true);
|
||||
expect(broadcastMock.mock.calls.some(c => c[1] === 'budget:deleted')).toBe(false);
|
||||
});
|
||||
@@ -433,11 +458,10 @@ describe('Tool: delete_transport', () => {
|
||||
arguments: { tripId: trip.id, type: 'flight', title: 'ZRH → CDG', endpoints: flightEndpoints },
|
||||
});
|
||||
const { reservation } = parseToolResult(created) as { reservation: { id: number } };
|
||||
const expense = (name: string) => Number(testDb.prepare(
|
||||
'INSERT INTO budget_items (trip_id, name, category, total_price, reservation_id) VALUES (?, ?, ?, ?, ?)',
|
||||
).run(trip.id, name, 'flights', 50, reservation.id).lastInsertRowid);
|
||||
const fare = expense('Fare');
|
||||
const seat = expense('Seat');
|
||||
const expense = async (name: string) =>
|
||||
(await makeBudgetItem(orm, trip.id, { name, category: 'flights', total_price: 50, reservation: reservation.id })).id;
|
||||
const fare = await expense('Fare');
|
||||
const seat = await expense('Seat');
|
||||
broadcastMock.mockClear();
|
||||
|
||||
const result = await h.client.callTool({
|
||||
@@ -445,7 +469,7 @@ describe('Tool: delete_transport', () => {
|
||||
arguments: { tripId: trip.id, reservationId: reservation.id },
|
||||
});
|
||||
expect(parseToolResult(result)).toEqual({ success: true });
|
||||
expect(testDb.prepare('SELECT COUNT(*) AS n FROM budget_items WHERE trip_id = ?').get(trip.id)).toEqual({ n: 0 });
|
||||
expect(await countRows(orm, BudgetItems, { trip: trip.id })).toBe(0);
|
||||
// One event per expense, and all of them before the transport itself goes.
|
||||
expect(broadcastMock.mock.calls.map(c => c[1])).toEqual(['budget:deleted', 'budget:deleted', 'reservation:deleted']);
|
||||
expect(broadcastMock.mock.calls.slice(0, 2).map(c => (c[2] as { itemId: number }).itemId)).toEqual([fare, seat]);
|
||||
@@ -507,7 +531,7 @@ describe('Tool: create_transport (multi-leg)', () => {
|
||||
},
|
||||
});
|
||||
const data = parseToolResult(result) as any;
|
||||
expect(storedMetadata(data.reservation.id).legs).toEqual([
|
||||
expect((await storedMetadata(data.reservation.id)).legs).toEqual([
|
||||
{ from: 'AMS', to: 'CDG', dep_day_id: day.id, dep_time: '09:00', arr_day_id: null, arr_time: '10:00' },
|
||||
{ from: 'CDG', to: 'FCO', dep_day_id: null, dep_time: '11:00', arr_day_id: null, arr_time: '12:00' },
|
||||
]);
|
||||
@@ -537,7 +561,7 @@ describe('Tool: create_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
})) as any;
|
||||
const meta = storedMetadata(data.reservation.id);
|
||||
const meta = await storedMetadata(data.reservation.id);
|
||||
expect(meta.departure_airport).toBe('AMS');
|
||||
expect(meta.arrival_airport).toBe('FCO');
|
||||
expect(meta.airline).toBe('KLM');
|
||||
@@ -561,7 +585,7 @@ describe('Tool: create_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
})) as any;
|
||||
const meta = storedMetadata(data.reservation.id);
|
||||
const meta = await storedMetadata(data.reservation.id);
|
||||
expect(meta.legs[0].confirmation_number).toBe('ABC123');
|
||||
// A leg without one gets no key at all, rather than an undefined that
|
||||
// serialises away and back as null.
|
||||
@@ -589,7 +613,7 @@ describe('Tool: create_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
})) as any;
|
||||
const meta = storedMetadata(data.reservation.id);
|
||||
const meta = await storedMetadata(data.reservation.id);
|
||||
expect(meta.airline).toBe('KLM Cityhopper');
|
||||
expect(meta.legs[0].airline).toBe('KLM');
|
||||
});
|
||||
@@ -607,7 +631,7 @@ describe('Tool: create_transport (multi-leg)', () => {
|
||||
legs: [{ dep_time: '09:00', arr_time: '10:00' }, { dep_time: '11:00', arr_time: '12:00' }],
|
||||
},
|
||||
})) as any;
|
||||
const meta = storedMetadata(data.reservation.id);
|
||||
const meta = await storedMetadata(data.reservation.id);
|
||||
expect(meta.legs.map((l: any) => [l.from, l.to])).toEqual([['AMS', 'CDG'], ['CDG', 'FCO']]);
|
||||
});
|
||||
});
|
||||
@@ -631,7 +655,7 @@ describe('Tool: create_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
})) as any;
|
||||
const meta = storedMetadata(data.reservation.id);
|
||||
const meta = await storedMetadata(data.reservation.id);
|
||||
// Station endpoints carry no code, so the labels come from their names.
|
||||
expect(meta.legs.map((l: any) => [l.from, l.to])).toEqual([
|
||||
['Basel SBB', 'Lugano'], ['Lugano', 'Milano Centrale'],
|
||||
@@ -846,7 +870,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
await withHarness(user.id, async (h) => {
|
||||
const created = await seedStopover(h, trip.id);
|
||||
// What an AirTrail import leaves behind: sync ids plus per-leg planner positions.
|
||||
testDb.prepare('UPDATE reservations SET metadata = ? WHERE id = ?').run(JSON.stringify({
|
||||
await setMetadata(JSON.stringify({
|
||||
departure_airport: 'AMS', arrival_airport: 'FCO', airtrail_ids: ['17', '18'],
|
||||
legs: [
|
||||
{ from: 'AMS', to: 'CDG', dep_time: '09:00', arr_time: '10:00', day_positions: { 5: 2 } },
|
||||
@@ -864,7 +888,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const meta = storedMetadata((parseToolResult(result) as any).reservation.id);
|
||||
const meta = await storedMetadata((parseToolResult(result) as any).reservation.id);
|
||||
expect(meta.airtrail_ids).toEqual(['17', '18']);
|
||||
expect(meta.departure_airport).toBe('AMS');
|
||||
expect(meta.legs[0].arr_time).toBe('10:15');
|
||||
@@ -888,7 +912,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const meta = storedMetadata((parseToolResult(result) as any).reservation.id);
|
||||
const meta = await storedMetadata((parseToolResult(result) as any).reservation.id);
|
||||
// The copy-through list in applyLegs is what carries the field: a field
|
||||
// missing from it is accepted by the schema and then silently dropped.
|
||||
expect(meta.legs.map((l: any) => l.confirmation_number)).toEqual(['ABC123', 'XYZ789']);
|
||||
@@ -912,7 +936,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const meta = storedMetadata((parseToolResult(result) as any).reservation.id);
|
||||
const meta = await storedMetadata((parseToolResult(result) as any).reservation.id);
|
||||
expect(meta.confirmation_source).toBe('email');
|
||||
expect(meta.legs).toHaveLength(2);
|
||||
// departure_airport is mirrored back in because the supplied metadata dropped it.
|
||||
@@ -925,12 +949,12 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
const trip = createTrip(testDb, user.id);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const created = await seedStopover(h, trip.id);
|
||||
const before = storedMetadata(created.reservation.id);
|
||||
const before = await storedMetadata(created.reservation.id);
|
||||
await h.client.callTool({
|
||||
name: 'update_transport',
|
||||
arguments: { tripId: trip.id, reservationId: created.reservation.id, status: 'confirmed' },
|
||||
});
|
||||
expect(storedMetadata(created.reservation.id)).toEqual(before);
|
||||
expect(await storedMetadata(created.reservation.id)).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -959,8 +983,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
const trip = createTrip(testDb, user.id);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const created = await seedStopover(h, trip.id);
|
||||
const idsBefore = (testDb.prepare('SELECT id FROM reservation_endpoints WHERE reservation_id = ? ORDER BY sequence')
|
||||
.all(created.reservation.id) as any[]).map(r => r.id);
|
||||
const idsBefore = await endpointIds(created.reservation.id);
|
||||
await h.client.callTool({
|
||||
name: 'update_transport',
|
||||
arguments: {
|
||||
@@ -971,8 +994,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const idsAfter = (testDb.prepare('SELECT id FROM reservation_endpoints WHERE reservation_id = ? ORDER BY sequence')
|
||||
.all(created.reservation.id) as any[]).map(r => r.id);
|
||||
const idsAfter = await endpointIds(created.reservation.id);
|
||||
expect(idsAfter).toEqual(idsBefore);
|
||||
});
|
||||
});
|
||||
@@ -1005,7 +1027,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
const trip = createTrip(testDb, user.id);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const created = await seedStopover(h, trip.id);
|
||||
testDb.prepare('UPDATE reservations SET metadata = ? WHERE id = ?').run('not json at all', created.reservation.id);
|
||||
await setMetadata('not json at all', created.reservation.id);
|
||||
const result = await h.client.callTool({
|
||||
name: 'update_transport',
|
||||
arguments: {
|
||||
@@ -1017,7 +1039,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
},
|
||||
});
|
||||
expect((result as { isError?: boolean }).isError).toBeFalsy();
|
||||
expect(storedMetadata((parseToolResult(result) as any).reservation.id).legs).toHaveLength(2);
|
||||
expect((await storedMetadata((parseToolResult(result) as any).reservation.id)).legs).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1026,7 +1048,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
const trip = createTrip(testDb, user.id);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const created = await seedStopover(h, trip.id);
|
||||
testDb.prepare('UPDATE reservations SET metadata = ? WHERE id = ?').run(JSON.stringify(JSON.stringify({
|
||||
await setMetadata(JSON.stringify(JSON.stringify({
|
||||
legs: [{ from: 'AMS', to: 'CDG', day_positions: { 9: 1 } }, { from: 'CDG', to: 'FCO' }],
|
||||
})), created.reservation.id);
|
||||
const result = await h.client.callTool({
|
||||
@@ -1039,7 +1061,7 @@ describe('Tool: update_transport (multi-leg)', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(storedMetadata((parseToolResult(result) as any).reservation.id).legs[0].day_positions).toEqual({ 9: 1 });
|
||||
expect((await storedMetadata((parseToolResult(result) as any).reservation.id)).legs[0].day_positions).toEqual({ 9: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1110,8 +1132,7 @@ describe('Transport tools: the full type list', () => {
|
||||
expect((result as { isError?: boolean }).isError).toBeFalsy();
|
||||
const data = parseToolResult(result) as any;
|
||||
expect(data.reservation.type).toBe(type);
|
||||
const row = testDb.prepare('SELECT type FROM reservations WHERE id = ?').get(data.reservation.id) as any;
|
||||
expect(row.type).toBe(type);
|
||||
expect((await findRow(orm, Reservations, { id: data.reservation.id }))?.type).toBe(type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1138,9 +1159,9 @@ describe('Transport tools: the full type list', () => {
|
||||
expect(data.reservation.type).toBe('bus');
|
||||
expect(data.reservation.confirmation_number).toBe('FLIX-8891');
|
||||
expect(data.reservation.endpoints).toHaveLength(2);
|
||||
const row = testDb.prepare('SELECT day_id, end_day_id FROM reservations WHERE id = ?').get(data.reservation.id) as any;
|
||||
expect(row.day_id).toBe(depDay.id);
|
||||
expect(row.end_day_id).toBe(arrDay.id);
|
||||
const row = (await findRow(orm, Reservations, { id: data.reservation.id }));
|
||||
expect(row?.day_id).toBe(depDay.id);
|
||||
expect(row?.end_day_id).toBe(arrDay.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1175,8 +1196,7 @@ describe('Transport tools: the full type list', () => {
|
||||
arguments: { tripId: trip.id, reservationId: reservation.id, type },
|
||||
});
|
||||
expect((result as { isError?: boolean }).isError).toBeFalsy();
|
||||
const row = testDb.prepare('SELECT type FROM reservations WHERE id = ?').get(reservation.id) as any;
|
||||
expect(row.type).toBe(type);
|
||||
expect((await findRow(orm, Reservations, { id: reservation.id }))?.type).toBe(type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1195,8 +1215,7 @@ describe('Transport tools: the full type list', () => {
|
||||
arguments: { tripId: trip.id, reservationId: reservation.id, status: 'confirmed' },
|
||||
});
|
||||
expect((result as { isError?: boolean }).isError).toBeFalsy();
|
||||
const row = testDb.prepare('SELECT status FROM reservations WHERE id = ?').get(reservation.id) as any;
|
||||
expect(row.status).toBe('confirmed');
|
||||
expect((await findRow(orm, Reservations, { id: reservation.id }))?.status).toBe('confirmed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1228,16 +1247,14 @@ describe('Transport tools: the full type list', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
// As create_transit_journey leaves it: a transport row carrying an itinerary.
|
||||
const reservationId = Number(testDb.prepare(
|
||||
"INSERT INTO reservations (trip_id, title, type, status) VALUES (?, 'Tram 4', 'transit', 'pending')"
|
||||
).run(trip.id).lastInsertRowid);
|
||||
const reservationId = await insertRow(orm, Reservations, { trip: trip.id, title: 'Tram 4', type: 'transit', status: 'pending' });
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'update_transport',
|
||||
arguments: { tripId: trip.id, reservationId, status: 'confirmed' },
|
||||
});
|
||||
expect((result as { isError?: boolean }).isError).toBeFalsy();
|
||||
const row = testDb.prepare('SELECT status, type FROM reservations WHERE id = ?').get(reservationId) as any;
|
||||
const row = (await findRow(orm, Reservations, { id: reservationId }));
|
||||
expect(row).toMatchObject({ status: 'confirmed', type: 'transit' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user