mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(reservations): answer an id that resolves to nothing, instead of crashing
The scoping guard asked whether a referenced id belongs to a different trip. An id that belongs to no row at all cannot, so it passed, reached the raw INSERT/UPDATE, and SQLite's own foreign-key enforcement threw. Nothing caught it, so a stale place_id from a client-side race came back as a bare 500 with no actionable message — where the same route answers a clean 400 for an id that does exist somewhere else. A second, disjoint guard now names the ids that resolve to nothing and answers 400. It deliberately does not cover accommodation_id: that column has no row to find once a shortened trip cascades its stay away, and refusing the write there would make the booking permanently uneditable. The controller guard reads the request body, which is not enough on its own — an update rebinds whatever the row already held, so a stored id whose row is gone reaches SQL untouched. day_id, end_day_id, place_id and assignment_id are all declared ON DELETE SET NULL, so a reference that resolves to nothing is precisely the state that cascade leaves behind and is now cleared to NULL, the same way a dangling accommodation_id already heals. day_accommodations' day columns are NOT NULL and have nothing to heal to, so those are refused. The plugin surface ran neither guard. It now refuses an unresolvable id, which turns a crash into an error; it still accepts an id from another trip, because taking that away belongs to the plugin contract and not to a bug fix. The reservation MCP tools already validated every referenced id against the trip. Closes #2355
This commit is contained in:
@@ -27,6 +27,10 @@ const { db } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
const tmp = new Database(':memory:');
|
||||
tmp.exec('PRAGMA journal_mode = WAL');
|
||||
// What production runs (db/database.ts) and what createTestDb gives every
|
||||
// unit suite. Without it the reservation foreign keys are inert here, and an
|
||||
// id that resolves to nothing passes the mount unnoticed.
|
||||
tmp.exec('PRAGMA foreign_keys = ON');
|
||||
return { db: tmp };
|
||||
});
|
||||
|
||||
@@ -146,6 +150,54 @@ describe('Reservations + accommodations e2e (real auth guard + temp SQLite, real
|
||||
expect(bad.body.error).toContain('title');
|
||||
});
|
||||
|
||||
// The reported repro (#2355): an id that resolves to nothing used to reach
|
||||
// the statement and come back as an unhandled SqliteError, i.e. a bare 500.
|
||||
it('400 on an update whose place_id exists nowhere, and the row is left alone', async () => {
|
||||
const rid = Number(db.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Dinner', 'other')").run(tripId).lastInsertRowid);
|
||||
|
||||
const res = await request(server)
|
||||
.put(`/api/trips/${tripId}/reservations/${rid}`)
|
||||
.set('Cookie', sessionCookie(1))
|
||||
.send({ title: 'Dinner, later', place_id: 999999 });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body).toEqual({ error: 'Unknown reference: place_id' });
|
||||
expect(db.prepare('SELECT title, place_id FROM reservations WHERE id = ?').get(rid)).toEqual({ title: 'Dinner', place_id: null });
|
||||
});
|
||||
|
||||
it('400 on a create whose create_accommodation day exists nowhere, and no stay is written', async () => {
|
||||
const placeId = Number(db.prepare('INSERT INTO places (trip_id, name) VALUES (?, ?)').run(tripId, 'Hotel Unknown').lastInsertRowid);
|
||||
const dayId = Number(db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, 7, ?)').run(tripId, '2026-03-07').lastInsertRowid);
|
||||
const before = db.prepare('SELECT COUNT(*) as c FROM day_accommodations WHERE trip_id = ?').get(tripId);
|
||||
|
||||
const res = await request(server)
|
||||
.post(`/api/trips/${tripId}/reservations`)
|
||||
.set('Cookie', sessionCookie(1))
|
||||
.send({ title: 'Stay', type: 'hotel', create_accommodation: { place_id: placeId, start_day_id: dayId, end_day_id: 999999 } });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body).toEqual({ error: 'Unknown reference: create_accommodation.end_day_id' });
|
||||
expect(db.prepare('SELECT COUNT(*) as c FROM day_accommodations WHERE trip_id = ?').get(tripId)).toEqual(before);
|
||||
});
|
||||
|
||||
// #522, which must survive all of the above: shortening a trip cascades the
|
||||
// stay away and leaves the booking pointing at a gap, and the booking still
|
||||
// has to be savable.
|
||||
it('200 on an update whose stored accommodation_id no longer resolves', async () => {
|
||||
const rid = Number(
|
||||
db.prepare("INSERT INTO reservations (trip_id, title, type, accommodation_id) VALUES (?, 'Stay', 'hotel', 999999)").run(tripId).lastInsertRowid,
|
||||
);
|
||||
|
||||
const res = await request(server)
|
||||
.put(`/api/trips/${tripId}/reservations/${rid}`)
|
||||
.set('Cookie', sessionCookie(1))
|
||||
.send({ title: 'Stay, renamed', accommodation_id: 999999 });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(db.prepare('SELECT title, accommodation_id FROM reservations WHERE id = ?').get(rid))
|
||||
.toEqual({ title: 'Stay, renamed', accommodation_id: null });
|
||||
});
|
||||
|
||||
it('200 list accommodations + 201 create (real insert + auto hotel reservation), 404 on bad refs', async () => {
|
||||
const placeId = Number(db.prepare('INSERT INTO places (trip_id, name) VALUES (?, ?)').run(tripId, 'Grand Hotel').lastInsertRowid);
|
||||
const dayId = Number(db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, 1, ?)').run(tripId, '2026-03-01').lastInsertRowid);
|
||||
|
||||
@@ -16,6 +16,7 @@ function makeService(overrides: Partial<ReservationsService> = {}): Reservations
|
||||
verifyTripAccess: vi.fn().mockReturnValue(trip),
|
||||
canEdit: vi.fn().mockReturnValue(true),
|
||||
referencesOutsideTrip: vi.fn().mockReturnValue([]),
|
||||
unresolvedReferences: vi.fn().mockReturnValue([]),
|
||||
broadcast: vi.fn(),
|
||||
syncBudgetOnCreate: vi.fn(),
|
||||
syncBudgetOnUpdate: vi.fn(),
|
||||
@@ -68,6 +69,30 @@ describe('ReservationsController (parity with the legacy /api/trips/:tripId/rese
|
||||
.toEqual({ status: 400, body: { error: 'Not part of this trip: accommodation_id' } });
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('400s on a body id that exists nowhere, in its own words, without writing', () => {
|
||||
const create = vi.fn();
|
||||
const svc = makeService({
|
||||
create,
|
||||
unresolvedReferences: vi.fn().mockReturnValue(['place_id']),
|
||||
} as Partial<ReservationsService>);
|
||||
const body = { title: 'Hotel', place_id: 4711 };
|
||||
// Not 'Not part of this trip': an id that is part of nothing would send
|
||||
// the caller looking for it on another trip.
|
||||
expect(thrown(() => new ReservationsController(svc, airtrailLink).create(user, '5', body)))
|
||||
.toEqual({ status: 400, body: { error: 'Unknown reference: place_id' } });
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('answers a foreign id with the older message when it is both', () => {
|
||||
const svc = makeService({
|
||||
create: vi.fn(),
|
||||
referencesOutsideTrip: vi.fn().mockReturnValue(['place_id']),
|
||||
unresolvedReferences: vi.fn().mockReturnValue(['place_id']),
|
||||
} as Partial<ReservationsService>);
|
||||
expect(thrown(() => new ReservationsController(svc, airtrailLink).create(user, '5', { title: 'Hotel', place_id: 4711 })))
|
||||
.toEqual({ status: 400, body: { error: 'Not part of this trip: place_id' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /positions', () => {
|
||||
@@ -112,6 +137,20 @@ describe('ReservationsController (parity with the legacy /api/trips/:tripId/rese
|
||||
.toEqual({ status: 400, body: { error: 'Not part of this trip: day_id, place_id' } });
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('400s on a body id that exists nowhere, without writing', () => {
|
||||
const update = vi.fn();
|
||||
const svc = makeService({
|
||||
getReservation: vi.fn().mockReturnValue({ title: 'Old', type: 'lodging' }),
|
||||
update,
|
||||
unresolvedReferences: vi.fn().mockReturnValue(['day_id', 'place_id']),
|
||||
} as Partial<ReservationsService>);
|
||||
// The reporter's request: a foreign-key error used to reach the caller as
|
||||
// a bare 500 here.
|
||||
expect(thrown(() => new ReservationsController(svc, airtrailLink).update(user, '5', '9', { day_id: 1, place_id: 2 })))
|
||||
.toEqual({ status: 400, body: { error: 'Unknown reference: day_id, place_id' } });
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /:id/travelers', () => {
|
||||
|
||||
@@ -43,10 +43,11 @@ const budget = { createBudgetItem: vi.fn(), updateBudgetItem: vi.fn(), deleteBud
|
||||
|
||||
const { notif } = vi.hoisted(() => ({ notif: { send: vi.fn().mockResolvedValue(undefined) } }));
|
||||
|
||||
import { HttpException } from '@nestjs/common';
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, createReservation, createBudgetItem, createPlace, createDay, createDayAccommodation, addTripMember } from '../../helpers/factories';
|
||||
import { createUser, createTrip, createReservation, createBudgetItem, createPlace, createDay, createDayAccommodation, createDayAssignment, addTripMember } from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import type { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
||||
import { ReservationsService } from '../../../src/nest/reservations/reservations.service';
|
||||
@@ -1054,6 +1055,151 @@ describe('ReservationsService — referenced ids stay inside the trip', () => {
|
||||
expect(testDb.prepare('SELECT check_in FROM day_accommodations WHERE id = ?').get(foreignAcc.id)).toEqual({ check_in: '14:00' });
|
||||
expect(testDb.prepare('SELECT accommodation_id FROM reservations WHERE id = ?').get(res.id)).toEqual({ accommodation_id: null });
|
||||
});
|
||||
|
||||
/** A row from before the foreign keys, or from a window where they were off:
|
||||
* the only way to plant one today is the way it got there. */
|
||||
function withForeignKeysOff(plant: () => void) {
|
||||
testDb.exec('PRAGMA foreign_keys = OFF');
|
||||
try { plant(); } finally { testDb.exec('PRAGMA foreign_keys = ON'); }
|
||||
}
|
||||
|
||||
it('RESV-SCOPE-006: an id that resolves to nothing is named too, accommodation_id excepted', () => {
|
||||
const { mine } = twoTrips();
|
||||
const gone = 999999;
|
||||
|
||||
expect(svc.unresolvedReferences(String(mine.id), {
|
||||
title: 'x', type: 'hotel',
|
||||
day_id: gone, end_day_id: gone, place_id: gone, assignment_id: gone, accommodation_id: gone,
|
||||
create_accommodation: { place_id: gone, start_day_id: gone, end_day_id: gone },
|
||||
})).toEqual([
|
||||
'day_id', 'end_day_id', 'place_id', 'assignment_id',
|
||||
'create_accommodation.place_id', 'create_accommodation.start_day_id', 'create_accommodation.end_day_id',
|
||||
]);
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-007: ids on the trip pass, and a falsy one is not a reference at all', () => {
|
||||
const { mine } = twoTrips();
|
||||
const place = createPlace(testDb, mine.id);
|
||||
const day = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').get(mine.id) as { id: number };
|
||||
const assignment = createDayAssignment(testDb, day.id, place.id);
|
||||
|
||||
expect(svc.unresolvedReferences(String(mine.id), {
|
||||
title: 'x', type: 'hotel',
|
||||
day_id: day.id, end_day_id: day.id, place_id: place.id, assignment_id: assignment.id,
|
||||
create_accommodation: { place_id: place.id, start_day_id: day.id, end_day_id: day.id },
|
||||
})).toEqual([]);
|
||||
// The write paths coerce these to NULL before they reach SQL, so naming
|
||||
// them would 400 a body that stores a null today.
|
||||
expect(svc.unresolvedReferences(String(mine.id), { title: 'x', day_id: 0, end_day_id: 0, place_id: 0, assignment_id: 0 })).toEqual([]);
|
||||
// Only a hotel booking writes the stay row.
|
||||
expect(svc.unresolvedReferences(String(mine.id), {
|
||||
title: 'x', type: 'flight', create_accommodation: { place_id: 999999, start_day_id: 999999, end_day_id: 999999 },
|
||||
})).toEqual([]);
|
||||
// A stay without a place is a state the booking form writes.
|
||||
expect(svc.unresolvedReferences(String(mine.id), {
|
||||
title: 'x', type: 'hotel', create_accommodation: { start_day_id: day.id, end_day_id: day.id },
|
||||
})).toEqual([]);
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-008: an id from another trip is named by both guards, so the older one answers first', () => {
|
||||
const { mine, theirs } = twoTrips();
|
||||
const foreignPlace = createPlace(testDb, theirs.id);
|
||||
const body = { title: 'x', place_id: foreignPlace.id };
|
||||
|
||||
expect(svc.referencesOutsideTrip(String(mine.id), body)).toEqual(['place_id']);
|
||||
expect(svc.unresolvedReferences(String(mine.id), body)).toEqual(['place_id']);
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-009: an update heals the stored ids whose rows are gone instead of failing on them', () => {
|
||||
const { mine } = twoTrips();
|
||||
const res = createReservation(testDb, mine.id, { title: 'Dinner' });
|
||||
withForeignKeysOff(() => {
|
||||
testDb.prepare('UPDATE reservations SET day_id = ?, end_day_id = ?, place_id = ?, assignment_id = ? WHERE id = ?')
|
||||
.run(999999, 999998, 999997, 999996, res.id);
|
||||
});
|
||||
const current = svc.getReservation(String(res.id), String(mine.id))!;
|
||||
|
||||
// The body names none of them, so the statement rebinds what the row holds.
|
||||
svc.update(String(res.id), String(mine.id), { title: 'Dinner, later' }, current);
|
||||
|
||||
expect(testDb.prepare('SELECT title, day_id, end_day_id, place_id, assignment_id FROM reservations WHERE id = ?').get(res.id))
|
||||
.toEqual({ title: 'Dinner, later', day_id: null, end_day_id: null, place_id: null, assignment_id: null });
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-010: a supplied id that resolves to nothing is stored as NULL, on create and on update', () => {
|
||||
const { mine } = twoTrips();
|
||||
const { reservation } = svc.create(String(mine.id), {
|
||||
title: 'Dinner', day_id: 999999, end_day_id: 999998, place_id: 999997, assignment_id: 999996,
|
||||
});
|
||||
expect(testDb.prepare('SELECT day_id, end_day_id, place_id, assignment_id FROM reservations WHERE id = ?').get(reservation.id))
|
||||
.toEqual({ day_id: null, end_day_id: null, place_id: null, assignment_id: null });
|
||||
|
||||
const current = svc.getReservation(String(reservation.id), String(mine.id))!;
|
||||
svc.update(String(reservation.id), String(mine.id), { place_id: 999997 }, current);
|
||||
expect(testDb.prepare('SELECT place_id FROM reservations WHERE id = ?').get(reservation.id)).toEqual({ place_id: null });
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-011: a stay whose refs resolve to nothing is refused, never written and never dropped in silence', () => {
|
||||
const { mine } = twoTrips();
|
||||
const place = createPlace(testDb, mine.id);
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').all(mine.id) as { id: number }[];
|
||||
|
||||
expect(() => svc.create(String(mine.id), {
|
||||
title: 'Hotel', type: 'hotel',
|
||||
create_accommodation: { place_id: 999999, start_day_id: 999998, end_day_id: 999997 },
|
||||
})).toThrow('Unknown reference: create_accommodation.place_id, create_accommodation.start_day_id, create_accommodation.end_day_id');
|
||||
expect(testDb.prepare('SELECT COUNT(*) as c FROM day_accommodations WHERE trip_id = ?').get(mine.id)).toEqual({ c: 0 });
|
||||
expect(testDb.prepare('SELECT COUNT(*) as c FROM reservations WHERE trip_id = ?').get(mine.id)).toEqual({ c: 0 });
|
||||
|
||||
// On an existing stay the edit is refused rather than skipped: dropping it
|
||||
// would leave the caller with a 200 and the old dates.
|
||||
const acc = createDayAccommodation(testDb, mine.id, place.id, days[0].id, days[1].id);
|
||||
const res = createReservation(testDb, mine.id, { title: 'Hotel', type: 'hotel' });
|
||||
testDb.prepare('UPDATE reservations SET accommodation_id = ? WHERE id = ?').run(acc.id, res.id);
|
||||
const current = svc.getReservation(String(res.id), String(mine.id))!;
|
||||
|
||||
expect(() => svc.update(String(res.id), String(mine.id), {
|
||||
type: 'hotel', create_accommodation: { place_id: place.id, start_day_id: days[0].id, end_day_id: 999999 },
|
||||
}, current)).toThrow('Unknown reference: create_accommodation.end_day_id');
|
||||
expect(testDb.prepare('SELECT end_day_id FROM day_accommodations WHERE id = ?').get(acc.id)).toEqual({ end_day_id: days[1].id });
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-012: a stay booked without a place is still written', () => {
|
||||
const { mine } = twoTrips();
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').all(mine.id) as { id: number }[];
|
||||
|
||||
const { reservation, accommodationCreated } = svc.create(String(mine.id), {
|
||||
title: 'Hotel', type: 'hotel',
|
||||
create_accommodation: { start_day_id: days[0].id, end_day_id: days[1].id, check_in: '15:00' },
|
||||
});
|
||||
|
||||
expect(accommodationCreated).toBe(true);
|
||||
expect(testDb.prepare('SELECT place_id, start_day_id FROM day_accommodations WHERE id = ?').get(Number(reservation.accommodation_id)))
|
||||
.toEqual({ place_id: null, start_day_id: days[0].id });
|
||||
});
|
||||
|
||||
it('RESV-SCOPE-013: the refusal carries the 400 the write surfaces send, not a status the filter has to guess', () => {
|
||||
const { mine } = twoTrips();
|
||||
|
||||
// The write surfaces name the field before the service ever sees it, so the
|
||||
// class only matters to whoever reaches this floor without one. It answers
|
||||
// as they do — the filter passes a 400 through and keeps the message, where
|
||||
// a class it cannot place would become 'Internal server error' (#2355).
|
||||
let thrown: unknown;
|
||||
try {
|
||||
svc.create(String(mine.id), {
|
||||
title: 'Hotel', type: 'hotel',
|
||||
create_accommodation: { start_day_id: 999998, end_day_id: 999997 },
|
||||
});
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
expect(thrown).toBeInstanceOf(HttpException);
|
||||
expect((thrown as HttpException).getStatus()).toBe(400);
|
||||
expect((thrown as HttpException).message)
|
||||
.toBe('Unknown reference: create_accommodation.start_day_id, create_accommodation.end_day_id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the day stop a hotel booking implies', () => {
|
||||
|
||||
@@ -195,6 +195,8 @@ describe('service errors are translated into the RPC taxonomy', () => {
|
||||
getReservation: vi.fn(() => ({ id: 5, title: 'Hotel', type: 'lodging' })),
|
||||
update: vi.fn(() => ({ reservation: { id: 5 }, accommodationChanged: false })),
|
||||
syncBudgetOnUpdate: vi.fn(),
|
||||
referencesOutsideTrip: vi.fn(() => []),
|
||||
unresolvedReferences: vi.fn(() => []),
|
||||
notifyBookingChange,
|
||||
} as never;
|
||||
const host = new PluginRpcHost('p', ALL, makeDeps(), createTestPluginRegistry([new ReservationsRpc(reservations, realtime(), guards)]));
|
||||
|
||||
@@ -30,7 +30,7 @@ const req = (method: string, params: Record<string, unknown> = {}): RpcRequest =
|
||||
const events = (r: { broadcast: ReturnType<typeof vi.fn> }) => r.broadcast.mock.calls.map((c) => c[1]);
|
||||
|
||||
/** Trip 1 belongs to user 42; reservation 5 and accommodation 11 sit on it. */
|
||||
function build(opts: { canEdit?: boolean; cascade?: boolean; stop?: boolean; seenActions?: string[] } = {}) {
|
||||
function build(opts: { canEdit?: boolean; cascade?: boolean; stop?: boolean; seenActions?: string[]; unresolved?: string[]; foreign?: string[] } = {}) {
|
||||
const realtime = { broadcast: vi.fn() } as unknown as RealtimeService & { broadcast: ReturnType<typeof vi.fn> };
|
||||
const reservations = {
|
||||
create: vi.fn(() => ({ reservation: { id: 40 }, accommodationCreated: !!opts.cascade })),
|
||||
@@ -44,6 +44,9 @@ function build(opts: { canEdit?: boolean; cascade?: boolean; stop?: boolean; see
|
||||
syncBudgetOnCreate: vi.fn(),
|
||||
syncBudgetOnUpdate: vi.fn(),
|
||||
notifyBookingChange: vi.fn(),
|
||||
// Both guards run before a write now, so the fixture answers for them.
|
||||
referencesOutsideTrip: vi.fn(() => opts.foreign ?? []),
|
||||
unresolvedReferences: vi.fn(() => opts.unresolved ?? []),
|
||||
} as unknown as ReservationsService & Record<string, ReturnType<typeof vi.fn>>;
|
||||
/** What a stay write did to the day plan, on top of writing the stay itself. */
|
||||
type Mirror = { created: { id: number; day_id: number } | null; removed: { id: number; dayId: number }[]; stamped: null };
|
||||
@@ -193,6 +196,34 @@ describe('ReservationsRpc', () => {
|
||||
expect(f.reservations.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('BOOK-RPC-018 an id that resolves to nothing is BAD_PARAMS, not a constraint failure', async () => {
|
||||
const f = build({ unresolved: ['place_id'] });
|
||||
const res = (await f.host().dispatch(
|
||||
req('reservations.create', { tripId: 1, input: { title: 'Hotel', type: 'lodging', place_id: 999999 } }), 42,
|
||||
)) as RpcError;
|
||||
expect(res.error.code).toBe('BAD_PARAMS');
|
||||
expect(res.error.message).toBe('unknown reference: place_id');
|
||||
expect(f.reservations.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('BOOK-RPC-019 the same on update, while an id on another trip still writes', async () => {
|
||||
const f = build({ unresolved: ['day_id'] });
|
||||
const res = (await f.host().dispatch(
|
||||
req('reservations.update', { tripId: 1, reservationId: 5, input: { title: 'x', type: 'lodging', day_id: 999999 } }), 42,
|
||||
)) as RpcError;
|
||||
expect(res.error.code).toBe('BAD_PARAMS');
|
||||
expect(f.reservations.update).not.toHaveBeenCalled();
|
||||
|
||||
// A plugin has always been able to name another trip's row through this
|
||||
// surface. Taking that away is a change to the plugin contract, not part of
|
||||
// turning a crash into an error.
|
||||
const foreign = build({ unresolved: ['day_id'], foreign: ['day_id'] });
|
||||
expect((await foreign.host().dispatch(
|
||||
req('reservations.update', { tripId: 1, reservationId: 5, input: { title: 'x', type: 'lodging', day_id: 7 } }), 42,
|
||||
)).ok).toBe(true);
|
||||
expect(foreign.reservations.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('BOOK-RPC-009 the class is listed in its module providers', () => {
|
||||
expectRegisteredProvider(ReservationsModule, ReservationsRpc);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user