mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
test(server): seed the share, quota, photo and stats specs through the factories
Converts the raw prepare() fixtures and reads in the share and place shadow
e2e suites, the google quota, travel stats and MCP guard services, the
memories MCP spec, the admin seeder spec and the assignment participants
repository spec to the ORM factories and readers.
The place shadow e2e moves from hand-rolled tables to the migrated snapshot
with its users pinned to the ids their session cookies are signed for. The
share e2e drops the raw canAccessTrip it still put on the database mock:
nothing has read it since trip access moved onto the repositories. The
quota spec computes yesterday and the retention window in UTC, the same
calendar day SQLite's date('now') gives. The participants oracle keeps its
raw statement behind an allow marker.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* /api/place-shadow e2e — the real JwtAuthGuard, the real AdminGuard and the
|
||||
* real Zod pipe against a temp SQLite db.
|
||||
* real Zod pipe against a migrated temp SQLite db.
|
||||
*
|
||||
* The three things worth booting a server for: that a non-admin cannot read
|
||||
* other people's searches, that a switched-off log answers 200 instead of an
|
||||
@@ -14,33 +14,24 @@ import type { Server } from 'http';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { APP_PIPE } from '@nestjs/core';
|
||||
import { ZodValidationPipe } from 'nestjs-zod';
|
||||
import { seedUser, sessionCookie } from './harness';
|
||||
import { sessionCookie } from './harness';
|
||||
|
||||
const { db } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Database = require('better-sqlite3');
|
||||
const tmp = new Database(':memory:');
|
||||
tmp.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE, role TEXT NOT NULL DEFAULT 'user', password_version INTEGER NOT NULL DEFAULT 0);`);
|
||||
tmp.exec('CREATE TABLE app_settings (key TEXT PRIMARY KEY, value TEXT);');
|
||||
tmp.exec(`CREATE TABLE place_shadow_picks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
query TEXT NOT NULL, lang TEXT, bias_lat REAL, bias_lng REAL, source TEXT NOT NULL,
|
||||
live_rank INTEGER NOT NULL, live_count INTEGER NOT NULL,
|
||||
picked_name TEXT NOT NULL, picked_lat REAL NOT NULL, picked_lng REAL NOT NULL,
|
||||
picked_place_id TEXT);`);
|
||||
return { db: tmp };
|
||||
vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
|
||||
vi.mock('../../src/db/database', () => ({
|
||||
db, canAccessTrip: vi.fn(), isOwner: vi.fn(), getPlaceWithTags: vi.fn(), closeDb: () => {}, reinitialize: () => {},
|
||||
}));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { PlaceShadowPicks } from '../../src/db/entities/PlaceShadowPicks.entity';
|
||||
import { PlaceShadowModule } from '../../src/nest/place-shadow/place-shadow.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm';
|
||||
import { makeAdmin, makeUser } from '../helpers/factories/users';
|
||||
import { countRows, deleteRows } from '../helpers/factories/rows';
|
||||
import { setAppSetting } from '../helpers/factories/settings';
|
||||
|
||||
let orm: TestOrm;
|
||||
|
||||
const PICK = {
|
||||
query: 'kaffee bar am dobi',
|
||||
@@ -56,21 +47,18 @@ const PICK = {
|
||||
const USER = 1;
|
||||
const ADMIN = 2;
|
||||
|
||||
function enable(on: boolean) {
|
||||
db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)')
|
||||
.run('place_shadow_enabled', on ? 'true' : 'false');
|
||||
async function enable(on: boolean): Promise<void> {
|
||||
await setAppSetting(orm, 'place_shadow_enabled', on ? 'true' : 'false');
|
||||
}
|
||||
|
||||
describe('/api/place-shadow e2e (real guards + temp SQLite)', () => {
|
||||
describe('/api/place-shadow e2e (real guards + migrated temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
// JwtAuthGuard (Plan 3b Task 1) injects EntityManager — needs
|
||||
// JwtAuthGuard (Plan 3b Task 1) injects EntityManager, so it needs
|
||||
// MikroOrmModule.forRoot in the graph, same as every other e2e harness
|
||||
// guarding a route with it; this suite's minimal hand-rolled `users`
|
||||
// table already carries the five columns
|
||||
// `findByIdWithPasswordVersion` selects.
|
||||
// guarding a route with it.
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), PlaceShadowModule],
|
||||
providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }],
|
||||
@@ -83,19 +71,22 @@ describe('/api/place-shadow e2e (real guards + temp SQLite)', () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
seedUser(db as never, { id: USER });
|
||||
seedUser(db as never, { id: ADMIN, email: 'admin@example.com', role: 'admin' });
|
||||
orm = await createTestOrm(db);
|
||||
// Pinned ids: sessionCookie(USER) and (ADMIN) sign for exactly these users.
|
||||
await makeUser(orm, { id: USER, email: 'e2e@example.test' });
|
||||
await makeAdmin(orm, { id: ADMIN, email: 'admin@example.com' });
|
||||
app = await build();
|
||||
server = app.getHttpServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
await orm.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
db.prepare('DELETE FROM place_shadow_picks').run();
|
||||
enable(true);
|
||||
beforeEach(async () => {
|
||||
await deleteRows(orm, PlaceShadowPicks);
|
||||
await enable(true);
|
||||
});
|
||||
|
||||
describe('POST pick', () => {
|
||||
@@ -107,15 +98,15 @@ describe('/api/place-shadow e2e (real guards + temp SQLite)', () => {
|
||||
const res = await request(server).post('/api/place-shadow/pick').set('Cookie', sessionCookie(USER)).send(PICK);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ recorded: true });
|
||||
expect(db.prepare('SELECT COUNT(*) AS n FROM place_shadow_picks').get()).toEqual({ n: 1 });
|
||||
expect(await countRows(orm, PlaceShadowPicks)).toBe(1);
|
||||
});
|
||||
|
||||
it('200 { recorded: false } while the log is off, not an error status', async () => {
|
||||
enable(false);
|
||||
await enable(false);
|
||||
const res = await request(server).post('/api/place-shadow/pick').set('Cookie', sessionCookie(USER)).send(PICK);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ recorded: false });
|
||||
expect(db.prepare('SELECT COUNT(*) AS n FROM place_shadow_picks').get()).toEqual({ n: 0 });
|
||||
expect(await countRows(orm, PlaceShadowPicks)).toBe(0);
|
||||
});
|
||||
|
||||
it('400 from the pipe on a malformed body, and nothing is written', async () => {
|
||||
@@ -129,7 +120,7 @@ describe('/api/place-shadow e2e (real guards + temp SQLite)', () => {
|
||||
const res = await request(server).post('/api/place-shadow/pick').set('Cookie', sessionCookie(USER)).send(bad);
|
||||
expect(res.status, JSON.stringify(bad).slice(0, 60)).toBe(400);
|
||||
}
|
||||
expect(db.prepare('SELECT COUNT(*) AS n FROM place_shadow_picks').get()).toEqual({ n: 0 });
|
||||
expect(await countRows(orm, PlaceShadowPicks)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,7 +160,7 @@ describe('/api/place-shadow e2e (real guards + temp SQLite)', () => {
|
||||
const wiped = await request(server).delete('/api/place-shadow').set('Cookie', sessionCookie(ADMIN));
|
||||
expect(wiped.status).toBe(200);
|
||||
expect(wiped.body).toEqual({ removed: 1 });
|
||||
expect(db.prepare('SELECT COUNT(*) AS n FROM place_shadow_picks').get()).toEqual({ n: 0 });
|
||||
expect(await countRows(orm, PlaceShadowPicks)).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* public /api/shared/:token endpoints through the real JwtAuthGuard against a
|
||||
* temp SQLite db. ShareService runs its real SQL via DatabaseModule (the
|
||||
* DATABASE_CONNECTION factory picks up the mocked db singleton); trip access
|
||||
* resolves through a real-SQL canAccessTrip over the temp db. Only the
|
||||
* resolves through the real repositories over the temp db. Only the
|
||||
* permission check and the photo-cache path lookup stay mocked. Focuses on
|
||||
* auth, trip-access 404, permission 403, the create-201-vs-update-200 split
|
||||
* and the unguarded public read.
|
||||
@@ -20,14 +20,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const db = createSnapshotTestDb();
|
||||
return {
|
||||
db,
|
||||
// Real-SQL trip access over the temp db — ShareService.verifyTripAccess and
|
||||
// DatabaseModule both read the mocked singleton.
|
||||
canAccessTrip: (tripId: number | string, 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: () => false,
|
||||
getPlaceWithTags: () => null,
|
||||
closeDb: () => {},
|
||||
@@ -53,7 +45,14 @@ import { PlacePhotoCacheService } from '../../src/nest/place-photos/place-photo-
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule } from '../helpers/test-orm';
|
||||
import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm';
|
||||
import { makeUser } from '../helpers/factories/users';
|
||||
import { makeTrip } from '../helpers/factories/trips';
|
||||
import { findRow, insertRow } from '../helpers/factories/rows';
|
||||
import { Places } from '../../src/db/entities/Places.entity';
|
||||
import { ShareTokens } from '../../src/db/entities/ShareTokens.entity';
|
||||
|
||||
let orm: TestOrm;
|
||||
|
||||
describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
|
||||
let server: Server;
|
||||
@@ -75,31 +74,31 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// seedUser() omits password_hash, which the real schema requires NOT NULL.
|
||||
db.prepare(
|
||||
"INSERT INTO users (id, username, email, password_hash, role) VALUES (1, 'e2e-user', 'e2e@example.test', 'x', 'user')",
|
||||
).run();
|
||||
orm = await createTestOrm(db);
|
||||
// Pinned id: sessionCookie(1) signs for exactly this user.
|
||||
await makeUser(orm, { id: 1, username: 'e2e-user', email: 'e2e@example.test' });
|
||||
app = await build();
|
||||
checkPermission = vi.spyOn(app.get(PermissionsService), 'checkPermission');
|
||||
server = app.getHttpServer();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
db.exec('DELETE FROM share_tokens');
|
||||
db.exec('DELETE FROM places');
|
||||
db.exec('DELETE FROM trip_members');
|
||||
db.exec('DELETE FROM trips');
|
||||
tripId = Number(db.prepare('INSERT INTO trips (user_id, title) VALUES (1, ?)').run('Trip').lastInsertRowid);
|
||||
tripId = (await makeTrip(orm, 1, { title: 'Trip' })).id;
|
||||
checkPermission.mockReturnValue(true);
|
||||
serveKey.mockReset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
await orm.close();
|
||||
});
|
||||
|
||||
function tokenRow() {
|
||||
return db.prepare('SELECT * FROM share_tokens WHERE trip_id = ?').get(tripId) as any;
|
||||
return findRow(orm, ShareTokens, { trip: tripId });
|
||||
}
|
||||
|
||||
it('401 without a session cookie', async () => {
|
||||
@@ -109,7 +108,7 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
|
||||
it('201 on first create, 200 on a subsequent update', async () => {
|
||||
const created = await request(server).post(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1)).send({ share_map: true });
|
||||
expect(created.status).toBe(201);
|
||||
expect(created.body).toEqual({ token: tokenRow().token });
|
||||
expect(created.body).toEqual({ token: (await tokenRow())?.token });
|
||||
|
||||
const updated = await request(server).post(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1)).send({});
|
||||
expect(updated.status).toBe(200);
|
||||
@@ -121,7 +120,7 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
|
||||
const res = await request(server).post(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1)).send({});
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body).toEqual({ error: 'No permission' });
|
||||
expect(tokenRow()).toBeUndefined();
|
||||
expect(await tokenRow()).toBeNull();
|
||||
});
|
||||
|
||||
it('404 when the trip is not accessible', async () => {
|
||||
@@ -134,12 +133,12 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
|
||||
await request(server).post(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1)).send({ share_budget: true });
|
||||
const info = await request(server).get(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1));
|
||||
expect(info.status).toBe(200);
|
||||
expect(info.body).toEqual(expect.objectContaining({ token: tokenRow().token, share_budget: true, share_packing: false }));
|
||||
expect(info.body).toEqual(expect.objectContaining({ token: (await tokenRow())?.token, share_budget: true, share_packing: false }));
|
||||
|
||||
const removed = await request(server).delete(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1));
|
||||
expect(removed.status).toBe(200);
|
||||
expect(removed.body).toEqual({ success: true });
|
||||
expect(tokenRow()).toBeUndefined();
|
||||
expect(await tokenRow()).toBeNull();
|
||||
|
||||
const after = await request(server).get(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1));
|
||||
expect(after.body).toEqual({ token: null });
|
||||
@@ -186,8 +185,7 @@ describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => {
|
||||
|
||||
it('streams cached bytes with no cookie (unguarded) for a valid token + place', async () => {
|
||||
const created = await request(server).post(`/api/trips/${tripId}/share-link`).set('Cookie', sessionCookie(1)).send({});
|
||||
db.prepare('INSERT INTO places (trip_id, name, image_url) VALUES (?, ?, ?)')
|
||||
.run(tripId, 'Louvre', '/api/maps/place-photo/ChIJabc/bytes');
|
||||
await insertRow(orm, Places, { trip: tripId, name: 'Louvre', image_url: '/api/maps/place-photo/ChIJabc/bytes' });
|
||||
serveKey.mockReturnValueOnce(photoName);
|
||||
const res = await request(server).get(`/api/shared/${created.body.token}/place-photo/ChIJabc/bytes`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
@@ -15,6 +15,8 @@ import { AssignmentParticipants } from '../../../../src/db/entities/AssignmentPa
|
||||
import type { AssignmentParticipantsRepository } from '../../../../src/db/repositories/AssignmentParticipants.repository';
|
||||
import { UnitOfWork } from '../../../../src/nest/database/unit-of-work';
|
||||
import { withRequestContext } from '../../../../src/nest/database/request-context';
|
||||
import { Users } from '../../../../src/db/entities/Users.entity';
|
||||
import { findRows, insertRow, updateRows } from '../../../helpers/factories/rows';
|
||||
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
@@ -29,8 +31,12 @@ beforeAll(async () => {
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
function addParticipant(assignmentId: number, userId: number): void {
|
||||
testDb.prepare('INSERT INTO assignment_participants (assignment_id, user_id) VALUES (?, ?)').run(assignmentId, userId);
|
||||
async function addParticipant(assignmentId: number, userId: number): Promise<void> {
|
||||
await insertRow(t, AssignmentParticipants, { assignment: assignmentId, user: userId });
|
||||
}
|
||||
|
||||
async function participantUserIds(assignmentId: number) {
|
||||
return (await findRows(t, AssignmentParticipants, { assignment: assignmentId })).map((r) => ({ user_id: r.user_id }));
|
||||
}
|
||||
|
||||
async function withQueryCount<T>(fn: () => Promise<T>): Promise<{ value: T; queries: number }> {
|
||||
@@ -54,12 +60,12 @@ describe('AssignmentParticipantsRepository.listForAssignments', () => {
|
||||
it('ASSIGNPARTREPO-002: joins the user\'s username/avatar, with NO COALESCE(display_name, username) — raw username only', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: participant } = createUser(testDb, { username: 'raw-username' });
|
||||
testDb.prepare('UPDATE users SET display_name = ? WHERE id = ?').run('A Display Name', participant.id);
|
||||
await updateRows(t, Users, { id: participant.id }, { display_name: 'A Display Name' });
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(assignment.id, participant.id);
|
||||
await addParticipant(assignment.id, participant.id);
|
||||
|
||||
const rows = await participants.listForAssignments([assignment.id]);
|
||||
expect(rows).toEqual([{ assignment_id: assignment.id, user_id: participant.id, username: 'raw-username', avatar: null }]);
|
||||
@@ -75,8 +81,8 @@ describe('AssignmentParticipantsRepository.listForAssignments', () => {
|
||||
const placeB = createPlace(testDb, trip.id, { name: 'B' });
|
||||
const a1 = createDayAssignment(testDb, day.id, placeA.id);
|
||||
const a2 = createDayAssignment(testDb, day.id, placeB.id);
|
||||
addParticipant(a1.id, p1.id);
|
||||
addParticipant(a2.id, p2.id);
|
||||
await addParticipant(a1.id, p1.id);
|
||||
await addParticipant(a2.id, p2.id);
|
||||
|
||||
const rows = await participants.listForAssignments([a1.id, a2.id]);
|
||||
expect(rows.map((r) => r.assignment_id).sort()).toEqual([a1.id, a2.id].sort());
|
||||
@@ -110,7 +116,7 @@ describe('AssignmentParticipantsRepository.listForAssignments', () => {
|
||||
// nothing here, but the mechanical fix is applied uniformly rather than
|
||||
// singled out.
|
||||
await t.repo(AssignmentParticipants).find({}, { disableIdentityMap: false }); // populate the identity map with an unrelated (empty) read
|
||||
addParticipant(assignment.id, participant.id);
|
||||
await addParticipant(assignment.id, participant.id);
|
||||
const rows = await participants.listForAssignments([assignment.id]);
|
||||
expect(rows).toEqual([{ assignment_id: assignment.id, user_id: participant.id, username: 'fresh-participant', avatar: null }]);
|
||||
});
|
||||
@@ -125,6 +131,7 @@ describe('AssignmentParticipantsRepository.listForAssignments', () => {
|
||||
|
||||
/** The AS2/AS15/AS31 statement, run raw — the parity oracle every assertion below is checked against. */
|
||||
function legacyParticipantRows(assignmentId: number): unknown {
|
||||
// test-sql-allow: the legacy statement is this parity test's oracle and has to run as written.
|
||||
return testDb.prepare(`
|
||||
SELECT ap.user_id, COALESCE(u.display_name, u.username) AS username, u.avatar
|
||||
FROM assignment_participants ap
|
||||
@@ -141,7 +148,7 @@ describe('AssignmentParticipantsRepository.listWithDisplayName (AS2/AS15/AS31)',
|
||||
const day = createDay(testDb, trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(assignment.id, participant.id);
|
||||
await addParticipant(assignment.id, participant.id);
|
||||
|
||||
const rows = await participants.listWithDisplayName(assignment.id);
|
||||
expect(rows).toStrictEqual(legacyParticipantRows(assignment.id));
|
||||
@@ -151,14 +158,14 @@ describe('AssignmentParticipantsRepository.listWithDisplayName (AS2/AS15/AS31)',
|
||||
it('ASSIGNPARTREPO-007: username COALESCEs display_name over username, when set', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: withDisplayName } = createUser(testDb, { username: 'raw-username' });
|
||||
testDb.prepare('UPDATE users SET display_name = ? WHERE id = ?').run('Fancy Name', withDisplayName.id);
|
||||
await updateRows(t, Users, { id: withDisplayName.id }, { display_name: 'Fancy Name' });
|
||||
const { user: withoutDisplayName } = createUser(testDb, { username: 'plain-username' });
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(assignment.id, withDisplayName.id);
|
||||
addParticipant(assignment.id, withoutDisplayName.id);
|
||||
await addParticipant(assignment.id, withDisplayName.id);
|
||||
await addParticipant(assignment.id, withoutDisplayName.id);
|
||||
|
||||
const rows = await participants.listWithDisplayName(assignment.id);
|
||||
expect(rows).toEqual(expect.arrayContaining([
|
||||
@@ -189,7 +196,7 @@ describe('AssignmentParticipantsRepository.listWithDisplayName (AS2/AS15/AS31)',
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
await t.repo(AssignmentParticipants).find({}, { disableIdentityMap: false });
|
||||
addParticipant(assignment.id, participant.id);
|
||||
await addParticipant(assignment.id, participant.id);
|
||||
const rows = await participants.listWithDisplayName(assignment.id);
|
||||
expect(rows).toEqual([{ user_id: participant.id, username: 'fresh-participant-2', avatar: null }]);
|
||||
});
|
||||
@@ -205,8 +212,8 @@ describe('AssignmentParticipantsRepository.deleteForAssignment / insertIgnore (A
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const a1 = createDayAssignment(testDb, day.id, place.id);
|
||||
const a2 = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(a1.id, p1.id);
|
||||
addParticipant(a2.id, p2.id);
|
||||
await addParticipant(a1.id, p1.id);
|
||||
await addParticipant(a2.id, p2.id);
|
||||
|
||||
await withRequestContext(t.orm, async () => {
|
||||
await participants.deleteForAssignment(a1.id);
|
||||
@@ -228,7 +235,7 @@ describe('AssignmentParticipantsRepository.deleteForAssignment / insertIgnore (A
|
||||
await participants.insertIgnore(assignment.id, [p1.id, p1.id]);
|
||||
});
|
||||
|
||||
const rows = testDb.prepare('SELECT user_id FROM assignment_participants WHERE assignment_id = ?').all(assignment.id);
|
||||
const rows = await participantUserIds(assignment.id);
|
||||
expect(rows).toEqual([{ user_id: p1.id }]);
|
||||
});
|
||||
|
||||
@@ -240,7 +247,7 @@ describe('AssignmentParticipantsRepository.deleteForAssignment / insertIgnore (A
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const a1 = createDayAssignment(testDb, day.id, place.id);
|
||||
const a2 = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(a1.id, shared.id);
|
||||
await addParticipant(a1.id, shared.id);
|
||||
|
||||
await withRequestContext(t.orm, async () => {
|
||||
await participants.insertIgnore(a2.id, [shared.id]);
|
||||
@@ -271,7 +278,7 @@ describe('AssignmentParticipantsRepository.deleteForAssignment / insertIgnore (A
|
||||
const day = createDay(testDb, trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(assignment.id, p1.id);
|
||||
await addParticipant(assignment.id, p1.id);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
@@ -285,7 +292,7 @@ describe('AssignmentParticipantsRepository.deleteForAssignment / insertIgnore (A
|
||||
} catch (e) { caught = e; }
|
||||
expect((caught as Error).message).toBe('force rollback');
|
||||
|
||||
const rows = testDb.prepare('SELECT user_id FROM assignment_participants WHERE assignment_id = ?').all(assignment.id);
|
||||
const rows = await participantUserIds(assignment.id);
|
||||
expect(rows).toEqual([{ user_id: p1.id }]);
|
||||
});
|
||||
});
|
||||
@@ -302,13 +309,13 @@ describe('AssignmentParticipantsRepository.listForTrip (TP53)', () => {
|
||||
const day = createDay(testDb, trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
addParticipant(assignment.id, p1.id);
|
||||
addParticipant(assignment.id, p2.id);
|
||||
await addParticipant(assignment.id, p1.id);
|
||||
await addParticipant(assignment.id, p2.id);
|
||||
|
||||
const otherDay = createDay(testDb, other.id);
|
||||
const otherPlace = createPlace(testDb, other.id);
|
||||
const otherAssignment = createDayAssignment(testDb, otherDay.id, otherPlace.id);
|
||||
addParticipant(otherAssignment.id, p1.id);
|
||||
await addParticipant(otherAssignment.id, p1.id);
|
||||
|
||||
const rows = await participants.listForTrip(trip.id);
|
||||
expect(rows.map((r) => r.user_id).sort((a, b) => a - b)).toEqual([p1.id, p2.id].sort((a, b) => a - b));
|
||||
|
||||
@@ -13,20 +13,25 @@
|
||||
import { AdminSeeder } from '../../../src/db/seeders/AdminSeeder';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { countRows, findRow } from '../../helpers/factories/rows';
|
||||
import { makeAdmin } from '../../helpers/factories/users';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
|
||||
import type Database from 'better-sqlite3';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
const ENV_KEYS = ['ADMIN_EMAIL', 'ADMIN_PASSWORD', 'DEMO_MODE', 'OIDC_ONLY', 'OIDC_ISSUER', 'OIDC_CLIENT_ID'];
|
||||
|
||||
function countUsers(db: Database.Database): number {
|
||||
return (db.prepare('SELECT COUNT(*) as c FROM users').get() as { c: number }).c;
|
||||
function countUsers(orm: TestOrm): Promise<number> {
|
||||
return countRows(orm, Users);
|
||||
}
|
||||
|
||||
function insertExistingUser(db: Database.Database): void {
|
||||
db.prepare(
|
||||
"INSERT INTO users (username, email, password_hash, role) VALUES ('admin', 'admin@trek.local', 'x', 'admin')",
|
||||
).run();
|
||||
async function insertExistingUser(orm: TestOrm): Promise<void> {
|
||||
await makeAdmin(orm, { username: 'admin', email: 'admin@trek.local' });
|
||||
}
|
||||
|
||||
function userByEmail(orm: TestOrm, email: string) {
|
||||
return findRow(orm, Users, { email });
|
||||
}
|
||||
|
||||
describe('AdminSeeder — first-run admin', () => {
|
||||
@@ -61,36 +66,34 @@ describe('AdminSeeder — first-run admin', () => {
|
||||
|
||||
await new AdminSeeder().run(t.em);
|
||||
|
||||
const user = db
|
||||
.prepare('SELECT email, role, must_change_password FROM users WHERE email = ?')
|
||||
.get('me@example.com') as { email: string; role: string; must_change_password: number } | undefined;
|
||||
expect(user).toBeDefined();
|
||||
const user = await userByEmail(t, 'me@example.com');
|
||||
expect(user).not.toBeNull();
|
||||
expect(user!.role).toBe('admin');
|
||||
expect(user!.must_change_password).toBe(1);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('warns and creates nothing when ADMIN_* is set but a user already exists', async () => {
|
||||
insertExistingUser(db);
|
||||
await insertExistingUser(t);
|
||||
process.env.ADMIN_EMAIL = 'new@example.com';
|
||||
process.env.ADMIN_PASSWORD = 'whatever';
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
await new AdminSeeder().run(t.em);
|
||||
|
||||
expect(countUsers(db)).toBe(1);
|
||||
expect(db.prepare('SELECT 1 FROM users WHERE email = ?').get('new@example.com')).toBeUndefined();
|
||||
expect(await countUsers(t)).toBe(1);
|
||||
expect(await userByEmail(t, 'new@example.com')).toBeNull();
|
||||
const msg = warn.mock.calls.map((c) => c.join(' ')).join('\n');
|
||||
expect(msg).toContain('only apply on first run');
|
||||
});
|
||||
|
||||
it('stays silent when no admin env is set and a user already exists', async () => {
|
||||
insertExistingUser(db);
|
||||
await insertExistingUser(t);
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
await new AdminSeeder().run(t.em);
|
||||
|
||||
expect(countUsers(db)).toBe(1);
|
||||
expect(await countUsers(t)).toBe(1);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -101,8 +104,8 @@ describe('AdminSeeder — first-run admin', () => {
|
||||
await new AdminSeeder().run(t.em);
|
||||
|
||||
// Falls back to the default local admin, NOT the provided email.
|
||||
expect(db.prepare('SELECT 1 FROM users WHERE email = ?').get('admin@trek.local')).toBeDefined();
|
||||
expect(db.prepare('SELECT 1 FROM users WHERE email = ?').get('me@example.com')).toBeUndefined();
|
||||
expect(await userByEmail(t, 'admin@trek.local')).not.toBeNull();
|
||||
expect(await userByEmail(t, 'me@example.com')).toBeNull();
|
||||
const msg = warn.mock.calls.map((c) => c.join(' ')).join('\n');
|
||||
expect(msg).toContain('Only one of ADMIN_EMAIL/ADMIN_PASSWORD');
|
||||
});
|
||||
|
||||
@@ -31,6 +31,12 @@ import { createMcpHarness, parseToolResult, type McpHarness } from '../../helper
|
||||
import { ImmichService } from '../../../src/nest/memories/immich.service';
|
||||
import { SynologyService } from '../../../src/nest/memories/synology.service';
|
||||
import { PhotoCaptureBackfillService } from '../../../src/nest/memories/photo-capture-backfill.service';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { countRows, deleteRows, findRow, findRows, upsertRow } from '../../helpers/factories/rows';
|
||||
import { JourneyEntryPhotos } from '../../../src/db/entities/JourneyEntryPhotos.entity';
|
||||
import { JourneyPhotos } from '../../../src/db/entities/JourneyPhotos.entity';
|
||||
import { PhotoProviders } from '../../../src/db/entities/PhotoProviders.entity';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
|
||||
const immichSearch = vi.spyOn(ImmichService.prototype, 'searchPhotos');
|
||||
const immichAlbums = vi.spyOn(ImmichService.prototype, 'listAlbums');
|
||||
@@ -50,21 +56,19 @@ const SYNOLOGY_ASSET = { id: 's1', takenAt: '2026-07-02T10:00:00.000Z', lat: 48.
|
||||
* photo_providers is seed data, so resetTestDb leaves it alone and a toggle
|
||||
* leaks into the next case. Every case states what it needs.
|
||||
*/
|
||||
function setProviderEnabled(id: string, enabled: boolean): void {
|
||||
testDb.prepare(
|
||||
'INSERT INTO photo_providers (id, name, enabled) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled',
|
||||
).run(id, id, enabled ? 1 : 0);
|
||||
async function setProviderEnabled(id: string, enabled: boolean): Promise<void> {
|
||||
await upsertRow(orm, PhotoProviders, { id, name: id, enabled: enabled ? 1 : 0 }, ['enabled']);
|
||||
}
|
||||
|
||||
function removeProvider(id: string): void {
|
||||
testDb.prepare('DELETE FROM photo_providers WHERE id = ?').run(id);
|
||||
async function removeProvider(id: string): Promise<void> {
|
||||
await deleteRows(orm, PhotoProviders, { id });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
resetTestDb(testDb);
|
||||
setAddonEnabled(testDb, ADDON_IDS.JOURNEY, true);
|
||||
setProviderEnabled('immich', true);
|
||||
setProviderEnabled('synologyphotos', true);
|
||||
await setProviderEnabled('immich', true);
|
||||
await setProviderEnabled('synologyphotos', true);
|
||||
broadcastMock.mockClear();
|
||||
delete process.env.DEMO_MODE;
|
||||
|
||||
@@ -77,7 +81,14 @@ beforeEach(() => {
|
||||
backfillRun.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
let orm: TestOrm;
|
||||
|
||||
beforeAll(async () => {
|
||||
orm = await createTestOrm(testDb);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await orm.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
@@ -146,7 +157,7 @@ describe('Tool: search_provider_photos', () => {
|
||||
|
||||
it('refuses a provider the admin has switched off, without calling it', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
setProviderEnabled('synologyphotos', false);
|
||||
await setProviderEnabled('synologyphotos', false);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'search_provider_photos',
|
||||
@@ -160,7 +171,7 @@ describe('Tool: search_provider_photos', () => {
|
||||
|
||||
it('refuses a provider that is not in the provider table at all', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
removeProvider('synologyphotos');
|
||||
await removeProvider('synologyphotos');
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'search_provider_photos',
|
||||
@@ -207,8 +218,8 @@ describe('Tool: search_provider_photos', () => {
|
||||
|
||||
it('is not registered when every photo provider is switched off', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
setProviderEnabled('immich', false);
|
||||
setProviderEnabled('synologyphotos', false);
|
||||
await setProviderEnabled('immich', false);
|
||||
await setProviderEnabled('synologyphotos', false);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const names = (await h.client.listTools()).tools.map(t => t.name);
|
||||
expect(names).not.toContain('search_provider_photos');
|
||||
@@ -245,8 +256,8 @@ describe('Tool: search_provider_photos', () => {
|
||||
|
||||
it('is registered again as soon as one provider is on', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
setProviderEnabled('immich', true);
|
||||
setProviderEnabled('synologyphotos', false);
|
||||
await setProviderEnabled('immich', true);
|
||||
await setProviderEnabled('synologyphotos', false);
|
||||
await withHarness(user.id, async (h) => {
|
||||
expect((await h.client.listTools()).tools.map(t => t.name)).toContain('search_provider_photos');
|
||||
});
|
||||
@@ -288,7 +299,7 @@ describe('Tool: list_provider_albums', () => {
|
||||
|
||||
it('refuses a disabled provider', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
setProviderEnabled('immich', false);
|
||||
await setProviderEnabled('immich', false);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({ name: 'list_provider_albums', arguments: { provider: 'immich' } });
|
||||
expect(result.isError).toBe(true);
|
||||
@@ -337,7 +348,7 @@ describe('Tool: list_provider_album_photos', () => {
|
||||
|
||||
it('refuses a disabled provider', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
setProviderEnabled('synologyphotos', false);
|
||||
await setProviderEnabled('synologyphotos', false);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'list_provider_album_photos', arguments: { provider: 'synologyphotos', album_id: '7' },
|
||||
@@ -364,15 +375,29 @@ describe('Tool: list_provider_album_photos', () => {
|
||||
// add_journey_provider_photos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function entryPhotoRows(entryId: number) {
|
||||
return testDb.prepare(`
|
||||
SELECT tp.provider, tp.asset_id, tp.owner_id, tp.media_type, gp.journey_id, gp.caption
|
||||
FROM journey_entry_photos jep
|
||||
JOIN journey_photos gp ON gp.id = jep.journey_photo_id
|
||||
JOIN trek_photos tp ON tp.id = gp.photo_id
|
||||
WHERE jep.entry_id = ?
|
||||
ORDER BY tp.asset_id
|
||||
`).all(entryId) as Array<Record<string, unknown>>;
|
||||
/** The provider photo behind each gallery row, the way the journey_photos to trek_photos join reads it. */
|
||||
async function withTrekPhotos(galleryRows: Array<{ photo_id?: number; journey_id?: number; caption?: string | null }>) {
|
||||
const rows = [];
|
||||
for (const gp of galleryRows) {
|
||||
const tp = await findRow(orm, TrekPhotos, { id: gp.photo_id });
|
||||
if (tp) rows.push({ gp, tp });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function entryPhotoRows(entryId: number) {
|
||||
const links = await findRows(orm, JourneyEntryPhotos, { entry: entryId });
|
||||
const gallery = [];
|
||||
for (const link of links) {
|
||||
const gp = await findRow(orm, JourneyPhotos, { id: link.journey_photo_id });
|
||||
if (gp) gallery.push(gp);
|
||||
}
|
||||
return (await withTrekPhotos(gallery))
|
||||
.map(({ gp, tp }) => ({
|
||||
provider: tp.provider, asset_id: tp.asset_id, owner_id: tp.owner_id, media_type: tp.media_type,
|
||||
journey_id: gp.journey_id, caption: gp.caption,
|
||||
}))
|
||||
.sort((a, b) => ((a.asset_id ?? '') < (b.asset_id ?? '') ? -1 : (a.asset_id ?? '') > (b.asset_id ?? '') ? 1 : 0));
|
||||
}
|
||||
|
||||
describe('Tool: add_journey_provider_photos', () => {
|
||||
@@ -393,7 +418,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
expect(data.added).toBe(2);
|
||||
expect(data.skipped).toBe(0);
|
||||
|
||||
const rows = entryPhotoRows(entry.id);
|
||||
const rows = await entryPhotoRows(entry.id);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows.map(r => r.asset_id)).toEqual(['a1', 'a2']);
|
||||
expect(rows.every(r => r.provider === 'immich' && r.owner_id === user.id)).toBe(true);
|
||||
@@ -414,11 +439,10 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
})) as any;
|
||||
expect(data.added).toBe(1);
|
||||
|
||||
const gallery = testDb.prepare(
|
||||
'SELECT tp.asset_id, tp.provider FROM journey_photos gp JOIN trek_photos tp ON tp.id = gp.photo_id WHERE gp.journey_id = ?',
|
||||
).all(journey.id) as Array<Record<string, unknown>>;
|
||||
const gallery = (await withTrekPhotos(await findRows(orm, JourneyPhotos, { journey: journey.id })))
|
||||
.map(({ tp }) => ({ asset_id: tp.asset_id, provider: tp.provider }));
|
||||
expect(gallery).toEqual([{ asset_id: 'g1', provider: 'synologyphotos' }]);
|
||||
expect(entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
expect(await entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -432,7 +456,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
const second = parseToolResult(await h.client.callTool({ name: 'add_journey_provider_photos', arguments: args })) as any;
|
||||
expect(second.added).toBe(0);
|
||||
expect(second.skipped).toBe(1);
|
||||
expect(entryPhotoRows(entry.id)).toHaveLength(1);
|
||||
expect(await entryPhotoRows(entry.id)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -445,7 +469,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
name: 'add_journey_provider_photos',
|
||||
arguments: { journeyId: journey.id, entryId: entry.id, provider: 'immich', asset_ids: ['bf-1'] },
|
||||
});
|
||||
const photoId = (testDb.prepare('SELECT id FROM trek_photos WHERE asset_id = ? AND owner_id = ?').get('bf-1', user.id) as { id: number }).id;
|
||||
const photoId = (await findRow(orm, TrekPhotos, { asset_id: 'bf-1', owner: user.id }))?.id;
|
||||
expect(backfillRun).toHaveBeenCalledWith([photoId], user.id);
|
||||
});
|
||||
});
|
||||
@@ -481,7 +505,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect((result as any).content[0].text).toBe('Journey not found or access denied.');
|
||||
expect(entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
expect(await entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -509,7 +533,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect((result as any).content[0].text).toBe('Entry not found in this journey.');
|
||||
expect(entryPhotoRows(foreignEntry.id)).toHaveLength(0);
|
||||
expect(await entryPhotoRows(foreignEntry.id)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -524,7 +548,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
arguments: { journeyId: journey.id, entryId: entry.id, provider: 'immich', asset_ids: ['x1'] },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
expect(await entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,7 +565,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
},
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
expect(await entryPhotoRows(entry.id)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -554,7 +578,7 @@ describe('Tool: add_journey_provider_photos', () => {
|
||||
arguments: { journeyId: journey.id, provider: 'local', asset_ids: ['x1'] },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(testDb.prepare('SELECT COUNT(*) c FROM journey_photos').get()).toEqual({ c: 0 });
|
||||
expect(await countRows(orm, JourneyPhotos)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ import { GoogleApiUsage } from '../../../src/db/entities/GoogleApiUsage.entity';
|
||||
import type { GoogleApiUsageRepository } from '../../../src/db/repositories/GoogleApiUsage.repository';
|
||||
import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository';
|
||||
import { createTestAppSettingsRepo, sharedTestOrm } from '../../helpers/test-uow';
|
||||
import { countRows, deleteRows, findRow, insertRow, insertRows } from '../../helpers/factories/rows';
|
||||
import { readAppSetting } from '../../helpers/factories/settings';
|
||||
import { AppSettings } from '../../../src/db/entities/AppSettings.entity';
|
||||
|
||||
/** The UTC calendar day `days` before today, the text SQLite's date('now', '-N days') gives. */
|
||||
const utcDaysAgo = (days: number): string => new Date(Date.now() - days * 86_400_000).toISOString().slice(0, 10);
|
||||
|
||||
/** GQUOTA-001..007 — the daily ceiling on Google API calls (#1582). */
|
||||
|
||||
@@ -24,9 +30,9 @@ describe('GoogleQuotaService', () => {
|
||||
usage = (await sharedTestOrm(db)).repo(GoogleApiUsage);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
db.exec('DELETE FROM google_api_usage');
|
||||
db.prepare('DELETE FROM app_settings WHERE key = ?').run(GOOGLE_DAILY_LIMIT_SETTING);
|
||||
await deleteRows(await sharedTestOrm(db), AppSettings, { key: GOOGLE_DAILY_LIMIT_SETTING });
|
||||
// A fresh service per case, so the once-a-day warning state starts clean.
|
||||
quota = new GoogleQuotaService(appSettings, usage);
|
||||
});
|
||||
@@ -57,29 +63,32 @@ describe('GoogleQuotaService', () => {
|
||||
|
||||
it('GQUOTA-003: yesterday does not count against today', async () => {
|
||||
await quota.setDailyLimit(1);
|
||||
db.prepare("INSERT INTO google_api_usage (day, calls) VALUES (date('now', '-1 day'), 50)").run();
|
||||
await insertRow(await sharedTestOrm(db), GoogleApiUsage, { day: utcDaysAgo(1), calls: 50 });
|
||||
expect(await quota.usedToday()).toBe(0);
|
||||
expect(await quota.exhausted()).toBe(false);
|
||||
});
|
||||
|
||||
it('GQUOTA-004: 0 and null remove the ceiling', async () => {
|
||||
await quota.setDailyLimit(10);
|
||||
expect(db.prepare('SELECT value FROM app_settings WHERE key = ?').get(GOOGLE_DAILY_LIMIT_SETTING)).toEqual({ value: '10' });
|
||||
expect(await readAppSetting(await sharedTestOrm(db), GOOGLE_DAILY_LIMIT_SETTING)).toBe('10');
|
||||
expect((await quota.setDailyLimit(0)).daily_limit).toBeNull();
|
||||
await quota.setDailyLimit(10);
|
||||
expect((await quota.setDailyLimit(null)).daily_limit).toBeNull();
|
||||
expect(db.prepare('SELECT value FROM app_settings WHERE key = ?').get(GOOGLE_DAILY_LIMIT_SETTING)).toBeUndefined();
|
||||
expect(await findRow(await sharedTestOrm(db), AppSettings, { key: GOOGLE_DAILY_LIMIT_SETTING })).toBeNull();
|
||||
});
|
||||
|
||||
it('GQUOTA-005: a stored value that is not a positive number means no ceiling', async () => {
|
||||
db.prepare('INSERT INTO app_settings (key, value) VALUES (?, ?)').run(GOOGLE_DAILY_LIMIT_SETTING, 'lots');
|
||||
await insertRow(await sharedTestOrm(db), AppSettings, { key: GOOGLE_DAILY_LIMIT_SETTING, value: 'lots' });
|
||||
expect(await quota.dailyLimit()).toBeNull();
|
||||
});
|
||||
|
||||
it('GQUOTA-006: the status prunes days past the retention window', async () => {
|
||||
db.prepare("INSERT INTO google_api_usage (day, calls) VALUES (date('now', '-500 days'), 9), (date('now', '-10 days'), 4)").run();
|
||||
await insertRows(await sharedTestOrm(db), GoogleApiUsage, [
|
||||
{ day: utcDaysAgo(500), calls: 9 },
|
||||
{ day: utcDaysAgo(10), calls: 4 },
|
||||
]);
|
||||
await quota.status();
|
||||
expect(db.prepare('SELECT COUNT(*) AS n FROM google_api_usage').get()).toEqual({ n: 1 });
|
||||
expect(await countRows(await sharedTestOrm(db), GoogleApiUsage)).toBe(1);
|
||||
});
|
||||
|
||||
it('GQUOTA-007: raising the ceiling reopens the day at once', async () => {
|
||||
|
||||
@@ -21,7 +21,12 @@ import { McpToolGuardsService } from '../../../src/nest/mcp-shared/mcp-tool-guar
|
||||
import { McpSharedModule } from '../../../src/nest/mcp-shared/mcp-shared.module';
|
||||
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { createTestUnitOfWork, createTestAppSettingsRepo, createTestTripsRepo, createTestUsersRepo } from '../../helpers/test-uow';
|
||||
import { createTestUnitOfWork, createTestAppSettingsRepo, createTestTripsRepo, createTestUsersRepo, sharedTestOrm } from '../../helpers/test-uow';
|
||||
import { deleteRows, updateRows } from '../../helpers/factories/rows';
|
||||
import { makeTrip } from '../../helpers/factories/trips';
|
||||
import { TripMembers } from '../../../src/db/entities/TripMembers.entity';
|
||||
import { Trips } from '../../../src/db/entities/Trips.entity';
|
||||
import { Users } from '../../../src/db/entities/Users.entity';
|
||||
|
||||
let svc: McpToolGuardsService;
|
||||
beforeAll(async () => {
|
||||
@@ -35,16 +40,16 @@ beforeAll(async () => {
|
||||
);
|
||||
});
|
||||
|
||||
function createTrip(ownerId: number): number {
|
||||
const r = testDb.prepare("INSERT INTO trips (user_id, title) VALUES (?, 'T')").run(ownerId);
|
||||
return Number(r.lastInsertRowid);
|
||||
async function createTrip(ownerId: number): Promise<number> {
|
||||
return (await makeTrip(await sharedTestOrm(testDb), ownerId, { title: 'T' })).id;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
testDb.prepare('DELETE FROM trip_members').run();
|
||||
testDb.prepare('DELETE FROM trips').run();
|
||||
testDb.prepare('DELETE FROM users').run();
|
||||
const orm = await sharedTestOrm(testDb);
|
||||
await deleteRows(orm, TripMembers);
|
||||
await deleteRows(orm, Trips);
|
||||
await deleteRows(orm, Users);
|
||||
});
|
||||
|
||||
describe('hasTripPermission', () => {
|
||||
@@ -56,22 +61,22 @@ describe('hasTripPermission', () => {
|
||||
it('GRD-002: the owner passes owner-level actions; a stranger does not', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: stranger } = createUser(testDb);
|
||||
const tripId = createTrip(owner.id);
|
||||
const tripId = await createTrip(owner.id);
|
||||
expect(await svc.hasTripPermission('trip_delete', tripId, owner.id)).toBe(true);
|
||||
expect(await svc.hasTripPermission('trip_delete', tripId, stranger.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('GRD-003: an unknown user falls back to the plain user role', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const tripId = createTrip(owner.id);
|
||||
const tripId = await createTrip(owner.id);
|
||||
expect(await svc.hasTripPermission('trip_delete', tripId, 424242)).toBe(false);
|
||||
});
|
||||
|
||||
it('GRD-004: a global admin passes regardless of membership', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: admin } = createUser(testDb);
|
||||
testDb.prepare("UPDATE users SET role = 'admin' WHERE id = ?").run(admin.id);
|
||||
const tripId = createTrip(owner.id);
|
||||
await updateRows(await sharedTestOrm(testDb), Users, { id: admin.id }, { role: 'admin' });
|
||||
const tripId = await createTrip(owner.id);
|
||||
expect(await svc.hasTripPermission('trip_delete', tripId, admin.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -80,7 +85,7 @@ describe('isAdminUser', () => {
|
||||
it('GRD-010: reflects the users.role column and is false for unknown ids', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
expect(await svc.isAdminUser(user.id)).toBe(false);
|
||||
testDb.prepare("UPDATE users SET role = 'admin' WHERE id = ?").run(user.id);
|
||||
await updateRows(await sharedTestOrm(testDb), Users, { id: user.id }, { role: 'admin' });
|
||||
expect(await svc.isAdminUser(user.id)).toBe(true);
|
||||
expect(await svc.isAdminUser(424242)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,11 @@ import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, createPlace, createReservation, addTripMember } from '../../helpers/factories';
|
||||
import { AtlasService } from '../../../src/nest/atlas/atlas.service';
|
||||
import { createTestUnitOfWork, createTestTripsRepo, createTestPlacesRepo, createTestReservationEndpointsRepo, sharedTestOrm } from '../../helpers/test-uow';
|
||||
import { insertRow, upsertRow } from '../../helpers/factories/rows';
|
||||
import { addReservationTraveler, makeReservationEndpoint } from '../../helpers/factories/reservations';
|
||||
import { PlaceRegions } from '../../../src/db/entities/PlaceRegions.entity';
|
||||
import { Places } from '../../../src/db/entities/Places.entity';
|
||||
import { VisitedCountries } from '../../../src/db/entities/VisitedCountries.entity';
|
||||
import {
|
||||
createTestBucketListRepo,
|
||||
createTestHiddenCountriesRepo,
|
||||
@@ -63,7 +68,7 @@ afterAll(() => { testDb.close(); });
|
||||
// ── getTravelStats — dashboard passport card ────────────────────────────────
|
||||
|
||||
describe('getTravelStats', () => {
|
||||
function endpoint(
|
||||
async function endpoint(
|
||||
reservationId: number,
|
||||
role: 'from' | 'to' | 'stop',
|
||||
sequence: number,
|
||||
@@ -73,9 +78,9 @@ describe('getTravelStats', () => {
|
||||
localDate: string | null = null,
|
||||
localTime: string | null = null
|
||||
) {
|
||||
testDb.prepare(
|
||||
'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, lat, lng, code, local_date, local_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(reservationId, role, sequence, `Endpoint ${sequence}`, lat, lng, code, localDate, localTime);
|
||||
await makeReservationEndpoint(await sharedTestOrm(testDb), reservationId, {
|
||||
role, sequence, name: `Endpoint ${sequence}`, lat, lng, code, local_date: localDate, local_time: localTime,
|
||||
});
|
||||
}
|
||||
|
||||
// Every trip below is dated in the past: since #1048 the passport card only counts
|
||||
@@ -88,8 +93,8 @@ describe('getTravelStats', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Tokyo Trip', start_date: PAST_START, end_date: PAST_END });
|
||||
const res = createReservation(testDb, trip.id, { type: 'flight' });
|
||||
endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
endpoint(res.id, 'to', 1, 35.6762, 139.6503); // Tokyo
|
||||
await endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
await endpoint(res.id, 'to', 1, 35.6762, 139.6503); // Tokyo
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
expect(stats.countries).toContain('BE');
|
||||
@@ -102,9 +107,9 @@ describe('getTravelStats', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Connection Trip', start_date: PAST_START, end_date: PAST_END });
|
||||
const res = createReservation(testDb, trip.id, { type: 'flight' });
|
||||
endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
endpoint(res.id, 'stop', 1, 35.6762, 139.6503); // Tokyo — never leaves the airport
|
||||
endpoint(res.id, 'to', 2, -33.8688, 151.2093); // Sydney
|
||||
await endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
await endpoint(res.id, 'stop', 1, 35.6762, 139.6503); // Tokyo — never leaves the airport
|
||||
await endpoint(res.id, 'to', 2, -33.8688, 151.2093); // Sydney
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
expect(stats.countries).toContain('BE');
|
||||
@@ -116,8 +121,8 @@ describe('getTravelStats', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Tokyo Trip', start_date: PAST_START, end_date: PAST_END });
|
||||
const res = createReservation(testDb, trip.id, { type: 'flight' });
|
||||
endpoint(res.id, 'from', 0, 50.9014, 4.4844);
|
||||
endpoint(res.id, 'to', 1, 35.6762, 139.6503);
|
||||
await endpoint(res.id, 'from', 0, 50.9014, 4.4844);
|
||||
await endpoint(res.id, 'to', 1, 35.6762, 139.6503);
|
||||
|
||||
expect((await atlas.getTravelStats(user.id)).countries).toContain('JP');
|
||||
|
||||
@@ -132,19 +137,19 @@ describe('getTravelStats', () => {
|
||||
// Relative offsets, not literal dates — a hardcoded "future" date expires.
|
||||
const iso = (days: number) => new Date(Date.now() + days * 86_400_000).toISOString().slice(0, 10);
|
||||
|
||||
function placeInRegion(tripId: number, countryCode: string, regionCode: string) {
|
||||
async function placeInRegion(tripId: number, countryCode: string, regionCode: string) {
|
||||
const place = createPlace(testDb, tripId, { name: `Place in ${countryCode}` });
|
||||
testDb
|
||||
.prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)')
|
||||
.run(place.id, countryCode, regionCode, regionCode);
|
||||
await upsertRow(await sharedTestOrm(testDb), PlaceRegions, {
|
||||
place: place.id, country_code: countryCode, region_code: regionCode, region_name: regionCode,
|
||||
});
|
||||
}
|
||||
|
||||
it('AUTH-DB-094: #1048 a place in a future trip does not stamp its country; a past trip does', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const past = createTrip(testDb, user.id, { title: 'Paris, last month', start_date: iso(-40), end_date: iso(-30) });
|
||||
const future = createTrip(testDb, user.id, { title: 'Tokyo, next month', start_date: iso(30), end_date: iso(40) });
|
||||
placeInRegion(past.id, 'FR', 'FR-75');
|
||||
placeInRegion(future.id, 'JP', 'JP-13');
|
||||
await placeInRegion(past.id, 'FR', 'FR-75');
|
||||
await placeInRegion(future.id, 'JP', 'JP-13');
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
|
||||
@@ -155,9 +160,9 @@ describe('getTravelStats', () => {
|
||||
it('AUTH-DB-095: #1048 a trip with no dates at all stamps nothing', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const dateless = createTrip(testDb, user.id, { title: 'Someday: Japan' });
|
||||
placeInRegion(dateless.id, 'JP', 'JP-13');
|
||||
await placeInRegion(dateless.id, 'JP', 'JP-13');
|
||||
const res = createReservation(testDb, dateless.id, { type: 'flight' });
|
||||
endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
await endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
|
||||
@@ -168,8 +173,8 @@ describe('getTravelStats', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const future = createTrip(testDb, user.id, { title: 'Tokyo, next month', start_date: iso(30), end_date: iso(40) });
|
||||
const res = createReservation(testDb, future.id, { type: 'flight' });
|
||||
endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
endpoint(res.id, 'to', 1, 35.6762, 139.6503); // Tokyo
|
||||
await endpoint(res.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
await endpoint(res.id, 'to', 1, 35.6762, 139.6503); // Tokyo
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
|
||||
@@ -182,19 +187,19 @@ describe('getTravelStats', () => {
|
||||
// same pairing: a hub stored as the 'to' of one booking and the 'from' of the next
|
||||
// slips past the #1486 role filter. Brussels → Helsinki → New York.
|
||||
|
||||
function splitChainThroughHelsinki(tripId: number, onwardDate: string, onwardTime: string) {
|
||||
async function splitChainThroughHelsinki(tripId: number, onwardDate: string, onwardTime: string) {
|
||||
const inbound = createReservation(testDb, tripId, { type: 'flight', title: 'BRU-HEL' });
|
||||
endpoint(inbound.id, 'from', 0, 50.9014, 4.4844, 'BRU', '2023-05-01', '07:00');
|
||||
endpoint(inbound.id, 'to', 1, 60.3172, 24.9633, 'HEL', '2023-05-01', '09:30');
|
||||
await endpoint(inbound.id, 'from', 0, 50.9014, 4.4844, 'BRU', '2023-05-01', '07:00');
|
||||
await endpoint(inbound.id, 'to', 1, 60.3172, 24.9633, 'HEL', '2023-05-01', '09:30');
|
||||
const onward = createReservation(testDb, tripId, { type: 'flight', title: 'HEL-JFK' });
|
||||
endpoint(onward.id, 'from', 0, 60.3172, 24.9633, 'HEL', onwardDate, onwardTime);
|
||||
endpoint(onward.id, 'to', 1, 40.6413, -73.7781, 'JFK', onwardDate, '15:00');
|
||||
await endpoint(onward.id, 'from', 0, 60.3172, 24.9633, 'HEL', onwardDate, onwardTime);
|
||||
await endpoint(onward.id, 'to', 1, 40.6413, -73.7781, 'JFK', onwardDate, '15:00');
|
||||
}
|
||||
|
||||
it('AUTH-DB-099: #1535 a plane change booked as two flights does not stamp the hub country', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'New York via Helsinki', start_date: PAST_START, end_date: PAST_END });
|
||||
splitChainThroughHelsinki(trip.id, '2023-05-01', '11:00');
|
||||
await splitChainThroughHelsinki(trip.id, '2023-05-01', '11:00');
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
expect(stats.countries).toContain('BE');
|
||||
@@ -205,7 +210,7 @@ describe('getTravelStats', () => {
|
||||
it('AUTH-DB-100: #1535 a stopover of two days still stamps the hub country', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Helsinki stopover', start_date: PAST_START, end_date: PAST_END });
|
||||
splitChainThroughHelsinki(trip.id, '2023-05-03', '11:00');
|
||||
await splitChainThroughHelsinki(trip.id, '2023-05-03', '11:00');
|
||||
|
||||
expect((await atlas.getTravelStats(user.id)).countries).toContain('FI');
|
||||
});
|
||||
@@ -213,7 +218,7 @@ describe('getTravelStats', () => {
|
||||
it('AUTH-DB-101: #1490 removing a country still subtracts it around the layover pairing', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'New York via Helsinki', start_date: PAST_START, end_date: PAST_END });
|
||||
splitChainThroughHelsinki(trip.id, '2023-05-01', '11:00');
|
||||
await splitChainThroughHelsinki(trip.id, '2023-05-01', '11:00');
|
||||
|
||||
await atlas.unmarkCountry(user.id, 'BE');
|
||||
|
||||
@@ -227,8 +232,8 @@ describe('getTravelStats', () => {
|
||||
// filter must not reach it.
|
||||
const { user } = createUser(testDb);
|
||||
const future = createTrip(testDb, user.id, { title: 'Tokyo, next month', start_date: iso(30), end_date: iso(40) });
|
||||
placeInRegion(future.id, 'JP', 'JP-13');
|
||||
testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP');
|
||||
await placeInRegion(future.id, 'JP', 'JP-13');
|
||||
await insertRow(await sharedTestOrm(testDb), VisitedCountries, { user: user.id, country_code: 'JP' });
|
||||
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
|
||||
@@ -240,7 +245,7 @@ describe('travel-stats quirk fixes', () => {
|
||||
it('AUTH-DB-089: getTravelStats keeps a place at lat 0 / lng 0 (equator, prime meridian)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Null Island' });
|
||||
testDb.prepare('INSERT INTO places (trip_id, name, lat, lng) VALUES (?, ?, ?, ?)').run(trip.id, 'Null Island', 0, 0);
|
||||
await insertRow(await sharedTestOrm(testDb), Places, { trip: trip.id, name: 'Null Island', lat: 0, lng: 0 });
|
||||
const stats = await atlas.getTravelStats(user.id);
|
||||
expect(stats.coords).toContainEqual({ lat: 0, lng: 0 });
|
||||
});
|
||||
@@ -264,36 +269,36 @@ describe('personal figures on a shared trip (#1966)', () => {
|
||||
const PAST_START = '2023-05-01';
|
||||
const PAST_END = '2023-05-10';
|
||||
|
||||
const endpoint = (reservationId: number, role: 'from' | 'to', sequence: number, lat: number, lng: number, code: string) =>
|
||||
testDb.prepare(
|
||||
'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, lat, lng, code) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(reservationId, role, sequence, `Endpoint ${sequence}`, lat, lng, code);
|
||||
const endpoint = async (reservationId: number, role: 'from' | 'to', sequence: number, lat: number, lng: number, code: string) =>
|
||||
makeReservationEndpoint(await sharedTestOrm(testDb), reservationId, {
|
||||
role, sequence, name: `Endpoint ${sequence}`, lat, lng, code,
|
||||
});
|
||||
|
||||
const assignTo = (reservationId: number, userId: number) =>
|
||||
testDb.prepare('INSERT INTO reservation_travelers (reservation_id, user_id) VALUES (?, ?)').run(reservationId, userId);
|
||||
const assignTo = async (reservationId: number, userId: number) =>
|
||||
addReservationTraveler(await sharedTestOrm(testDb), reservationId, userId);
|
||||
|
||||
/** A shared trip: A flies Rome→New York, B flies Mexico City→New York. */
|
||||
function sharedTrip() {
|
||||
async function sharedTrip() {
|
||||
const { user: alice } = createUser(testDb);
|
||||
const { user: bob } = createUser(testDb);
|
||||
const trip = createTrip(testDb, alice.id, { title: 'Shared', start_date: PAST_START, end_date: PAST_END });
|
||||
addTripMember(testDb, trip.id, bob.id);
|
||||
|
||||
const fromRome = createReservation(testDb, trip.id, { type: 'flight', title: 'FCO-JFK' });
|
||||
endpoint(fromRome.id, 'from', 0, 41.8003, 12.2389, 'FCO');
|
||||
endpoint(fromRome.id, 'to', 1, 40.6413, -73.7781, 'JFK');
|
||||
await endpoint(fromRome.id, 'from', 0, 41.8003, 12.2389, 'FCO');
|
||||
await endpoint(fromRome.id, 'to', 1, 40.6413, -73.7781, 'JFK');
|
||||
|
||||
const fromMexico = createReservation(testDb, trip.id, { type: 'flight', title: 'MEX-JFK' });
|
||||
endpoint(fromMexico.id, 'from', 0, 19.4363, -99.0721, 'MEX');
|
||||
endpoint(fromMexico.id, 'to', 1, 40.6413, -73.7781, 'JFK');
|
||||
await endpoint(fromMexico.id, 'from', 0, 19.4363, -99.0721, 'MEX');
|
||||
await endpoint(fromMexico.id, 'to', 1, 40.6413, -73.7781, 'JFK');
|
||||
|
||||
return { alice, bob, trip, fromRome, fromMexico };
|
||||
}
|
||||
|
||||
it('credits each traveler with their own flight only', async () => {
|
||||
const { alice, bob, fromRome, fromMexico } = sharedTrip();
|
||||
assignTo(fromRome.id, alice.id);
|
||||
assignTo(fromMexico.id, bob.id);
|
||||
const { alice, bob, fromRome, fromMexico } = await sharedTrip();
|
||||
await assignTo(fromRome.id, alice.id);
|
||||
await assignTo(fromMexico.id, bob.id);
|
||||
|
||||
const a = await atlas.getTravelStats(alice.id);
|
||||
const b = await atlas.getTravelStats(bob.id);
|
||||
@@ -306,9 +311,9 @@ describe('personal figures on a shared trip (#1966)', () => {
|
||||
});
|
||||
|
||||
it('does not stamp another traveler s departure country on your passport', async () => {
|
||||
const { alice, bob, fromRome, fromMexico } = sharedTrip();
|
||||
assignTo(fromRome.id, alice.id);
|
||||
assignTo(fromMexico.id, bob.id);
|
||||
const { alice, bob, fromRome, fromMexico } = await sharedTrip();
|
||||
await assignTo(fromRome.id, alice.id);
|
||||
await assignTo(fromMexico.id, bob.id);
|
||||
|
||||
// Alice never went through Mexico.
|
||||
expect((await atlas.getTravelStats(alice.id)).countries).not.toContain('MX');
|
||||
@@ -320,7 +325,7 @@ describe('personal figures on a shared trip (#1966)', () => {
|
||||
* is empty on every install upgrading from 3.4.1.
|
||||
*/
|
||||
it('still counts a booking nobody is named on, for everyone on the trip', async () => {
|
||||
const { alice, bob } = sharedTrip();
|
||||
const { alice, bob } = await sharedTrip();
|
||||
|
||||
const a = await atlas.getTravelStats(alice.id);
|
||||
const b = await atlas.getTravelStats(bob.id);
|
||||
@@ -329,11 +334,11 @@ describe('personal figures on a shared trip (#1966)', () => {
|
||||
});
|
||||
|
||||
it('counts a booking they are both named on, once for each of them', async () => {
|
||||
const { alice, bob, fromRome, fromMexico } = sharedTrip();
|
||||
assignTo(fromRome.id, alice.id);
|
||||
assignTo(fromRome.id, bob.id);
|
||||
assignTo(fromMexico.id, alice.id);
|
||||
assignTo(fromMexico.id, bob.id);
|
||||
const { alice, bob, fromRome, fromMexico } = await sharedTrip();
|
||||
await assignTo(fromRome.id, alice.id);
|
||||
await assignTo(fromRome.id, bob.id);
|
||||
await assignTo(fromMexico.id, alice.id);
|
||||
await assignTo(fromMexico.id, bob.id);
|
||||
|
||||
const a = await atlas.getTravelStats(alice.id);
|
||||
const b = await atlas.getTravelStats(bob.id);
|
||||
@@ -342,8 +347,8 @@ describe('personal figures on a shared trip (#1966)', () => {
|
||||
});
|
||||
|
||||
it('leaves a traveler assigned nothing with the unassigned bookings only', async () => {
|
||||
const { alice, bob, fromRome } = sharedTrip();
|
||||
assignTo(fromRome.id, bob.id);
|
||||
const { alice, bob, fromRome } = await sharedTrip();
|
||||
await assignTo(fromRome.id, bob.id);
|
||||
|
||||
// Alice keeps the Mexico flight (nobody named) but loses the Rome one.
|
||||
const a = await atlas.getTravelStats(alice.id);
|
||||
|
||||
Reference in New Issue
Block a user