Test control API
Reset state, load fixtures, inspect request history, and drain webhook deliveries over HTTP during automated tests.
Calendarpit has a local management API under /__calendarpit. Use it to seed data, inspect server state, and reset between test runs.
Request rules
-
Base path: All management endpoints start with
/__calendarpit. For example:http://127.0.0.1:4173/__calendarpit/reset. -
No authentication: Management endpoints do not use a bearer token. Calendarpit is a local development server and binds to loopback by default.
-
Content-Type: application/jsonon writes: Every request other thanGETorHEADmust send aContent-Typeheader that starts withapplication/json(case-sensitive).- This includes
DELETEroutes andPOSTcontrols with no body, such as/resetand/deliveries/drain. - Requests without the header fail with HTTP
415(unsupportedMediaType). - For
POSTcontrols with no request body, send an empty body or{}.
- This includes
-
Same-origin check: A request fails with HTTP
403(forbidden) in either case:- Its
Originheader does not match the server origin. - It has
Sec-Fetch-Site: cross-site.
Non-browser clients such as
curland test runners usually omitOrigin, so they are allowed. - Its
-
Health check exemption:
/__calendarpit/healthskips these checks.GETandHEADreadiness probes need no extra headers. -
Excluded from history: Requests to
/__calendarpit/*are not recorded in the protocol request history. -
Error format: Management errors use the Google JSON error envelope with
domain: "calendarpit".
If you bind to a non-loopback address (for example, inside a container), anyone who can reach the port can call these endpoints.
Management endpoints
All paths below are relative to /__calendarpit.
| Method | Path | Behavior |
|---|---|---|
GET / HEAD | /health | Return server information and capability flags. |
GET | /state | Return a snapshot of the server state. |
POST | /fixtures | Validate and replace fixture data (200 snapshot). |
POST | /reset | Restore the startup fixture baseline (200 snapshot). |
GET / DELETE | /history | Return (200) or clear (204) the latest 200 protocol requests. |
POST | /users | Create a user (201). |
PATCH / DELETE | /users/{id} | Edit or delete (204) a user. |
POST | /calendars | Create a calendar (201). |
PATCH / DELETE | /calendars/{id} | Edit or delete (204) a calendar. |
GET / POST | /calendars/{id}/events | List events or create an event (200). |
GET / PUT / PATCH / DELETE | /calendars/{id}/events/{eventId} | Read, replace, patch, or delete (204) an event. |
POST | /connections/{id}/revoke | Revoke the connection's tokens and stop its watch channels (204). |
POST | /deliveries/drain | Wait for pending webhook deliveries, then return the delivery log (200). |
Endpoint details
/health: Returns the servername("Calendarpit"),version,stage, andstorage("memory"or"sqlite"). It also returns the capability flagscalendar,oauth,watch, andfixtures.HEADreturns an empty200./state: Removes expired channels first. It then returns a snapshot ofstorage,users,calendars,events,clients(without secrets),connections,channels(tokenredacted),deliveries,notifications, andrevision./fixtures: Validates and replaces users, calendars, events, and clients. It clears runtime state and invalidates connections./reset: Restores the startup fixture baseline and clears all runtime state./history:GETreturns the requests ordered from oldest to newest./users: Acceptsname,email, and an optionalid. Returns409 duplicateif the ID already exists./users/{id}:PATCHeditsnameandemail.idis immutable. Deleting a user also deletes the data they own./calendars: AcceptsuserId,summary, and optionaltimeZone,primary, andid./calendars/{id}:idanduserIdare immutable./calendars/{id}/events: Listing supportsevents.listparameters, includingsyncToken. Listed items includecalendarId./calendars/{id}/events/{eventId}:DELETEkeeps a tombstone./deliveries/drain: Also waits for the retries of pending deliveries. The response is the delivery log array.
Cascades and admin writes
- User deletion: Deleting a user revokes their OAuth connections and stops their watch channels. It also deletes their calendars and events without creating calendar tombstones.
- Primary flag: A user's first calendar becomes
primary: trueautomatically.- Setting
primary: trueon another calendar of the same user moves the flag to that calendar in one step. - Unsetting
primarydirectly returns400.
- Setting
- Calendar deletion: Deleting a calendar does the following:
- Deletes the calendar's events and stops its event watch channels.
- Records a
calendarListtombstone with default list settings. Per-user customizations such assummaryOverrideorcolorIdare dropped. - Makes another of the owner's calendars primary if the deleted calendar was primary. Calendarpit prefers a calendar that is still on the owner's calendar list.
- Calendar list notifications: Each admin calendar create (
POST /calendars), edit (PATCH /calendars/{id}), and delete queues anexistsnotification on the owner'scalendarList.watchchannels.- An edit sends a notification even when nothing changes.
- Expect these extra deliveries if your test counts them.
- Admin event writes: Event writes under
/__calendarpit/calendars/{id}/eventsdiffer from Calendar API writes:- They skip invitee restrictions.
- They do not require
conferenceDataVersion=1. - They can set read-only fields:
organizer,creator,iCalUID,recurringEventId, andoriginalStartTime. - Updates keep the stored
organizer,creator, andiCalUID. If an event already hasrecurringEventIdandoriginalStartTime, updates keep those too.
Reset vs fixture load
POST /__calendarpit/reset and POST /__calendarpit/fixtures both replace stored data, save it immediately, and clear all runtime state. The difference is the data they load.
| Behavior | POST /__calendarpit/reset | POST /__calendarpit/fixtures |
|---|---|---|
| Users, calendars, events, clients | Restored from the startup baseline | Replaced by the posted fixture JSON |
| Startup reset baseline | Unchanged | Unchanged (the next /reset still restores the original startup seed) |
| Runtime state | Cleared | Cleared |
Validation on POST /__calendarpit/fixtures is atomic. If any record is invalid, Calendarpit returns 400 and changes nothing.
What both endpoints clear
Both endpoints stop all active watch channels and abort their in-flight requests. They also clear:
- OAuth connections, access and refresh tokens, authorization codes, and pending consents
- Request history
- Sync tokens, plus event and calendar list page tokens
- Calendar tombstones
- Per-user calendar list settings: customizations made with
calendarList.insert/patch/update, and removals made withcalendarList.delete - Invitation notifications and all delivery log records
After either call:
- Old access tokens fail with
401(authError). - Old sync tokens fail with
410(fullSyncRequired).
Reset does not delete the SQLite database file. It does not change the fixture file on disk either.
Request history and state export
Request history
GET /__calendarpit/history returns the latest 200 requests served on /calendar/v3/*, /oauth*, and /userinfo. They are ordered from oldest to newest. Failed requests, such as 401 or 501 responses, are included.
Calendarpit masks credentials in the history:
- These keys are replaced with
"[REDACTED]":authorization,cookie,set-cookie,client_secret,secret,access_token,refresh_token,id_token,code,code_verifier,code_challenge,consent,token,x-goog-channel-token, andstate. This applies to headers, bodies, query strings, andLocationorRefererURLs. - Credentials inside URLs are removed.
- Strings longer than 12,000 characters are cut off and end with
[truncated].
State export
Use GET /__calendarpit/state for inspection only. You cannot load its output back with POST /__calendarpit/fixtures:
- It omits client secrets.
clientsentries contain onlyidandredirectUris. - Channel tokens appear as
"[REDACTED]". - It includes runtime fields (
storage,connections,channels,deliveries,notifications,revision).POST /__calendarpit/fixturesrejects them with400(unsupportedFeature). - It does not include calendar list settings. Read those with
calendarList.list/get.
Keep your original fixture files for seeding and reloading.
Writing repeatable tests
- Reset before each scenario: Call
POST /__calendarpit/resetbefore each test to restore the startup baseline. To load a specific fixture instead, callPOST /__calendarpit/fixtures. - Use fixed IDs: Set explicit
idvalues for users, calendars, events, and clients in your fixture file. Calendarpit generates missing IDs once at seed time. In:memory:mode, each new server process generates new IDs. - Isolate parallel workers: Run one Calendarpit process per test worker with
--port 0and--database :memory:. You can also give each worker its own SQLite file. - Read the port from stdout: With
--port 0, read the URL from the startup lineAdmin http://127.0.0.1:<port>/. For example, match it with/Admin\s+(http:\/\/[^\s]+)/. Then pollGET /__calendarpit/healthuntil it responds before you run tests. - Drain webhook deliveries instead of sleeping:
POST /__calendarpit/deliveries/drainwaits for the deliveries pending when you call it, including their retries. - Expect aborted webhooks after reset:
/resetaborts in-progress webhook requests. A receiver may still receive a request before it is aborted. - Refetch after restarts: When you test SQLite restarts, deliveries queued before shutdown are marked
cancelledand are not sent again. Refetch the calendar state after the restart.
const baseUrl = 'http://127.0.0.1:4173';
const jsonHeaders = { 'Content-Type': 'application/json' };
export async function resetCalendarpit() {
const res = await fetch(`${baseUrl}/__calendarpit/reset`, {
method: 'POST',
headers: jsonHeaders,
body: '{}',
});
if (!res.ok) throw new Error(`Reset failed: ${res.status}`);
return res.json();
}
export async function drainDeliveries() {
const res = await fetch(`${baseUrl}/__calendarpit/deliveries/drain`, {
method: 'POST',
headers: jsonHeaders,
body: '{}',
});
if (!res.ok) throw new Error(`Drain failed: ${res.status}`);
return res.json();
}