mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
530c7cb005
Fold the legacy tripService SQL into TripsService; port the trips MCP registrar/resources/trip-summary prompt to trips.mcp.ts and the share-link tools to share.mcp.ts (predicate access for the trips:delete/trips:share/canReadTrips gates, deprecation notice via the attach ctx); adopt trips.dto.ts and clear the seven allow-list entries; delete todo/share bridges with their last consumers. Includes the trailing quirk fixes: transactional deleteTrip/deleteGuest, owner display_name in listMembers.
418 lines
21 KiB
TypeScript
418 lines
21 KiB
TypeScript
/**
|
||
* Unit tests for the DI-native ShareService — SHARE-SVC-001 through
|
||
* SHARE-SVC-025 (the SHARE-001..026 range belongs to the HTTP parity suite in
|
||
* tests/integration/share.test.ts; these pin the service SQL directly; the
|
||
* 026–028 bridge-delegation cases died with share.bridge when the legacy
|
||
* share-link tools moved to share.mcp.ts). Uses a real in-memory SQLite DB so
|
||
* SQL logic is exercised faithfully.
|
||
*/
|
||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||
|
||
// ── DB setup ──────────────────────────────────────────────────────────────────
|
||
|
||
const { testDb, dbMock } = vi.hoisted(() => {
|
||
const Database = require('better-sqlite3');
|
||
const db = new Database(':memory:');
|
||
db.exec('PRAGMA journal_mode = WAL');
|
||
db.exec('PRAGMA foreign_keys = ON');
|
||
db.exec('PRAGMA busy_timeout = 5000');
|
||
const mock = {
|
||
db,
|
||
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 { testDb: db, dbMock: mock };
|
||
});
|
||
|
||
vi.mock('../../../src/db/database', () => dbMock);
|
||
vi.mock('../../../src/config', () => ({
|
||
JWT_SECRET: 'test-secret',
|
||
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
||
updateJwtSecret: () => {},
|
||
}));
|
||
|
||
const checkPermission = vi.fn();
|
||
const permissionsStub = { checkPermission } as unknown as PermissionsService;
|
||
|
||
const { serveFilePath } = vi.hoisted(() => ({ serveFilePath: vi.fn() }));
|
||
vi.mock('../../../src/services/placePhotoCache', () => ({ serveFilePath }));
|
||
|
||
import { createTables } from '../../../src/db/schema';
|
||
import { runMigrations } from '../../../src/db/migrations';
|
||
import { resetTestDb } from '../../helpers/test-db';
|
||
import {
|
||
createUser, createTrip, addTripMember, createDay, createPlace, createDayAssignment,
|
||
} from '../../helpers/factories';
|
||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||
import type { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
||
import { ShareService } from '../../../src/nest/share/share.service';
|
||
import { SettingsService } from '../../../src/nest/settings/settings.service';
|
||
import type { User } from '../../../src/types';
|
||
|
||
const svc = new ShareService(new DatabaseService(testDb), new SettingsService(new DatabaseService(testDb)), permissionsStub);
|
||
|
||
beforeAll(() => {
|
||
createTables(testDb);
|
||
runMigrations(testDb);
|
||
});
|
||
|
||
beforeEach(() => {
|
||
resetTestDb(testDb);
|
||
checkPermission.mockReset();
|
||
serveFilePath.mockReset();
|
||
});
|
||
|
||
afterAll(() => {
|
||
testDb.close();
|
||
});
|
||
|
||
function shareRow(tripId: number | string) {
|
||
return testDb.prepare('SELECT * FROM share_tokens WHERE trip_id = ?').get(tripId) as any;
|
||
}
|
||
|
||
/** Owner + trip + a share link with all five flags on unless overridden. */
|
||
function seedSharedTrip(flags: Record<string, boolean> = {}) {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
const { token } = svc.createOrUpdate(String(trip.id), user.id, {
|
||
share_map: true, share_bookings: true, share_packing: true, share_budget: true, share_collab: true,
|
||
...flags,
|
||
});
|
||
return { user, trip, token };
|
||
}
|
||
|
||
// ── verifyTripAccess / canManage ──────────────────────────────────────────────
|
||
|
||
describe('verifyTripAccess and canManage', () => {
|
||
it('SHARE-SVC-001: verifyTripAccess returns the trip for owner and member, nothing for a stranger', () => {
|
||
const { user: owner } = createUser(testDb);
|
||
const { user: member } = createUser(testDb);
|
||
const { user: stranger } = createUser(testDb);
|
||
const trip = createTrip(testDb, owner.id);
|
||
addTripMember(testDb, trip.id, member.id);
|
||
expect(svc.verifyTripAccess(String(trip.id), owner.id)?.id).toBe(trip.id);
|
||
expect(svc.verifyTripAccess(String(trip.id), member.id)).toBeDefined();
|
||
expect(svc.verifyTripAccess(String(trip.id), stranger.id)).toBeFalsy();
|
||
});
|
||
|
||
it('SHARE-SVC-002: canManage forwards the share_manage check with the ownership flag', () => {
|
||
checkPermission.mockReturnValue(true);
|
||
const trip = { id: 5, user_id: 1 } as any;
|
||
const owner = { id: 1, role: 'user' } as User;
|
||
const member = { id: 2, role: 'user' } as User;
|
||
expect(svc.canManage(trip, owner)).toBe(true);
|
||
expect(checkPermission).toHaveBeenLastCalledWith('share_manage', 'user', 1, 1, false);
|
||
svc.canManage(trip, member);
|
||
expect(checkPermission).toHaveBeenLastCalledWith('share_manage', 'user', 1, 2, true);
|
||
});
|
||
});
|
||
|
||
// ── createOrUpdate ────────────────────────────────────────────────────────────
|
||
|
||
describe('createOrUpdate', () => {
|
||
it('SHARE-SVC-003: creates a link with default flags (map/bookings on, rest off) and a 90-day expiry', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
const before = Date.now();
|
||
const result = svc.createOrUpdate(String(trip.id), user.id, {});
|
||
expect(result.created).toBe(true);
|
||
expect(result.token).toMatch(/^[A-Za-z0-9_-]+$/); // base64url
|
||
const row = shareRow(trip.id);
|
||
expect(row.token).toBe(result.token);
|
||
expect(row.created_by).toBe(user.id);
|
||
expect([row.share_map, row.share_bookings, row.share_packing, row.share_budget, row.share_collab])
|
||
.toEqual([1, 1, 0, 0, 0]);
|
||
const ninetyDays = 90 * 24 * 60 * 60 * 1000;
|
||
const expires = new Date(row.expires_at).getTime();
|
||
expect(expires).toBeGreaterThanOrEqual(before + ninetyDays - 5000);
|
||
expect(expires).toBeLessThanOrEqual(Date.now() + ninetyDays + 5000);
|
||
});
|
||
|
||
it('SHARE-SVC-004: explicit flags override the defaults in both directions', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
svc.createOrUpdate(String(trip.id), user.id, {
|
||
share_map: false, share_bookings: false, share_packing: true, share_budget: true, share_collab: true,
|
||
});
|
||
const row = shareRow(trip.id);
|
||
expect([row.share_map, row.share_bookings, row.share_packing, row.share_budget, row.share_collab])
|
||
.toEqual([0, 0, 1, 1, 1]);
|
||
});
|
||
|
||
it('SHARE-SVC-005: a second call updates the flags, keeps the token, and reports created:false', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
const first = svc.createOrUpdate(String(trip.id), user.id, {});
|
||
const second = svc.createOrUpdate(String(trip.id), user.id, { share_budget: true });
|
||
expect(second).toEqual({ token: first.token, created: false });
|
||
expect(shareRow(trip.id).share_budget).toBe(1);
|
||
});
|
||
|
||
it('SHARE-SVC-006: the update path re-applies the defaults for omitted flags and renews the 90-day expiry', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
svc.createOrUpdate(String(trip.id), user.id, { share_packing: true });
|
||
// Simulate a legacy pre-TTL row (NULL expiry): an explicit update moves it
|
||
// onto the 90-day clock.
|
||
testDb.prepare('UPDATE share_tokens SET expires_at = NULL WHERE trip_id = ?').run(trip.id);
|
||
const before = Date.now();
|
||
svc.createOrUpdate(String(trip.id), user.id, {});
|
||
const row = shareRow(trip.id);
|
||
// Omitted share_packing fell back to its default (off) — destructuring
|
||
// defaults apply on every call, not only on create.
|
||
expect(row.share_packing).toBe(0);
|
||
const ninetyDays = 90 * 24 * 60 * 60 * 1000;
|
||
const expires = new Date(row.expires_at).getTime();
|
||
expect(expires).toBeGreaterThanOrEqual(before + ninetyDays - 5000);
|
||
expect(expires).toBeLessThanOrEqual(Date.now() + ninetyDays + 5000);
|
||
});
|
||
});
|
||
|
||
// ── get / remove ──────────────────────────────────────────────────────────────
|
||
|
||
describe('get and remove', () => {
|
||
it('SHARE-SVC-007: get returns null when no link exists', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
expect(svc.get(String(trip.id))).toBeNull();
|
||
});
|
||
|
||
it('SHARE-SVC-008: get returns the token with the flags coerced to booleans', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
const { token } = svc.createOrUpdate(String(trip.id), user.id, { share_packing: true });
|
||
const info = svc.get(String(trip.id));
|
||
expect(info).toEqual({
|
||
token,
|
||
created_at: expect.any(String),
|
||
share_map: true,
|
||
share_bookings: true,
|
||
share_packing: true,
|
||
share_budget: false,
|
||
share_collab: false,
|
||
});
|
||
});
|
||
|
||
it('SHARE-SVC-009: remove deletes the link and is a no-op when none exists', () => {
|
||
const { user } = createUser(testDb);
|
||
const trip = createTrip(testDb, user.id);
|
||
svc.createOrUpdate(String(trip.id), user.id, {});
|
||
svc.remove(String(trip.id));
|
||
expect(shareRow(trip.id)).toBeUndefined();
|
||
expect(() => svc.remove(String(trip.id))).not.toThrow();
|
||
});
|
||
});
|
||
|
||
// ── getSharedTripData ─────────────────────────────────────────────────────────
|
||
|
||
describe('getSharedTripData', () => {
|
||
it('SHARE-SVC-010: returns null for an unknown token', () => {
|
||
expect(svc.getSharedTripData('nope')).toBeNull();
|
||
});
|
||
|
||
it('SHARE-SVC-011: returns null for an expired token but honours NULL expiry (legacy rows)', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
testDb.prepare('UPDATE share_tokens SET expires_at = ? WHERE trip_id = ?').run('2020-01-01T00:00:00.000Z', trip.id);
|
||
expect(svc.getSharedTripData(token)).toBeNull();
|
||
testDb.prepare('UPDATE share_tokens SET expires_at = NULL WHERE trip_id = ?').run(trip.id);
|
||
expect(svc.getSharedTripData(token)).not.toBeNull();
|
||
});
|
||
|
||
it('SHARE-SVC-012: returns null when the trip row is gone', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
// The token normally cascades away with its trip; orphan it deliberately
|
||
// to pin the defensive `if (!trip) return null` branch.
|
||
testDb.exec('PRAGMA foreign_keys = OFF');
|
||
testDb.prepare('DELETE FROM trips WHERE id = ?').run(trip.id);
|
||
testDb.exec('PRAGMA foreign_keys = ON');
|
||
expect(svc.getSharedTripData(token)).toBeNull();
|
||
});
|
||
|
||
it('SHARE-SVC-013: returns the trip projection, coerced permissions and grouped itinerary', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
const day = createDay(testDb, trip.id, { date: '2025-06-01' });
|
||
const place = createPlace(testDb, trip.id, { name: 'Louvre' });
|
||
createDayAssignment(testDb, day.id, place.id, { notes: 'go early' });
|
||
const data = svc.getSharedTripData(token)!;
|
||
expect(data).not.toBeNull();
|
||
expect(data.trip).toEqual(expect.objectContaining({ id: trip.id, title: trip.title }));
|
||
// Explicit column list — no owner id or internal fields on the trip row.
|
||
expect(Object.keys(data.trip).sort()).toEqual(
|
||
['cover_image', 'currency', 'description', 'end_date', 'id', 'start_date', 'title'],
|
||
);
|
||
expect(data.permissions).toEqual({
|
||
share_map: true, share_bookings: true, share_packing: true, share_budget: true, share_collab: true,
|
||
});
|
||
expect(data.days).toHaveLength(1);
|
||
const entries = data.assignments[day.id];
|
||
expect(entries).toHaveLength(1);
|
||
expect(entries[0]).toEqual(expect.objectContaining({ day_id: day.id, notes: 'go early' }));
|
||
expect(entries[0].place).toEqual(expect.objectContaining({
|
||
id: place.id, name: 'Louvre', tags: [],
|
||
}));
|
||
expect(entries[0].place.category).toEqual(expect.objectContaining({ id: place.category_id }));
|
||
expect(data.collab).toEqual([]);
|
||
});
|
||
|
||
it('SHARE-SVC-014: COALESCEs assignment times over place times', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
const day = createDay(testDb, trip.id);
|
||
const place = createPlace(testDb, trip.id);
|
||
testDb.prepare('UPDATE places SET place_time = ?, end_time = ? WHERE id = ?').run('09:00', '10:00', place.id);
|
||
const a = createDayAssignment(testDb, day.id, place.id);
|
||
let entry = svc.getSharedTripData(token)!.assignments[day.id][0];
|
||
expect(entry.place.place_time).toBe('09:00');
|
||
expect(entry.place.end_time).toBe('10:00');
|
||
testDb.prepare('UPDATE day_assignments SET assignment_time = ?, assignment_end_time = ? WHERE id = ?').run('14:00', '15:00', a.id);
|
||
entry = svc.getSharedTripData(token)!.assignments[day.id][0];
|
||
expect(entry.place.place_time).toBe('14:00');
|
||
expect(entry.place.end_time).toBe('15:00');
|
||
});
|
||
|
||
it('SHARE-SVC-015: orders assignments by order_index, then created_at as tiebreaker', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
const day = createDay(testDb, trip.id);
|
||
const p1 = createPlace(testDb, trip.id, { name: 'First' });
|
||
const p2 = createPlace(testDb, trip.id, { name: 'Second' });
|
||
testDb.prepare(
|
||
"INSERT INTO day_assignments (day_id, place_id, order_index, created_at) VALUES (?, ?, 0, '2025-01-01T11:00:00')"
|
||
).run(day.id, p2.id);
|
||
testDb.prepare(
|
||
"INSERT INTO day_assignments (day_id, place_id, order_index, created_at) VALUES (?, ?, 0, '2025-01-01T10:00:00')"
|
||
).run(day.id, p1.id);
|
||
const entries = svc.getSharedTripData(token)!.assignments[day.id];
|
||
expect(entries.map((e: any) => e.place.name)).toEqual(['First', 'Second']);
|
||
});
|
||
|
||
it('SHARE-SVC-016: attaches reservation day_positions, null when a reservation has none', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
const day = createDay(testDb, trip.id);
|
||
const r1 = testDb.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Flight', 'flight')").run(trip.id);
|
||
const r2 = testDb.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Hotel', 'hotel')").run(trip.id);
|
||
testDb.prepare('INSERT INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, 3)')
|
||
.run(r1.lastInsertRowid, day.id);
|
||
const reservations = svc.getSharedTripData(token)!.reservations;
|
||
const flight = reservations.find((r: any) => r.id === r1.lastInsertRowid);
|
||
const hotel = reservations.find((r: any) => r.id === r2.lastInsertRowid);
|
||
expect(flight.day_positions).toEqual({ [day.id]: 3 });
|
||
expect(hotel.day_positions).toBeNull();
|
||
});
|
||
|
||
it('SHARE-SVC-017: excludes private packing items from the public payload (#858)', () => {
|
||
const { user, trip, token } = seedSharedTrip();
|
||
testDb.prepare("INSERT INTO packing_items (trip_id, name, is_private, owner_id) VALUES (?, 'Common', 0, ?)").run(trip.id, user.id);
|
||
testDb.prepare("INSERT INTO packing_items (trip_id, name, is_private, owner_id) VALUES (?, 'Secret', 1, ?)").run(trip.id, user.id);
|
||
const packing = svc.getSharedTripData(token)!.packing;
|
||
expect(packing.map((p: any) => p.name)).toEqual(['Common']);
|
||
});
|
||
|
||
it('SHARE-SVC-018: includes non-deleted collab messages only when share_collab is on', () => {
|
||
const { user, trip, token } = seedSharedTrip();
|
||
testDb.prepare("INSERT INTO collab_messages (trip_id, user_id, text, deleted) VALUES (?, ?, 'Hello', 0)").run(trip.id, user.id);
|
||
testDb.prepare("INSERT INTO collab_messages (trip_id, user_id, text, deleted) VALUES (?, ?, 'Gone', 1)").run(trip.id, user.id);
|
||
const collab = svc.getSharedTripData(token)!.collab;
|
||
expect(collab).toHaveLength(1);
|
||
expect(collab[0]).toEqual(expect.objectContaining({ text: 'Hello', username: user.username }));
|
||
|
||
svc.createOrUpdate(String(trip.id), user.id, { share_collab: false });
|
||
expect(svc.getSharedTripData(token)!.collab).toEqual([]);
|
||
});
|
||
|
||
it('SHARE-SVC-019: withholds each section when its flag is off', () => {
|
||
const { trip, token } = seedSharedTrip({
|
||
share_map: false, share_bookings: false, share_packing: false, share_budget: false, share_collab: false,
|
||
});
|
||
const day = createDay(testDb, trip.id);
|
||
const place = createPlace(testDb, trip.id);
|
||
createDayAssignment(testDb, day.id, place.id);
|
||
testDb.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Flight', 'flight')").run(trip.id);
|
||
testDb.prepare("INSERT INTO packing_items (trip_id, name) VALUES (?, 'Socks')").run(trip.id);
|
||
testDb.prepare("INSERT INTO budget_items (trip_id, name, category, total_price) VALUES (?, 'Food', 'food', 10)").run(trip.id);
|
||
const data = svc.getSharedTripData(token)!;
|
||
expect(data.days).toEqual([]);
|
||
expect(data.assignments).toEqual({});
|
||
expect(data.dayNotes).toEqual({});
|
||
expect(data.places).toEqual([]);
|
||
expect(data.reservations).toEqual([]);
|
||
expect(data.accommodations).toEqual([]);
|
||
expect(data.packing).toEqual([]);
|
||
expect(data.budget).toEqual([]);
|
||
expect(data.collab).toEqual([]);
|
||
});
|
||
|
||
it('SHARE-SVC-020: baseCurrency falls back trip currency → EUR, with the owner default_currency winning (#1361)', () => {
|
||
const { user, trip, token } = seedSharedTrip();
|
||
expect(svc.getSharedTripData(token)!.baseCurrency).toBe('EUR');
|
||
testDb.prepare('UPDATE trips SET currency = ? WHERE id = ?').run('USD', trip.id);
|
||
expect(svc.getSharedTripData(token)!.baseCurrency).toBe('USD');
|
||
testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (?, 'default_currency', ' CHF ')").run(user.id);
|
||
expect(svc.getSharedTripData(token)!.baseCurrency).toBe('CHF');
|
||
});
|
||
|
||
it('SHARE-SVC-021: rewrites place-photo proxy URLs to the token-scoped route, passing others through', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
const proxied = createPlace(testDb, trip.id, { name: 'Proxied' });
|
||
const uploaded = createPlace(testDb, trip.id, { name: 'Uploaded' });
|
||
createPlace(testDb, trip.id, { name: 'Bare' });
|
||
testDb.prepare('UPDATE places SET image_url = ? WHERE id = ?').run('/api/maps/place-photo/ChIJabc/bytes', proxied.id);
|
||
testDb.prepare('UPDATE places SET image_url = ? WHERE id = ?').run('/uploads/pic.jpg', uploaded.id);
|
||
const byName = Object.fromEntries(
|
||
svc.getSharedTripData(token)!.places.map((p: any) => [p.name, p.image_url]),
|
||
);
|
||
expect(byName['Proxied']).toBe(`/api/shared/${token}/place-photo/ChIJabc/bytes`);
|
||
expect(byName['Uploaded']).toBe('/uploads/pic.jpg');
|
||
expect(byName['Bare']).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ── getSharedPlacePhotoPath ───────────────────────────────────────────────────
|
||
|
||
describe('getSharedPlacePhotoPath', () => {
|
||
it('SHARE-SVC-022: returns null for an unknown or expired token', () => {
|
||
expect(svc.getSharedPlacePhotoPath('nope', 'ChIJabc')).toBeNull();
|
||
const { trip, token } = seedSharedTrip();
|
||
testDb.prepare('UPDATE share_tokens SET expires_at = ? WHERE trip_id = ?').run('2020-01-01T00:00:00.000Z', trip.id);
|
||
expect(svc.getSharedPlacePhotoPath(token, 'ChIJabc')).toBeNull();
|
||
expect(serveFilePath).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('SHARE-SVC-023: returns null when the owner disabled the map section', () => {
|
||
const { trip, token } = seedSharedTrip({ share_map: false });
|
||
const place = createPlace(testDb, trip.id);
|
||
testDb.prepare('UPDATE places SET image_url = ? WHERE id = ?').run('/api/maps/place-photo/ChIJabc/bytes', place.id);
|
||
expect(svc.getSharedPlacePhotoPath(token, 'ChIJabc')).toBeNull();
|
||
expect(serveFilePath).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('SHARE-SVC-024: returns null when no place in the trip carries the proxy URL', () => {
|
||
const { token } = seedSharedTrip();
|
||
expect(svc.getSharedPlacePhotoPath(token, 'ChIJabc')).toBeNull();
|
||
expect(serveFilePath).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('SHARE-SVC-025: resolves via serveFilePath when the encoded placeId matches the stored URL', () => {
|
||
const { trip, token } = seedSharedTrip();
|
||
const place = createPlace(testDb, trip.id);
|
||
// Wikimedia pseudo-IDs contain characters that must round-trip encoded.
|
||
const placeId = 'coords:48.8,2.3';
|
||
testDb.prepare('UPDATE places SET image_url = ? WHERE id = ?')
|
||
.run(`/api/maps/place-photo/${encodeURIComponent(placeId)}/bytes`, place.id);
|
||
serveFilePath.mockReturnValue('/cache/abc.jpg');
|
||
expect(svc.getSharedPlacePhotoPath(token, placeId)).toBe('/cache/abc.jpg');
|
||
expect(serveFilePath).toHaveBeenCalledWith(placeId);
|
||
});
|
||
});
|
||
|
||
// SHARE-SVC-026..028 (share.bridge delegation) were deleted with the bridge —
|
||
// its last consumer, the legacy share-link tools in src/mcp/tools/trips.ts,
|
||
// moved to the DI-discovered share.mcp.ts.
|