対応範囲と制限
Calendarpit が対応している機能、対象外の機能、および現在のプロジェクト状況について説明します。
Calendarpit は、ローカルで動く Google Calendar 互換の API・OAuth サーバーです。本物の Google アカウントや Google Cloud プロジェクト、Google へのネットワーク接続なしで、アプリの開発やテストができます。
ローカル開発、CI、E2E テストに必要な範囲で、次の機能のサブセットを実装しています。
- Google Calendar API v3
- OAuth 2.0
- 差分同期
- Webhook 通知
対応している機能
このセクションの機能はすべて実装済みです。
実行環境と状態管理
- 単一の Bun プロセスが、API、OAuth フロー、管理画面を提供します。
- デフォルトのアドレスは
http://127.0.0.1:4173です。 - ストレージには SQLite または
:memory:を使います。
テストユーザーとカレンダー
- テストユーザーはパスワードなしで使え、カレンダーを所有します。
- 各カレンダーは IANA タイムゾーンを持ちます。
- 各ユーザーのプライマリカレンダーには
primaryでアクセスできます。 - カレンダー情報は
calendars.getで読み取ります。 - ユーザーごとのカレンダーリストは
calendarList(list、get、insert、patch、update、delete)で管理します。
ユーザーごとのリスト設定には、次のものがあります。
summaryOverridecolorId、またはcolorRgbFormatによるカスタム RGBhiddenとselecteddefaultRemindersnotificationSettings- プライマリ以外の所有カレンダーの、リストからの削除と復元
イベントと空き状況
- イベントの CRUD:
list、get、insert、update、patch、delete、instances - 例外(個別に変更された回)を含む繰り返しイベント。
RRULE・EXRULE・RDATE・EXDATEのサブセットに対応 - 夏時間(DST)を考慮した
[start, end)区間 - ETag と拡張プロパティ
- ローカルでの招待と出欠回答(RSVP)
- リマインダーの保存
- 擬似的な
hangoutsMeetリンク freebusy.query
COUNT も UNTIL もない繰り返しの系列は、終わりがありません。カレンダーにこの系列が含まれる場合、timeMin または singleEvents=true を使って一覧取得・展開するときは timeMax が必須です。
Calendar API の互換性とイベント、繰り返し、時刻の扱いを参照してください。
OAuth
- テストユーザーを選べるローカル同意画面
- 承認と拒否、および
stateの保持 redirect_uriの完全一致チェックS256PKCE による認可コード交換- スコープの検証
- リフレッシュトークン、
/userinfo、トークンの失効
ローカル OAuthを参照してください。
差分同期
events.listでのnextSyncTokenとsyncToken- ページネーション中の固定スナップショット
status: "cancelled"の削除済みレコード410 fullSyncRequiredレスポンス
差分同期を参照してください。
Watch
events.watch、calendarList.watch、channels.stopsyncとexistsの Webhook- コールバック先のホスト許可リスト
- 最大 3 回の配信試行
- チャネルの有効期限
POST /__calendarpit/deliveries/drain
watch 通知を参照してください。
管理画面
- 日表示と週表示
- 基本的なイベントエディタ
- テストユーザーとカレンダーの管理
- OAuth 接続の失効
- watch チャネルと配信ログの確認
- マスク処理済みのリクエスト履歴
- Fixture の読み込みとリセット操作
テスト制御
- JSON 形式の起動時 Fixture。固定 ID も任意で指定できます。
/__calendarpit/*の管理 API エンドポイント。状態の確認、Fixture の読み込み、リセット基準(起動時の状態)へのリセット、データ管理ができます。
動作の保証
デフォルトでループバックにバインド
--hostまたはCALENDARPIT_HOSTを指定しない限り、サーバーは127.0.0.1にバインドします。Dockerfileは、コンテナ内で明示的に0.0.0.0にバインドします。- Webhook のコールバック先は、
CALENDARPIT_WATCH_HOSTSを設定しない限りループバックホストだけです。
未対応機能に対する明示的なエラー
未対応の機能には、明示的なエラーを返します。
| リクエスト | ステータス | エラー |
|---|---|---|
| 未対応の Calendar API エンドポイント | 501 | reason: "unsupportedFeature" |
| 未対応のクエリパラメータ・フィールド・繰り返しルール | 400 | reason: "unsupportedFeature" |
events.import | 404 | notFound |
text パラメータ付きの events.quickAdd | 400 | unsupportedFeature |
text なしの events.quickAdd | 404 | notFound |
| 未対応のパラメータを含む OAuth 認可リクエスト | 400 | error: "unsupported_feature" |
| 未対応の OAuth エンドポイント | 501 | error: "unsupportedFeature" |
未対応の繰り返しルールは 400 unsupportedFeature で拒否します。ルールを無視して単発イベントとして保存することはありません。
Google への通信なし
- Calendarpit が Google に API、OAuth、Webhook のリクエストを送ることはありません。
- 管理画面は Google Fonts から Roboto Mono フォントを読み込みます。システムフォントへのフォールバックがあります。
再現可能なリセット
POST /__calendarpit/reset は、起動時 Fixture を同じ ID とカレンダーリストの ETag で復元します。次のものを消去します。
- トークン
- watch チャネル
- リクエスト履歴
- カレンダーリストの設定
- カレンダーの削除済みレコード
- 各種変更
再起動時の動作はストレージによって異なります。
- SQLite: リセット基準、カレンダーリストの設定、削除済みレコードは再起動後も残ります。
:memory:: 再起動をまたいで同じ ID を使うには、Fixture で固定 ID を指定してください。
対象外の機能
次の機能は実装していません。
ACL、共有カレンダー、委任
- テストユーザーがアクセスできるのは、自分が所有するカレンダー(
accessRole: "owner")だけです。 - すべての
acl.*メソッドは501 unsupportedFeatureを返します。 calendarList.insertは、calendarList.deleteでリストから外した所有カレンダーを戻すだけです。- 新しいカレンダーの作成や、他ユーザーのカレンダーの購読はしません。
- 対象のカレンダー(
primaryを含む)がすでにリストにある場合は409 duplicateを返します。
ユーザー設定、ACL とユーザー設定の watch、カレンダーリストの同期トークン
次のものには対応していません。
- すべての
settings.*メソッドは501 unsupportedFeatureを返します。 - ACL とユーザー設定の watch 購読(
acl.watch、settings.watch)は501 unsupportedFeatureを返します。 calendarList.listはnextSyncTokenを発行しません。syncTokenを指定すると400 unsupportedFeatureで拒否します。
calendarList.watch は、所有ユーザーのカレンダーリストを対象に実装しています。ただし次の制限があります。
paramsオブジェクトを指定すると400 unsupportedFeatureで拒否します。existsを送るのは、保存済みのリスト設定を変える書き込みがあったときだけです。- 管理 API でそのユーザーのカレンダーを作成・更新・削除したときも
existsを送ります。
showDeleted=true を指定した場合は次のとおりです。
calendarList.deleteで外したエントリは、保存済みの設定を保持します。- 管理 API で削除したカレンダーの削除済みレコードは、デフォルト設定で表示されます。削除時にプライマリだった場合は
primary: trueを保持します。
カレンダーの書き込み、カラー定義、events.import / quickAdd / move
次のメソッドは実装していません。
calendars.insert、update、patch、delete、clear、transferOwnershipcolors.getevents.import、quickAdd、move
カレンダーの作成・変更・削除は、管理画面または管理 API で行ってください。詳細はCalendar API の互換性を参照してください。
実際の招待メールとリマインダーメールの送信
sendUpdatesはメールを送らず、送信対象となる通知レコードを/__calendarpit/stateに記録します。- イベントのリマインダー、
calendarListのdefaultRemindersとnotificationSettingsは保存されます。ただし通知の発火やメール送信はしません。 notificationSettingsは、重複する通知type値を400 invalidParameterで拒否します。
実際の Google Meet 会議
会議作成リクエストは、再現可能な https://meet.calendarpit.invalid/<id> の URL を生成します。実際のオンライン会議は作成しません。
完全な OIDC
次のものには対応していません。
- パスワード認証
- 実際の Google ログイン
id_tokenの発行- JWKS・ディスカバリエンドポイント
他プロトコルおよび実際の Google Calendar との同期
Microsoft Graph、CalDAV、実際の Google Calendar との同期は対象外です。
管理画面でのドラッグによるイベント操作
- ドラッグによるイベントの移動や時間変更は実装していません。
- 管理画面のカレンダー表示では、繰り返しの系列(展開した各回ではなく親イベント)を開始日にだけ表示します。保存済みの例外は、それぞれの該当日に表示します。
- 管理画面のエディタは基本フィールドだけを扱います。
管理画面のエディタでイベントを保存すると、基本フィールドだけで上書きします。次のものは削除されます。
- 繰り返し設定(保存済みの例外もキャンセルされます)
- 参加者(招待コピーもキャンセルされます)
- 会議データ
- 色
- 公開範囲
カスタムリマインダーはデフォルト(reminders: { useDefault: true })に戻ります。これらのフィールドは API で設定してください。
Fixture は、指定したフィールドをそのまま保存します。招待コピーや擬似的な会議リンクは生成しません。詳細はFixtureを参照してください。
Calendarpit で再現できるワークフロー
Calendarpit では、次の E2E の連携ワークフローを再現できます。Google SDK を使ったサンプルフローはSDK の使用例を参照してください。
- ローカルの同意画面でテストユーザーとして認可し、カレンダーを選びます。
events.watchチャネルを登録し、初回のsync通知を受け取ります。- イベントを作成し、
freebusy.queryで busy 区間を確認します。 - イベントを更新・削除し、
exists通知を受け取って状態を再取得します。 channels.stopで watch チャネルを停止し、以降の変更が配信されないことを確認します。- 接続を失効させ、OAuth フローで再接続します。
- 同意画面で拒否し、
stateが保持され、既存のトークンが失われないことを確認します。 POST /__calendarpit/resetでリセットし、同じ Fixture と ID でフローを繰り返します。events.listでフル同期のスナップショットを取得します。その後syncTokenで、変更差分とstatus: "cancelled"の削除済みレコードを取得します。- 終日イベント(
dateTime: nullを含む)、繰り返しイベントの例外、ローカルでの出欠回答(RSVP)、擬似的な会議リンクを検証します。
現在のステータス
- 配布: Docker イメージ、Homebrew Tap、単体実行ファイルを公開しています。導入方法はインストールを参照してください。
- 単体実行ファイル: Bun、API、管理画面を内蔵します。配布対象は macOS arm64/x64、Linux glibc arm64/x64、Windows x64 です。公開前に各環境で検証します。
- Docker: 非 root ユーザー、永続ボリューム、ヘルスチェック、メモリモードを設定しています。リリース処理では、amd64/arm64 の起動と保存データのライフサイクルを検証します。