test(11-05): RED — WR-02 reminderLeadMinutes max(10080) in both Zod schemas
- outboxPayloadSchema: 10081 must hard-fail the row (currently dispatches) - eventFieldsSchema: POST /create with 10081 must 400 (currently 202) - boundary 10080 and null pass (already correct, no test fails expected)
This commit is contained in:
@@ -1018,3 +1018,79 @@ describe('runOutboxDrain — reminderLeadMinutes VALARM wiring (CAL-13/CAL-14, P
|
|||||||
expect(capturedIcsString as string).toContain('VALUE=DATE-TIME');
|
expect(capturedIcsString as string).toContain('VALUE=DATE-TIME');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Phase 11 Plan 05 WR-02: .max(10080) on reminderLeadMinutes in outboxPayloadSchema ──
|
||||||
|
// A payload with reminderLeadMinutes=10081 exceeds the 1-week UI cap (10080 min).
|
||||||
|
// The outboxPayloadSchema must reject it so the row is hard-failed rather than
|
||||||
|
// letting an out-of-range value silently flow into the VALARM trigger.
|
||||||
|
|
||||||
|
describe('runOutboxDrain — WR-02: reminderLeadMinutes max(10080) in outboxPayloadSchema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
mockPendingRows = [];
|
||||||
|
wireMockChain();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('WR-02: payload with reminderLeadMinutes=10081 is hard-failed (validation error)', async () => {
|
||||||
|
const { createCalendarEvent } = await import('../../src/broker/write.js');
|
||||||
|
vi.mocked(createCalendarEvent).mockResolvedValue(makeResponse(201));
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: 'Over-cap reminder',
|
||||||
|
allDay: false,
|
||||||
|
start: '2026-12-15T10:00:00Z',
|
||||||
|
end: '2026-12-15T11:00:00Z',
|
||||||
|
reminderLeadMinutes: 10081, // 1 min over the 1-week cap
|
||||||
|
});
|
||||||
|
mockPendingRows = [makeRow({ payload })];
|
||||||
|
|
||||||
|
await runOutboxDrain();
|
||||||
|
|
||||||
|
// Must NOT dispatch to CalDAV — validation must fire before ICS assembly
|
||||||
|
expect(createCalendarEvent).not.toHaveBeenCalled();
|
||||||
|
const setArg = mockUpdateSet.mock.calls[0]?.[0] as { status?: string; lastError?: string };
|
||||||
|
expect(setArg?.status).toBe('failed');
|
||||||
|
expect(setArg?.lastError).toMatch(/validation/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('WR-02: payload with reminderLeadMinutes=10080 (boundary) passes validation and dispatches', async () => {
|
||||||
|
const { createCalendarEvent } = await import('../../src/broker/write.js');
|
||||||
|
vi.mocked(createCalendarEvent).mockResolvedValue(makeResponse(201));
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: 'Max-cap reminder',
|
||||||
|
allDay: false,
|
||||||
|
start: '2026-12-15T10:00:00Z',
|
||||||
|
end: '2026-12-15T11:00:00Z',
|
||||||
|
reminderLeadMinutes: 10080, // exactly 1 week — must be allowed
|
||||||
|
});
|
||||||
|
mockPendingRows = [makeRow({ payload })];
|
||||||
|
|
||||||
|
await runOutboxDrain();
|
||||||
|
|
||||||
|
// Row is valid — CalDAV write must have been dispatched
|
||||||
|
expect(createCalendarEvent).toHaveBeenCalledTimes(1);
|
||||||
|
const setArg = mockUpdateSet.mock.calls[0]?.[0] as { status?: string };
|
||||||
|
expect(setArg?.status).toBe('done');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('WR-02: payload with reminderLeadMinutes=null (explicit clear) passes validation', async () => {
|
||||||
|
const { createCalendarEvent } = await import('../../src/broker/write.js');
|
||||||
|
vi.mocked(createCalendarEvent).mockResolvedValue(makeResponse(201));
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
title: 'Clear reminder',
|
||||||
|
allDay: false,
|
||||||
|
start: '2026-12-15T10:00:00Z',
|
||||||
|
end: '2026-12-15T11:00:00Z',
|
||||||
|
reminderLeadMinutes: null,
|
||||||
|
});
|
||||||
|
mockPendingRows = [makeRow({ payload })];
|
||||||
|
|
||||||
|
await runOutboxDrain();
|
||||||
|
|
||||||
|
// null (explicit clear) must pass .nullable()
|
||||||
|
const setArg = mockUpdateSet.mock.calls[0]?.[0] as { status?: string };
|
||||||
|
expect(setArg?.status).not.toBe('failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -930,3 +930,77 @@ describe('GET /api/events/writable-calendars', () => {
|
|||||||
expect(body.calendars.length).toBe(1);
|
expect(body.calendars.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WR-02: eventFieldsSchema must reject reminderLeadMinutes > 10080 (Phase 11 Plan 05)
|
||||||
|
// 10080 = 1 week in minutes; the UI cap prevents accidental over-wide lead values.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('WR-02: reminderLeadMinutes .max(10080) in eventFieldsSchema (Phase 11 Plan 05)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Wire a calendar row so POST /create reaches schema validation (not 403)
|
||||||
|
mockDbRows = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||||
|
displayName: 'Default',
|
||||||
|
color: '#4A90D9',
|
||||||
|
userId: 1,
|
||||||
|
isShared: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockSimpleWhere = vi.fn().mockImplementation(() => Promise.resolve(mockDbRows));
|
||||||
|
mockFromFn.mockReturnValue({ where: mockSimpleWhere });
|
||||||
|
mockSelectFn.mockReturnValue({ from: mockFromFn });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 400 for reminderLeadMinutes=10081 on POST /create', async () => {
|
||||||
|
const { app } = await import('../../src/index.js');
|
||||||
|
const res = await app.request('/api/events/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Over-cap',
|
||||||
|
allDay: false,
|
||||||
|
start: '2026-12-15T10:00:00Z',
|
||||||
|
end: '2026-12-15T11:00:00Z',
|
||||||
|
calendarUrl: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||||
|
reminderLeadMinutes: 10081,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 202 for reminderLeadMinutes=10080 (boundary) on POST /create', async () => {
|
||||||
|
const { app } = await import('../../src/index.js');
|
||||||
|
const res = await app.request('/api/events/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Max-cap',
|
||||||
|
allDay: false,
|
||||||
|
start: '2026-12-15T10:00:00Z',
|
||||||
|
end: '2026-12-15T11:00:00Z',
|
||||||
|
calendarUrl: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||||
|
reminderLeadMinutes: 10080,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(202);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 202 for reminderLeadMinutes=null (explicit clear) on POST /create', async () => {
|
||||||
|
const { app } = await import('../../src/index.js');
|
||||||
|
const res = await app.request('/api/events/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Clear reminder',
|
||||||
|
allDay: false,
|
||||||
|
start: '2026-12-15T10:00:00Z',
|
||||||
|
end: '2026-12-15T11:00:00Z',
|
||||||
|
calendarUrl: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||||
|
reminderLeadMinutes: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(202);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user