mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(trips): let a trip run past 365 days and refuse a longer range instead of clipping it
A trip longer than a year stopped at day 365: generateDays capped the day rows at MAX_TRIP_DAYS while the trip kept its full end date, so the last days had a date but no day to plan on, in the day list, the transport and booking dialogs and the phone's date pickers (#2403). The limit is MAX_TRIP_DAYS in @trek/shared now, 999 days, shared by the server, both trip forms and the MCP tools, and a range past it is refused rather than cut short: 400 on REST, an error result on MCP, bad params on the plugin RPC, and a message in the forms before anything is sent. The check runs before the trip row and the budget are touched, also catches a start date moved past the stored end on its own, and holds a day_count rebuild to the same bound. A trip that already carries a longer range can still be renamed. The MCP-only 90-day ceiling on create_trip goes with it; it clipped dated trips the same way. Closes #2403
This commit is contained in:
@@ -12,6 +12,7 @@ import { DatabaseModule } from '../../src/nest/database/database.module';
|
||||
import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { seedUser, sessionCookie } from './harness';
|
||||
import { MAX_TRIP_DAYS } from '@trek/shared';
|
||||
|
||||
const { db } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
@@ -197,6 +198,33 @@ describe('Trips e2e (real auth guard + temp SQLite)', () => {
|
||||
expect(forbidden.status).toBe(403);
|
||||
});
|
||||
|
||||
it('201 create keeps every day of a trip longer than a year (#2403)', async () => {
|
||||
// 2025-01-26 .. 2026-01-28 is 368 days; the day list used to stop at 365.
|
||||
const res = await request(server).post('/api/trips').set('Cookie', sessionCookie(1))
|
||||
.send({ title: 'Gap year', start_date: '2025-01-26', end_date: '2026-01-28' });
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.trip).toMatchObject({ start_date: '2025-01-26', end_date: '2026-01-28', day_count: 368 });
|
||||
const last = db.prepare('SELECT day_number, date FROM days WHERE trip_id = ? ORDER BY day_number DESC LIMIT 1')
|
||||
.get(res.body.trip.id) as { day_number: number; date: string };
|
||||
expect(last).toEqual({ day_number: 368, date: '2026-01-28' });
|
||||
});
|
||||
|
||||
it('400 on a date range past MAX_TRIP_DAYS, for create and update alike', async () => {
|
||||
const tooLong = await request(server).post('/api/trips').set('Cookie', sessionCookie(1))
|
||||
.send({ title: 'Decade', start_date: '2026-01-01', end_date: '2036-01-01' });
|
||||
expect(tooLong.status).toBe(400);
|
||||
expect(tooLong.body).toEqual({ error: `A trip can span at most ${MAX_TRIP_DAYS} days` });
|
||||
expect(db.prepare('SELECT COUNT(*) AS n FROM trips').get()).toEqual({ n: 0 });
|
||||
|
||||
const week = await request(server).post('/api/trips').set('Cookie', sessionCookie(1))
|
||||
.send({ title: 'Week', start_date: '2026-07-01', end_date: '2026-07-07' });
|
||||
const stretched = await request(server).put(`/api/trips/${week.body.trip.id}`).set('Cookie', sessionCookie(1))
|
||||
.send({ end_date: '2036-07-01' });
|
||||
expect(stretched.status).toBe(400);
|
||||
expect(stretched.body).toEqual({ error: `A trip can span at most ${MAX_TRIP_DAYS} days` });
|
||||
expect(db.prepare('SELECT end_date FROM trips WHERE id = ?').get(week.body.trip.id)).toEqual({ end_date: '2026-07-07' });
|
||||
});
|
||||
|
||||
it('404 on a missing trip', async () => {
|
||||
const res = await request(server).get('/api/trips/77').set('Cookie', sessionCookie(1));
|
||||
expect(res.status).toBe(404);
|
||||
|
||||
@@ -43,6 +43,7 @@ import { runMigrations } from '../../../src/db/migrations';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, createDay, createPlace, addTripMember, createBudgetItem, createPackingItem, createReservation, createDayNote, createCollabNote, createDayAssignment, createDayAccommodation } from '../../helpers/factories';
|
||||
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
|
||||
import { MAX_TRIP_DAYS } from '@trek/shared';
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
@@ -99,16 +100,29 @@ describe('Tool: create_trip', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('caps days at 90 for very long trips', async () => {
|
||||
it('generates every day of a trip longer than a year (#2403)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'create_trip',
|
||||
arguments: { title: 'Long Trip', start_date: '2026-01-01', end_date: '2027-12-31' },
|
||||
arguments: { title: 'Long Trip', start_date: '2025-01-26', end_date: '2026-01-28' },
|
||||
});
|
||||
const data = parseToolResult(result) as any;
|
||||
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as { c: number };
|
||||
expect(days.c).toBe(90);
|
||||
expect(days.c).toBe(368);
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a date range longer than MAX_TRIP_DAYS instead of cutting the days short', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'create_trip',
|
||||
arguments: { title: 'Decade', start_date: '2026-01-01', end_date: '2036-01-01' },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(JSON.stringify(result.content)).toContain(`at most ${MAX_TRIP_DAYS} days`);
|
||||
expect(testDb.prepare('SELECT COUNT(*) as c FROM trips').get()).toEqual({ c: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,11 +167,11 @@ describe('Tool: create_trip', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a day_count outside 1..365', async () => {
|
||||
it('refuses a day_count outside 1..MAX_TRIP_DAYS', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
expect((await h.client.callTool({ name: 'create_trip', arguments: { title: 'Zero', day_count: 0 } })).isError).toBe(true);
|
||||
expect((await h.client.callTool({ name: 'create_trip', arguments: { title: 'Huge', day_count: 400 } })).isError).toBe(true);
|
||||
expect((await h.client.callTool({ name: 'create_trip', arguments: { title: 'Huge', day_count: MAX_TRIP_DAYS + 1 } })).isError).toBe(true);
|
||||
expect(testDb.prepare('SELECT COUNT(*) as c FROM trips').get()).toEqual({ c: 0 });
|
||||
});
|
||||
});
|
||||
@@ -378,16 +392,28 @@ describe('Tool: update_trip', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a day_count outside 1..365', async () => {
|
||||
it('refuses a day_count outside 1..MAX_TRIP_DAYS', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Untouched' });
|
||||
await withHarness(user.id, async (h) => {
|
||||
expect((await h.client.callTool({ name: 'update_trip', arguments: { tripId: trip.id, day_count: 0 } })).isError).toBe(true);
|
||||
expect((await h.client.callTool({ name: 'update_trip', arguments: { tripId: trip.id, day_count: 400 } })).isError).toBe(true);
|
||||
expect((await h.client.callTool({ name: 'update_trip', arguments: { tripId: trip.id, day_count: MAX_TRIP_DAYS + 1 } })).isError).toBe(true);
|
||||
expect(testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(trip.id)).toEqual({ c: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses a date range longer than MAX_TRIP_DAYS and leaves the trip untouched', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Untouched', start_date: '2026-07-01', end_date: '2026-07-07' });
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({ name: 'update_trip', arguments: { tripId: trip.id, end_date: '2036-07-01' } });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(JSON.stringify(result.content)).toContain(`at most ${MAX_TRIP_DAYS} days`);
|
||||
const row = testDb.prepare('SELECT end_date FROM trips WHERE id = ?').get(trip.id) as { end_date: string };
|
||||
expect(row.end_date).toBe('2026-07-07');
|
||||
});
|
||||
});
|
||||
|
||||
it('changes reminder_days and can turn the reminder off', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { CalendarService } from '../../../src/nest/calendar/calendar.servic
|
||||
import type { TripReadModelService } from '../../../src/nest/trip-read-model/trip-read-model.service';
|
||||
import { NotFoundError, ValidationError } from '../../../src/nest/trips/trips.service';
|
||||
import type { User } from '../../../src/types';
|
||||
import { activeTripResponseSchema, tripCreateRequestSchema, tripTransferOwnershipRequestSchema } from '@trek/shared';
|
||||
import { MAX_TRIP_DAYS, activeTripResponseSchema, tripCreateRequestSchema, tripTransferOwnershipRequestSchema } from '@trek/shared';
|
||||
|
||||
const user = { id: 1, role: 'user', email: 'u@example.test' } as User;
|
||||
const req = { headers: {} } as Request;
|
||||
@@ -180,6 +180,24 @@ describe('TripsController (parity with the legacy /api/trips route)', () => {
|
||||
expect(create).toHaveBeenCalledWith(1, expect.objectContaining({ day_count: 7 }));
|
||||
});
|
||||
|
||||
it('clamps day_count to MAX_TRIP_DAYS', () => {
|
||||
const create = vi.fn().mockReturnValue({ trip: { id: 9 }, tripId: 9, reminderDays: 0 });
|
||||
tc(svc({ create } as Partial<TripsService>)).create(user, { title: 'T', day_count: MAX_TRIP_DAYS + 1 }, req);
|
||||
expect(create).toHaveBeenCalledWith(1, expect.objectContaining({ day_count: MAX_TRIP_DAYS }));
|
||||
});
|
||||
|
||||
it('maps a ValidationError from create to 400 (range past MAX_TRIP_DAYS)', () => {
|
||||
const create = vi.fn().mockImplementation(() => { throw new ValidationError(`A trip can span at most ${MAX_TRIP_DAYS} days`); });
|
||||
expect(thrown(() => tc(svc({ create } as Partial<TripsService>)).create(user, { title: 'T', start_date: '2026-01-01', end_date: '2036-01-01' }, req))).toEqual({
|
||||
status: 400, body: { error: `A trip can span at most ${MAX_TRIP_DAYS} days` },
|
||||
});
|
||||
});
|
||||
|
||||
it('re-throws an unknown error from create', () => {
|
||||
const create = vi.fn().mockImplementation(() => { throw new Error('boom'); });
|
||||
expect(() => tc(svc({ create } as Partial<TripsService>)).create(user, { title: 'T' }, req)).toThrow('boom');
|
||||
});
|
||||
|
||||
it('logs the reminder when reminderDays is set', () => {
|
||||
const create = vi.fn().mockReturnValue({ trip: { id: 9 }, tripId: 9, reminderDays: 3 });
|
||||
expect(tc(svc({ create } as Partial<TripsService>)).create(user, { title: 'T' }, req)).toEqual({ trip: { id: 9 } });
|
||||
|
||||
@@ -54,6 +54,7 @@ import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, createReservation, createPlace, createDay, createDayAssignment, createDayNote, addTripMember } from '../../helpers/factories';
|
||||
import { MAX_TRIP_DAYS } from '@trek/shared';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { DaysService } from '../../../src/nest/days/days.service';
|
||||
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
||||
@@ -169,6 +170,10 @@ function getNotes(dayId: number) {
|
||||
return testDb.prepare('SELECT * FROM day_notes WHERE day_id = ?').all(dayId) as { id: number; day_id: number }[];
|
||||
}
|
||||
|
||||
function addDaysIso(date: string, n: number) {
|
||||
return new Date(Date.parse(date + 'T00:00:00Z') + n * 86400000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('generateDays', () => {
|
||||
@@ -260,6 +265,24 @@ describe('generateDays', () => {
|
||||
expect(getAssignments(daysAfter[4].id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-062: a range longer than a year gets every one of its days (#2403)', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-01-26', end_date: '2025-01-28' });
|
||||
// The reporter's range: 368 days, and the days used to stop at 365.
|
||||
svc.generateDays(trip.id, '2025-01-26', '2026-01-28');
|
||||
const days = getDays(trip.id);
|
||||
expect(days).toHaveLength(368);
|
||||
expect(days[364].date).toBe('2026-01-25');
|
||||
expect(days[367]).toMatchObject({ day_number: 368, date: '2026-01-28' });
|
||||
});
|
||||
|
||||
it('TRIP-SVC-063: a dateless day_count is clamped to MAX_TRIP_DAYS', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
svc.generateDays(trip.id, null, null, MAX_TRIP_DAYS + 50);
|
||||
expect(getDays(trip.id)).toHaveLength(MAX_TRIP_DAYS);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-013: clearing dates converts all days to dateless without destroying assignments', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-09-01', end_date: '2025-09-04' });
|
||||
@@ -722,6 +745,17 @@ describe('folded trip CRUD', () => {
|
||||
expect(getDays(tripId)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-064: create refuses a range past MAX_TRIP_DAYS and writes nothing', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const before = (testDb.prepare('SELECT COUNT(*) AS n FROM trips').get() as { n: number }).n;
|
||||
expect(() => svc.create(user.id, { title: 'Decade', start_date: '2026-01-01', end_date: '2036-01-01' }))
|
||||
.toThrow(`A trip can span at most ${MAX_TRIP_DAYS} days`);
|
||||
expect((testDb.prepare('SELECT COUNT(*) AS n FROM trips').get() as { n: number }).n).toBe(before);
|
||||
// The longest allowed range goes through in full.
|
||||
const { tripId } = svc.create(user.id, { title: 'Longest', start_date: '2026-01-01', end_date: addDaysIso('2026-01-01', MAX_TRIP_DAYS - 1) });
|
||||
expect(getDays(tripId)).toHaveLength(MAX_TRIP_DAYS);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-045: remove deletes the trip, cleans skeleton journey entries and detaches filled ones', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
@@ -930,13 +964,15 @@ describe('folded trip CRUD', () => {
|
||||
|
||||
describe('TripsService wrapper helpers', () => {
|
||||
it('re-anchors the budget before the trip row leaves its old currency (#1543)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const order: string[] = [];
|
||||
const rebaseSpy = vi.spyOn(budgetSvc, 'rebaseTripCurrency').mockImplementation(async () => { order.push('rebase'); });
|
||||
const updateSpy = vi.spyOn(svc, 'updateTrip').mockImplementation(() => { order.push('update'); return {} as never; });
|
||||
try {
|
||||
await svc.update('9', 1, { currency: 'RUB' } as never, 'user');
|
||||
await svc.update(trip.id, user.id, { currency: 'RUB' } as never, 'user');
|
||||
// The rebase reads the outgoing currency off the trip row, so it has to run first.
|
||||
expect(rebaseSpy).toHaveBeenCalledWith('9', 'RUB');
|
||||
expect(rebaseSpy).toHaveBeenCalledWith(trip.id, 'RUB');
|
||||
expect(order).toEqual(['rebase', 'update']);
|
||||
} finally {
|
||||
rebaseSpy.mockRestore();
|
||||
@@ -944,6 +980,22 @@ describe('TripsService wrapper helpers', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('TRIP-SVC-068: update refuses a bad range before the budget is rebased (#2403)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-07-01', end_date: '2026-07-07' });
|
||||
const rebaseSpy = vi.spyOn(budgetSvc, 'rebaseTripCurrency').mockResolvedValue();
|
||||
try {
|
||||
await expect(svc.update(trip.id, user.id, { currency: 'USD', end_date: '2036-07-01' }, 'user'))
|
||||
.rejects.toThrow(`A trip can span at most ${MAX_TRIP_DAYS} days`);
|
||||
await expect(svc.update(trip.id, user.id, { currency: 'USD', start_date: '2026-07-10' }, 'user'))
|
||||
.rejects.toThrow('End date must be after start date');
|
||||
expect(rebaseSpy).not.toHaveBeenCalled();
|
||||
await expect(svc.update(99999, user.id, { currency: 'USD' }, 'user')).rejects.toThrow('Trip not found');
|
||||
} finally {
|
||||
rebaseSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('canAccessTrip delegates to the db helper; can() delegates to checkPermission; broadcast forwards', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
@@ -1006,6 +1058,40 @@ describe('folded quirk branches', () => {
|
||||
expect(() => svc.updateTrip(trip.id, owner.id, { start_date: '2025-06-10', end_date: '2025-06-01' }, 'user')).toThrow('End date must be after start date');
|
||||
});
|
||||
|
||||
it('TRIP-SVC-065: updateTrip refuses a range past MAX_TRIP_DAYS before touching the row', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Week', start_date: '2026-07-01', end_date: '2026-07-07' });
|
||||
expect(() => svc.updateTrip(trip.id, user.id, { title: 'Decade', end_date: '2036-07-01' }, 'user'))
|
||||
.toThrow(`A trip can span at most ${MAX_TRIP_DAYS} days`);
|
||||
expect(testDb.prepare('SELECT title, end_date FROM trips WHERE id = ?').get(trip.id)).toEqual({ title: 'Week', end_date: '2026-07-07' });
|
||||
expect(getDays(trip.id)).toHaveLength(7);
|
||||
// Moving only the start keeps the stored end and is measured against it.
|
||||
expect(() => svc.updateTrip(trip.id, user.id, { start_date: '2020-01-01' }, 'user'))
|
||||
.toThrow(`A trip can span at most ${MAX_TRIP_DAYS} days`);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-066: a trip whose stored range already exceeds the limit can still be renamed', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Legacy' });
|
||||
testDb.prepare("UPDATE trips SET start_date = '2020-01-01', end_date = '2030-01-01' WHERE id = ?").run(trip.id);
|
||||
const result = svc.updateTrip(trip.id, user.id, { title: 'Renamed' }, 'user');
|
||||
expect(result.newTitle).toBe('Renamed');
|
||||
expect(result.changes).toEqual({ title: 'Renamed' });
|
||||
// A day_count would rebuild the grid over the whole stored range, so it is held to the limit too.
|
||||
expect(() => svc.updateTrip(trip.id, user.id, { day_count: 5 }, 'user'))
|
||||
.toThrow(`A trip can span at most ${MAX_TRIP_DAYS} days`);
|
||||
expect(getDays(trip.id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-067: a start date moved past the stored end is refused instead of emptying the trip', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Week', start_date: '2026-07-01', end_date: '2026-07-07' });
|
||||
expect(() => svc.updateTrip(trip.id, user.id, { start_date: '2026-07-10' }, 'user'))
|
||||
.toThrow('End date must be after start date');
|
||||
expect(testDb.prepare('SELECT start_date FROM trips WHERE id = ?').get(trip.id)).toEqual({ start_date: '2026-07-01' });
|
||||
expect(getDays(trip.id)).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-049: addMember inserts the membership and reports the trip title; removeMember deletes it', () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: invitee } = createUser(testDb);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { PluginGuards } from '../../../src/nest/plugins/host/plugin-guards.servi
|
||||
import { TripsRpc } from '../../../src/nest/trips/trips.rpc';
|
||||
import { TripsModule } from '../../../src/nest/trips/trips.module';
|
||||
import { NotFoundError, ValidationError } from '../../../src/nest/trips/trips.service';
|
||||
import { MAX_TRIP_DAYS } from '@trek/shared';
|
||||
import type { TripsService } from '../../../src/nest/trips/trips.service';
|
||||
import type { ReservationsService } from '../../../src/nest/reservations/reservations.service';
|
||||
import type { DaysService } from '../../../src/nest/days/days.service';
|
||||
@@ -33,14 +34,17 @@ const req = (method: string, params: Record<string, unknown> = {}): RpcRequest =
|
||||
const ALL_TRIP_GRANTS = ['db:read:trips', 'db:write:trips', 'db:create:trips', 'db:write:members'];
|
||||
|
||||
/** Trip 1 belongs to user 42. `allow` decides each individual permission check. */
|
||||
export function build(opts: { allow?: (action: string) => boolean; updateThrows?: Error } = {}) {
|
||||
export function build(opts: { allow?: (action: string) => boolean; updateThrows?: Error; createThrows?: Error } = {}) {
|
||||
const trips = {
|
||||
list: vi.fn(() => [{ id: 1, title: 'Japan' }]),
|
||||
updateTrip: vi.fn(() => {
|
||||
if (opts.updateThrows) throw opts.updateThrows;
|
||||
return { updatedTrip: { id: 1, title: 'Japan' } };
|
||||
}),
|
||||
create: vi.fn((userId: number) => ({ trip: { id: 99, user_id: userId } })),
|
||||
create: vi.fn((userId: number) => {
|
||||
if (opts.createThrows) throw opts.createThrows;
|
||||
return { trip: { id: 99, user_id: userId } };
|
||||
}),
|
||||
removeMember: vi.fn(),
|
||||
} as unknown as TripsService & Record<string, ReturnType<typeof vi.fn>>;
|
||||
const reservations = { list: vi.fn(() => [{ id: 5 }]) } as unknown as ReservationsService & Record<string, ReturnType<typeof vi.fn>>;
|
||||
@@ -169,6 +173,12 @@ describe('TripsRpc writes', () => {
|
||||
expect(res.error.message).toBe('no permission to create trips');
|
||||
expect(f.trips.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('TRIPS-RPC-017 a range the service refuses comes back as BAD_PARAMS (#2403)', async () => {
|
||||
const f = build({ createThrows: new ValidationError(`A trip can span at most ${MAX_TRIP_DAYS} days`) });
|
||||
const res = (await f.host().dispatch(req('trips.create', { input: { title: 'Decade', start_date: '2026-01-01', end_date: '2036-01-01' } }), 42)) as RpcError;
|
||||
expect(res.error).toMatchObject({ code: 'BAD_PARAMS', message: `A trip can span at most ${MAX_TRIP_DAYS} days` });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TripsRpc membership', () => {
|
||||
|
||||
Reference in New Issue
Block a user