テストに使う

テスト制御 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/fixturesFixture のデータを検証して置き換えます(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/clientsOAuth クライアントを登録します(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/resetPOST /__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();
}

目次