Test your code
Run your app's calendar logic against an in-memory calendar, with no account and no network.
Your booking flow shouldn't need a Google account, OAuth tokens or a network to prove it picks the right slot. @cal-sdk/mock is a full calendar in memory: your code calls the same functions it calls in production, and you check the result on the mock.
npm install -D @cal-sdk/mockPass the calendar in
The one change to make in your app is to take the provider as an argument instead of creating it inside the function. Production passes google(...), tests pass the mock:
import { type CalendarProvider, createEvent, findFreeTime } from '@cal-sdk/core';
export async function bookIntroCall(calendar: CalendarProvider, guest: string) {
const { slots } = await findFreeTime({
calendars: [{ calendar }],
timeMin: '2026-11-09T00:00:00Z',
timeMax: '2026-11-14T00:00:00Z',
duration: 30,
workingHours: { start: '09:00', end: '17:00', timeZone: 'Europe/Berlin' },
});
const [slot] = slots;
if (!slot) return undefined;
const { event } = await createEvent({
calendar,
event: {
title: 'Intro call',
start: { dateTime: slot.start, timeZone: 'Europe/Berlin' },
end: { dateTime: slot.end, timeZone: 'Europe/Berlin' },
attendees: [{ email: guest }],
},
});
return event;
}import { mockCalendar } from '@cal-sdk/mock';
import { expect, test } from 'vitest';
import { bookIntroCall } from './book-intro-call';
test('books the first free half hour', async () => {
const calendar = mockCalendar({
events: [
{
title: 'Busy morning',
start: { dateTime: '2026-11-09T09:00:00+01:00', timeZone: 'Europe/Berlin' },
end: { dateTime: '2026-11-09T11:00:00+01:00', timeZone: 'Europe/Berlin' },
},
],
});
const event = await bookIntroCall(calendar, 'guest@example.com');
expect(event?.start.dateTime).toBe('2026-11-09T10:00:00.000Z'); // 11:00 in Berlin
expect(calendar.events).toHaveLength(2);
});Settings
- calendars?(Partial<Calendar> & { id: string })[]
- Calendars to start with. The first is primary unless another sets
isPrimary. Defaults to one calendar,'cal_1'. - events?(EventInput & { id?: string; calendarId?: string })[]
- Events to start with, created in order. They skip the checks
createEventruns, so give everydateTimean offset. - now?() => Date
- Clock for
createdAtandupdatedAt. Defaults to the real time. - user?Person
- The signed-in user. Becomes the organizer of created events.
- defaultPageSize?number= 250
- Page size for listings without
pageSize, and for sync rounds.
Inspect the state
calendar.events holds the stored events (single events, series and changed occurrences), and calendar.calendars holds the calendars. Both are copies, so changing them doesn't change the mock.
Ids are predictable, so you can assert on them: events are evt_1, evt_2 and so on, and an occurrence is <series id>_<UTC start>, such as evt_1_20261109T083000Z, the same shape Google uses. Pass now to make createdAt and updatedAt predictable too.
What the mock does
The mock behaves like a full-featured provider, so your tests take the same paths real calendars do. Recurring events follow RFC 5545, with one RRULE line plus EXDATE lines, and all three scopes work; RDATE and EXRULE throw UnsupportedFunctionalityError. Etags change on every write, so a stale ifMatch throws ConflictError. Sync keeps a change log, with cursors and pages like a real provider. Search works, and attendees, video links, reminders and colors are stored as given.
Free/busy has no endpoint of its own, so getFreeBusy and findFreeTime compute it from events and return a compatibility warning, as they do for most providers.
What the mock doesn't do is imitate any one provider's quirks, such as Outlook's limits on recurrence. A test that passes on the mock tells you your logic is right, not that Outlook will accept your RRULE, so test those paths against a recording of the real API.
Silence warnings in tests
Warnings go to console.warn by default, which clutters test output. Turn them off in a test setup file and assert on the warnings a call returns instead:
globalThis.CALSDK_WARNINGS = 'silent';