テスト制御 API
自動テストの中で、HTTP を使って状態のリセット、Fixture の読み込み、リクエスト履歴の確認、Webhook 配信のドレインを行います。
Calendarpit には、/__calendarpit の下にローカルの管理 API があります。この API で、データの投入、サーバー状態の確認、テストの実行ごとのリセットができます。
リクエストのルール
-
ベースパス: 管理エンドポイントのパスは、すべて
/__calendarpitで始まります。例:http://127.0.0.1:4173/__calendarpit/reset -
認証なし: 管理エンドポイントにベアラートークンは不要です。Calendarpit はローカル開発用のサーバーで、デフォルトではループバックアドレスにバインドします。
-
書き込みには
Content-Type: application/jsonが必要:GETとHEAD以外のリクエストでは、application/jsonで始まるContent-Typeヘッダーを送ってください(大文字と小文字は区別されます)。DELETEのルートや、ボディを受け取らないPOSTエンドポイント(/reset、/deliveries/drainなど)も対象です。- このヘッダーがない場合、リクエストは HTTP
415(unsupportedMediaType)で失敗します。 - ボディを受け取らない
POSTエンドポイントには、空のボディか{}を送れます。
-
同一オリジンのチェック: 次のどちらかに当てはまるリクエストは、HTTP
403(forbidden)で失敗します。Originヘッダーがサーバーのオリジンと一致しない。Sec-Fetch-Site: cross-siteが付いている。
curlやテストランナーのようなブラウザ以外のクライアントは、通常Originを送らないため許可されます。 -
ヘルスチェックは対象外:
/__calendarpit/healthはこれらのチェックを受けません。GETやHEADによる起動確認に追加のヘッダーは要りません。 -
履歴に残らない:
/__calendarpit/*へのリクエストは、プロトコルのリクエスト履歴に記録されません。 -
エラー形式: 管理 API のエラーは、Google の JSON エラー形式に
domain: "calendarpit"を入れて返されます。
コンテナ内などでループバック以外のアドレスにバインドした場合は、そのポートに到達できるすべてのクライアントがこれらのエンドポイントを呼び出せます。
管理エンドポイント
次の表のパスは、すべて /__calendarpit からの相対パスです。
| メソッド | パス | 動作 |
|---|---|---|
GET / HEAD | /health | サーバーの情報と機能フラグを返します。 |
GET | /state | サーバー状態のスナップショットを返します。 |
POST | /fixtures | Fixture のデータを検証して置き換えます(200 でスナップショット)。 |
POST | /reset | 起動時 Fixture のリセット基準を復元します(200 でスナップショット)。 |
GET / DELETE | /history | 直近 200 件のプロトコルリクエストを取得(200)・クリア(204)します。 |
POST | /users | ユーザーを作成します(201)。 |
PATCH / DELETE | /users/{id} | ユーザーを編集・削除(204)します。 |
POST | /calendars | カレンダーを作成します(201)。 |
PATCH / DELETE | /calendars/{id} | カレンダーを編集・削除(204)します。 |
GET / POST | /calendars/{id}/events | イベントを一覧取得・作成(200)します。 |
GET / PUT / PATCH / DELETE | /calendars/{id}/events/{eventId} | イベントを取得・置き換え・部分更新・削除(204)します。 |
POST | /clients | OAuth クライアントを登録します(201)。 |
PATCH / DELETE | /clients/{id} | OAuth クライアントを編集・削除(204)します。 |
POST | /connections/{id}/revoke | 接続のトークンを取り消し、その接続の watch チャネルを停止します(204)。 |
POST | /deliveries/drain | 待機中の Webhook 配信が終わるまで待ち、配信ログを返します(200)。 |
エンドポイントの詳細
/health: サーバーのname("Calendarpit")、version、stage、storage("memory"または"sqlite")を返します。機能フラグcalendar、oauth、watch、fixturesも返します。HEADは空の200を返します。/state: まず期限切れのチャネルを削除します。そのうえで、storage、users、calendars、events、clients(シークレットなし)、connections、channels(tokenは伏せ字)、deliveries、notifications、revisionのスナップショットを返します。/fixtures: ユーザー、カレンダー、イベント、クライアントを検証して置き換えます。ランタイム状態をクリアし、接続を無効にします。/reset: 起動時 Fixture のリセット基準を復元し、ランタイム状態をすべてクリアします。/history:GETはリクエストを古い順に返します。/users:name、email、省略可能なidを受け取ります。ID がすでにある場合は409 duplicateを返します。/users/{id}:PATCHでnameとemailを編集します。idは変更できません。ユーザーを削除すると、そのユーザーが所有するデータも削除されます。/calendars:userId、summary、省略可能なtimeZone、primary、idを受け取ります。/calendars/{id}:idとuserIdは変更できません。/clients: Fixture のクライアントと同じ規則でid、secret、redirectUrisを受け取ります。ID がすでにある場合は409 duplicateを返します。レスポンスにシークレットは含まれません。/clients/{id}:PATCHでsecretとredirectUrisを編集します。secretを省略すると現在の値を維持します。idは変更できません。ユーザー、カレンダー、イベント、接続は残り、発行済みのトークンも有効なままです。削除したリダイレクト URI 宛ての保留中の同意リクエストと未交換の認可コードは破棄されます。クライアントを削除すると、その接続は取り消されます。/calendars/{id}/events: 一覧取得ではsyncTokenを含むevents.listのパラメータを使えます。一覧の各項目にはcalendarIdが入ります。/calendars/{id}/events/{eventId}:DELETEしても削除済みレコードは残ります。/deliveries/drain: 待機中の配信のリトライが終わるまで待ちます。レスポンスは配信ログの配列です。
連鎖的な削除と管理 API からの書き込み
- ユーザーの削除: ユーザーを削除すると、そのユーザーの OAuth 接続が取り消され、watch チャネルも停止します。ユーザーのカレンダーとイベントも削除されます。このとき、カレンダーの削除済みレコードは作られません。
- プライマリフラグ: ユーザーの最初のカレンダーは、自動的に
primary: trueになります。- 同じユーザーの別のカレンダーに
primary: trueを設定すると、フラグは 1 回の操作でそのカレンダーに移ります。元のプライマリを先に外す必要はありません。 primaryを直接外そうとすると400が返ります。
- 同じユーザーの別のカレンダーに
- カレンダーの削除: カレンダーを削除すると、次の処理が行われます。
- そのカレンダーのイベントを削除し、イベントの watch チャネルを停止します。
- デフォルトのリスト設定で
calendarListの削除済みレコードを記録します。summaryOverrideやcolorIdなどのユーザーごとのカスタマイズは消えます。 - 削除したカレンダーがプライマリだった場合は、所有者の別のカレンダーをプライマリにします。このとき、所有者のカレンダーリストに残っているカレンダーが優先されます。
- カレンダーリストの通知: 管理 API でカレンダーを作成(
POST /calendars)・編集(PATCH /calendars/{id})・削除するたびに、所有者のcalendarList.watchチャネル向けのexists通知がキューに入ります。- 編集では、内容が何も変わらなくても通知が送られます。
- テストで配信数を数える場合は、この分の配信も考慮してください。
- 管理 API からのイベント書き込み:
/__calendarpit/calendars/{id}/eventsでのイベントの書き込みは、Calendar API からの書き込みと次の点が違います。- 招待者に関する制限がかかりません。
conferenceDataVersion=1は不要です。- 読み取り専用のフィールド(
organizer、creator、iCalUID、recurringEventId、originalStartTime)も設定できます。 - 更新時には、保存済みの
organizer、creator、iCalUIDが保持されます。イベントにすでにrecurringEventIdとoriginalStartTimeがある場合は、それらも保持されます。
リセットと Fixture 読み込みの違い
POST /__calendarpit/reset と POST /__calendarpit/fixtures は、どちらもデータを置き換えてすぐに保存し、ランタイム状態をすべてクリアします。違いは読み込むデータです。
| 項目 | POST /__calendarpit/reset | POST /__calendarpit/fixtures |
|---|---|---|
| ユーザー、カレンダー、イベント、クライアント | 起動時の状態から復元 | 送信したFixture JSONで置き換え |
| 起動時のリセット基準 | 変わらない | 変わらない(次の /reset では元の起動時のデータが復元されます) |
| ランタイム状態 | クリア | クリア |
POST /__calendarpit/fixtures の検証は、すべて成功するかすべて失敗するかのどちらかです。無効なレコードが 1 件でもあれば、Calendarpit は 400 を返し、何も変更しません。
両方のエンドポイントでクリアされるもの
どちらのエンドポイントも、有効なwatch チャネルをすべて停止し、送信中の Webhook リクエストを中断します。さらに、次のものをクリアします。
- OAuth 接続、アクセストークンとリフレッシュトークン、認可コード、処理中の同意
- リクエスト履歴
- 同期トークンと、イベントおよびカレンダーリストのページトークン
- カレンダーの削除済みレコード
- ユーザーごとのカレンダーリスト設定(
calendarList.insert/patch/updateによるカスタマイズと、calendarList.deleteによる削除) - 招待の通知と、すべての配信ログレコード
どちらかを呼び出した後は、次のようになります。
- 古いアクセストークンは
401(authError)で失敗します。 - 古い同期トークンは
410(fullSyncRequired)で失敗します。
リセットしても、SQLite のデータベースファイルは削除されません。ディスク上の Fixture ファイルも変更されません。
リクエスト履歴と状態のエクスポート
リクエスト履歴
GET /__calendarpit/history は、/calendar/v3/*、/oauth*、/userinfo で処理した直近 200 件のリクエストを返します。並びは古い順です。401 や 501 などの失敗したリクエストも含まれます。
Calendarpit は、履歴の中の認証情報を次のようにマスクします。
- 次のキーの値は
"[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、state。ヘッダー、ボディ、クエリ文字列、LocationやRefererの URL が対象です。 - URL に含まれる認証情報は削除されます。
- 12,000 文字を超える文字列は途中で切られ、末尾に
[truncated]が付きます。
状態のエクスポート
GET /__calendarpit/state は確認用です。その出力を POST /__calendarpit/fixtures で読み込み直すことはできません。理由は次のとおりです。
- クライアントのシークレットが含まれません。
clientsにはidとredirectUrisだけが入ります。 - チャネルのトークンは
"[REDACTED]"と表示されます。 - ランタイムのフィールド(
storage、connections、channels、deliveries、notifications、revision)が含まれます。POST /__calendarpit/fixturesはこれらを400(unsupportedFeature)で拒否します。 - カレンダーリストの設定は含まれません。設定は
calendarList.list/getで取得してください。
データの投入や再読み込みには、元の Fixture ファイルを残しておき、それを使ってください。
再現可能なテストの書き方
- シナリオごとにリセットする: 各テストの前に
POST /__calendarpit/resetを呼び出して、起動時の状態に戻します。特定の Fixture を読み込みたい場合は、代わりにPOST /__calendarpit/fixturesを呼び出します。 - 固定の ID を使う: Fixture ファイルで、ユーザー、カレンダー、イベント、クライアントに
idを明示してください。ID がない場合、Calendarpit はデータ投入時に一度だけ ID を生成します。:memory:モードでは、サーバープロセスを起動するたびに新しい ID が生成されます。 - 並列ワーカーを分離する: テストワーカーごとに、
--port 0と--database :memory:を付けて Calendarpit のプロセスを 1 つずつ起動します。ワーカーごとに別の SQLite ファイルを使う方法もあります。 - ポートは標準出力から読み取る:
--port 0を使う場合は、起動時の行Admin http://127.0.0.1:<port>/から URL を読み取ります。たとえば/Admin\s+(http:\/\/[^\s]+)/でマッチできます。その後、GET /__calendarpit/healthが応答するまでポーリングしてから、テストを始めてください。 - スリープではなく Webhook 配信をドレインする:
POST /__calendarpit/deliveries/drainは、呼び出した時点で待機中の配信が、リトライも含めて終わるまで待ちます。 - リセット時の中断に備える:
/resetは送信中の Webhook リクエストを中断します。ただし、中断される前に受信側がそのリクエストを受け取っている場合があります。 - 再起動後は状態を取得し直す: SQLite での再起動をテストする場合、停止前にキューに入っていた配信は
cancelledになり、再送されません。再起動後に、カレンダーの状態をもう一度取得してください。
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();
}