# google-calendar

Lightweight, dependency-free in-process fake of Google Calendar API v3 for parlel-pool tests.

Default port: `4615`

## Quick Start

```js
import { google } from "googleapis";
import { GoogleCalendarServer } from "./services/google-calendar/src/server.js";

const server = new GoogleCalendarServer(4615);
await server.start();

const calendar = google.calendar({ version: "v3", auth: "not-used-by-parlel" });
calendar.context._options.rootUrl = "http://127.0.0.1:4615/";

const created = await calendar.calendars.insert({
  requestBody: { summary: "Engineering", timeZone: "UTC" },
});

await calendar.events.insert({
  calendarId: created.data.id,
  requestBody: {
    summary: "Planning",
    start: { dateTime: "2026-01-01T10:00:00.000Z" },
    end: { dateTime: "2026-01-01T11:00:00.000Z" },
  },
});

await server.stop();
```

Reset state with `server.reset()` or `POST /_parlel/reset`. Health is available at `GET /_parlel/health`.

## Implemented Operations

### Calendars

| googleapis method | HTTP endpoint |
| --- | --- |
| `calendar.calendars.insert` | `POST /calendar/v3/calendars` |
| `calendar.calendars.get` | `GET /calendar/v3/calendars/{calendarId}` |
| `calendar.calendars.patch` | `PATCH /calendar/v3/calendars/{calendarId}` |
| `calendar.calendars.update` | `PUT /calendar/v3/calendars/{calendarId}` |
| `calendar.calendars.clear` | `POST /calendar/v3/calendars/{calendarId}/clear` |
| `calendar.calendars.delete` | `DELETE /calendar/v3/calendars/{calendarId}` |

### Calendar List

| googleapis method | HTTP endpoint |
| --- | --- |
| `calendar.calendarList.list` | `GET /calendar/v3/users/me/calendarList` |
| `calendar.calendarList.insert` | `POST /calendar/v3/users/me/calendarList` |
| `calendar.calendarList.get` | `GET /calendar/v3/users/me/calendarList/{calendarId}` |
| `calendar.calendarList.patch` | `PATCH /calendar/v3/users/me/calendarList/{calendarId}` |
| `calendar.calendarList.update` | `PUT /calendar/v3/users/me/calendarList/{calendarId}` |
| `calendar.calendarList.delete` | `DELETE /calendar/v3/users/me/calendarList/{calendarId}` |
| `calendar.calendarList.watch` | `POST /calendar/v3/users/me/calendarList/watch` |

### ACL

| googleapis method | HTTP endpoint |
| --- | --- |
| `calendar.acl.list` | `GET /calendar/v3/calendars/{calendarId}/acl` |
| `calendar.acl.insert` | `POST /calendar/v3/calendars/{calendarId}/acl` |
| `calendar.acl.get` | `GET /calendar/v3/calendars/{calendarId}/acl/{ruleId}` |
| `calendar.acl.patch` | `PATCH /calendar/v3/calendars/{calendarId}/acl/{ruleId}` |
| `calendar.acl.update` | `PUT /calendar/v3/calendars/{calendarId}/acl/{ruleId}` |
| `calendar.acl.delete` | `DELETE /calendar/v3/calendars/{calendarId}/acl/{ruleId}` |
| `calendar.acl.watch` | `POST /calendar/v3/calendars/{calendarId}/acl/watch` |

### Events

| googleapis method | HTTP endpoint |
| --- | --- |
| `calendar.events.list` | `GET /calendar/v3/calendars/{calendarId}/events` |
| `calendar.events.insert` | `POST /calendar/v3/calendars/{calendarId}/events` |
| `calendar.events.import` | `POST /calendar/v3/calendars/{calendarId}/events/import` |
| `calendar.events.quickAdd` | `POST /calendar/v3/calendars/{calendarId}/events/quickAdd?text=...` |
| `calendar.events.get` | `GET /calendar/v3/calendars/{calendarId}/events/{eventId}` |
| `calendar.events.patch` | `PATCH /calendar/v3/calendars/{calendarId}/events/{eventId}` |
| `calendar.events.update` | `PUT /calendar/v3/calendars/{calendarId}/events/{eventId}` |
| `calendar.events.delete` | `DELETE /calendar/v3/calendars/{calendarId}/events/{eventId}` |
| `calendar.events.move` | `POST /calendar/v3/calendars/{calendarId}/events/{eventId}/move?destination=...` |
| `calendar.events.instances` | `GET /calendar/v3/calendars/{calendarId}/events/{eventId}/instances` |
| `calendar.events.watch` | `POST /calendar/v3/calendars/{calendarId}/events/watch` |

### Other Resources

| googleapis method | HTTP endpoint |
| --- | --- |
| `calendar.freebusy.query` | `POST /calendar/v3/freeBusy` |
| `calendar.colors.get` | `GET /calendar/v3/colors` |
| `calendar.settings.list` | `GET /calendar/v3/users/me/settings` |
| `calendar.settings.get` | `GET /calendar/v3/users/me/settings/{setting}` |
| `calendar.settings.watch` | `POST /calendar/v3/users/me/settings/watch` |
| `calendar.channels.stop` | `POST /calendar/v3/channels/stop` |

All endpoints also accept the short `/v3/...` prefix.

## Supported Features

| Feature | Status | Notes |
| --- | --- | --- |
| In-memory calendars, calendar list, ACL, events, settings, channels | Supported | Ephemeral and resettable. |
| `googleapis` Calendar v3 REST paths | Supported | Use `rootUrl = "http://127.0.0.1:4615/"`. |
| Pagination | Supported | `maxResults` and numeric `pageToken`. |
| Event filters | Supported | `q`, `iCalUID`, `timeMin`, `timeMax`, `updatedMin`, `showDeleted`, `singleEvents`, `orderBy`. |
| Recurring event expansion | Supported | `RRULE:FREQ=DAILY;COUNT=n` and `RRULE:FREQ=WEEKLY;COUNT=n`. |
| Free/busy calculation | Supported | Ignores transparent and cancelled events. |
| Watch channels | Supported | Stores channel metadata in memory; no outbound webhook delivery. |
| OAuth and IAM | Intentionally unsupported | Requests are accepted without auth. |
| Push notification delivery | Intentionally unsupported | Channel resources are returned and tracked only. |
| Persistence | Intentionally unsupported | State disappears after reset or process exit. |
| Conference creation side effects | Intentionally unsupported | `conferenceData` fields are stored but no external meetings are created. |
| Email invites or reminders | Intentionally unsupported | Fields are stored but no email or notification is sent. |

## Error Shapes

Errors use Google-style JSON responses:

```json
{
  "error": {
    "code": 404,
    "message": "Calendar not found",
    "status": "NOT_FOUND",
    "errors": [
      {
        "message": "Calendar not found",
        "domain": "global",
        "reason": "notFound"
      }
    ]
  }
}
```

Common codes:

| Code | Status | Reasons |
| --- | --- | --- |
| `400` | `INVALID_ARGUMENT` | `invalidArgument`, `parseError`, `required` |
| `404` | `NOT_FOUND` | `notFound` |
| `405` | `METHOD_NOT_ALLOWED` | `methodNotAllowed` |
| `409` | `ALREADY_EXISTS` | `alreadyExists` |
| `500` | `INTERNAL` | `backendError` |
