Skip to content
This repository was archived by the owner on Aug 26, 2026. It is now read-only.

[Phase1] test/db/ RLS検証(最小8項目 + 権限マトリクス全項目) - #32

Merged
reitojike merged 20 commits into
mainfrom
26-phase1-db-rls-tests
Aug 6, 2026
Merged

reitojike merged 20 commits into
mainfrom
26-phase1-db-rls-tests

Conversation

@reitojike

@reitojike reitojike commented Aug 6, 2026

Copy link
Copy Markdown
Owner

概要

Issue #26 の対応。docs/permissions.md の権限マトリクスと最小検証セットに基づき、test/db/ に実DBに対するRLS検証テストを実装する。

実装内容

  • test/db/helpers.tsauth.signUpで実際のテストユーザー(認証済みクライアント)を作成するヘルパー。service_roleキーは一切使わない
  • profiles.test.ts / events.test.ts / event-participants.test.ts / ticket-entries.test.ts / expenses.test.ts / budgets.test.ts — 各テーブルのRLSポリシーを否定側中心に検証
  • .github/workflows/supabase.ymldb-test ジョブに supabase status -o env の出力を環境変数として渡すステップを追加(API_URL / ANON_KEY の2行のみに絞ってGITHUB_ENVへ)

特に否定側で検証している内容

  • 削除ガードが公開・非公開どちらの参加者でもすり抜けないこと
  • 招待経路でvisibility/participation_stateを上書きできないこと
  • 参加登録していないユーザーが他人を招待できないこと
  • profiles_public経由ではemail/is_adminが漏れないこと
  • 本人でもis_adminを書き換えられないこと、profilesをDELETEできないこと
  • 削除後もオーナー/支出保持者はイベントを閲覧できるが、無関係者は見えないこと
  • budgetsのNULLS NOT DISTINCT制約(全ジャンル合算枠の重複禁止)の回帰確認
  • 「他人の◯◯は更新・削除できない」系は、RETURNINGが空になるだけでなく本人視点で値が実際に変化していないこと・行が消えていないことまで確認(USING句破損を検出できない穴を修正)

Test plan

  • db-testジョブで全テスト(45件)がCIでpassすることを確認済み
  • 招待INSERTの実装バグ(招待者自身のセルフ参照RLSがRETURNINGを阻む問題)を、実際にCIで赤くなった状態から調査・修正する過程で実施済み(コミット履歴参照)。SECURITY DEFINER関数化・トリガー化まで試した末に真因を特定し、余計な変更は差し戻した

docs/permissions.mdの権限マトリクスと最小検証セットに基づき、
実際にauth.signUpで作成したユーザーのクライアントでRLSを検証する。
service_roleキーは一切使わない。

- profiles: 本人のみ全カラムSELECT、profiles_publicビュー経由の列制限、
  is_adminの書き換え拒否、DELETE不可
- events: 全員SELECT(未削除)、他人による編集不可、なりすまし登録不可、
  削除ガード(公開/非公開どちらの参加者でも失敗)、削除後もオーナー/
  支出保持者は閲覧可、無関係者は閲覧不可
- event_participants: 自己登録、なりすまし不可、招待(visibility/
  participation_state固定)、未参加者による招待不可、他人の行の
  更新・削除不可、visibility別の可視性
- ticket_entries/expenses/budgets: 本人のみ全操作、他人からは不可視
- budgets: NULLS NOT DISTINCT制約の重複防止を回帰テストとして固定

CI(.github/workflows/supabase.yml)でsupabase status -o envの出力を
環境変数として渡し、supabase startしたローカルインスタンスに対して
yarn test:dbを実行する。

Closes #26
Copilot AI lite review requested due to automatic review settings August 6, 2026 04:32
クォート付きのまま渡すとAPI_URLが"http://..."となりsupabase-jsの
URLバリデーションに失敗していた。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Issue #26 に対応し、docs/permissions.md の権限マトリクス/最小検証セットに基づいて test/db/ で実DBのRLS検証(否定側中心)を追加し、CIの db-test でSupabase接続情報(API_URL / ANON_KEY)を受け渡せるようにするPRです。

Changes:

  • test/db/helpers.ts を追加し、auth.signUp で作成した実セッションのクライアントでRLS検証できるようにした
  • 各テーブル(profiles/events/event_participants/ticket_entries/expenses/budgets)のRLS検証テストを test/db/ に追加
  • .github/workflows/supabase.ymldb-testsupabase status -o env の出力を GITHUB_ENV 経由で渡すステップを追加

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
yarn.lock @supabase/supabase-js 追加に伴うロック更新
package.json DBテスト用に @supabase/supabase-js を依存追加
.github/workflows/supabase.yml db-test 実行時に API_URL / ANON_KEY を環境変数へエクスポート
test/db/helpers.ts auth.signUp で作成した認証済みクライアント/イベント作成ヘルパーを追加
test/db/profiles.test.ts profiles / profiles_public のRLS・カラム露出の否定側検証を追加
test/db/events.test.ts events の更新・削除ガード/削除後閲覧のRLS検証を追加
test/db/event-participants.test.ts 参加登録/招待/visibility/他人操作不可のRLS検証を追加
test/db/ticket-entries.test.ts ticket_entries の本人のみCRUD・他人不可のRLS検証を追加
test/db/expenses.test.ts expenses の本人のみCRUD・他人不可のRLS検証を追加
test/db/budgets.test.ts budgets の本人のみCRUD・他人不可 + NULLS NOT DISTINCT回帰検証を追加
test/db/.gitkeep test/db/ ディレクトリ維持用ファイル
Suppressed comments (6)

test/db/budgets.test.ts:53

  • const id = created?.id ?? "" は、setupのinsertが失敗して created がnullでもテストが進み、空IDに対するupdate/deleteが0件で通ってしまう可能性があります。setupのerror/dataを検証してから created.id を使ってください。
      amount: 10000,
    })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/budgets.test.ts:80

  • 本人のDELETEは error === null だけだと「0件削除(実は権限が無い)」でも通ってしまいます。setupのinsert成功を検証した上で、deleteで返る行数も確認して「本当に削除できた」ことを担保してください。
  const { error } = await user.client.from("budgets").delete().eq("id", created?.id ?? "");
  expect(error).toBeNull();

test/db/expenses.test.ts:50

  • const id = created?.id ?? "" はsetupのinsert失敗時に空IDでupdate/deleteして0件となり、RLSの回帰を見逃す可能性があります。insertのerror/dataを検証してから created.id を使ってください。
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/expenses.test.ts:73

  • 本人のDELETEを error === null だけで判定すると、0件削除でも通ってしまいます(RLSが効いていない/効きすぎている回帰を検出できない)。setupのinsert成功を検証し、deleteの返却行数も確認してください。
  const { error } = await self.client.from("expenses").delete().eq("id", created?.id ?? "");
  expect(error).toBeNull();

test/db/ticket-entries.test.ts:50

  • const id = created?.id ?? "" はsetupのinsert失敗時に空IDでupdate/deleteして0件となり、RLSの回帰を見逃す可能性があります。insertのerror/dataを検証してから created.id を使ってください。
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/ticket-entries.test.ts:80

  • 本人のDELETEを error === null だけで判定すると、0件削除でも通ってしまいます。setupのinsert成功を検証し、deleteの返却行数も確認して「本当に削除できた」ことを担保してください。
  const { error } = await self.client
    .from("ticket_entries")
    .delete()
    .eq("id", created?.id ?? "");
  expect(error).toBeNull();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/db/budgets.test.ts Outdated
Comment thread test/db/budgets.test.ts Outdated
Comment on lines +22 to +38
const { data: created } = await self.client
.from("budgets")
.insert({
user_id: self.userId,
period_type: "monthly",
period_start: "2026-08-01",
amount: 10000,
})
.select()
.single();

const { data, error } = await stranger.client
.from("budgets")
.select()
.eq("id", created?.id ?? "");
expect(error).toBeNull();
expect(data).toHaveLength(0);
Comment thread test/db/expenses.test.ts Outdated
Comment on lines +24 to +35
const { data: created } = await self.client
.from("expenses")
.insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
.select()
.single();

const { data, error } = await stranger.client
.from("expenses")
.select()
.eq("id", created?.id ?? "");
expect(error).toBeNull();
expect(data).toHaveLength(0);
Comment thread test/db/ticket-entries.test.ts Outdated
Comment on lines +24 to +35
const { data: created } = await self.client
.from("ticket_entries")
.insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
.select()
.single();

const { data, error } = await stranger.client
.from("ticket_entries")
.select()
.eq("id", created?.id ?? "");
expect(error).toBeNull();
expect(data).toHaveLength(0);
Comment thread test/db/events.test.ts
Comment on lines +85 to +90
const { error } = await owner.client
.from("events")
.update({ deleted_at: new Date().toISOString() })
.eq("id", event.id);
expect(error).toBeNull();
});
Comment thread test/db/events.test.ts Outdated
Comment on lines +95 to +99
await owner.client
.from("events")
.update({ deleted_at: new Date().toISOString() })
.eq("id", event.id);

Comment thread test/db/events.test.ts Outdated
Comment on lines +127 to +135
await spender.client.from("expenses").insert({
user_id: spender.userId,
event_id: event.id,
category: "ticket",
});
await owner.client
.from("events")
.update({ deleted_at: new Date().toISOString() })
.eq("id", event.id);
Copilot AI review requested due to automatic review settings August 6, 2026 04:37
Comment thread test/db/events.test.ts
Comment on lines +16 to +34
test("無関係のユーザーは他人のイベントを編集できない", async () => {
const [owner, stranger] = await Promise.all([createTestUser(), createTestUser()]);
const event = await createEvent(owner);

const { data, error } = await stranger.client
.from("events")
.update({ title: "hijacked" })
.eq("id", event.id)
.select();
expect(error).toBeNull();
expect(data).toHaveLength(0);

const { data: unchanged } = await owner.client
.from("events")
.select("title")
.eq("id", event.id)
.single();
expect(unchanged?.title).toBe("test event");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/permissions.mdの「最小の検証セット」8項目のうち、「オーナー以外がイベントを削除 → 失敗」に対応するテストが見当たりません。このファイルには「オーナー以外がイベントを更新できない」テスト(16-34行目)はありますが、deleted_atを設定しようとする削除の試行はすべてowner.clientから行われており(47, 64, 81, 95, 111, 124行目)、無関係のユーザーやオーナー以外が削除を試みて弾かれることを確認するテストがありません。

events_update_owner_onlyポリシーはUPDATE全般(title変更もdeleted_at設定も)を同じUSING/WITH CHECK句でカバーしているため実質的には守られているはずですが、最小検証セットが明示的にこの項目を独立して挙げているのは、将来DELETE専用ポリシーや別のガード条件に分岐した場合の回帰を検出するためだと考えられます。ポリシーを一時的に無効化して赤くなることを確認する対象としても、独立したテストを追加した方が安全です。

Comment on lines +57 to +76
test("招待時にvisibilityをpublicへ上書きしようとすると失敗する", async () => {
const [owner, inviter, invitee] = await Promise.all([
createTestUser(),
createTestUser(),
createTestUser(),
]);
const event = await createEvent(owner);
await inviter.client
.from("event_participants")
.insert({ event_id: event.id, user_id: inviter.userId, status: "considering" });

const { error } = await inviter.client.from("event_participants").insert({
event_id: event.id,
user_id: invitee.userId,
invited_by: inviter.userId,
status: "considering",
visibility: "public",
});
expect(error).not.toBeNull();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR概要には「招待経路でvisibility/participation_stateを上書きできないこと」を否定側で検証したとありますが、実際にこのファイルで明示的に失敗を確認しているのはvisibilityのみです(このテスト)。participation_stateについては31-55行目の正常系テストで挿入後の値が"joined"であることをアサートしているだけで、招待時にparticipation_stateを明示的に上書きしようとして失敗する、という否定側のテストがありません。

docs/testing.mdが「正常系だけのテストは、権限が全開放されていても通る。否定側が本体」と明記している通り、31-55行目のテストはpayloadにparticipation_stateを含めていないため、仮にwith check句からparticipation_state = 'joined'の制約が抜け落ちても(visibility同様に)このテストは通り続けてしまいます。57-76行目と対になるparticipation_stateの上書き失敗テストの追加をおすすめします。

Comment thread package.json Outdated
"gen:types": "supabase gen types typescript --local > supabase/types.ts"
},
"dependencies": {
"@supabase/supabase-js": "^2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: @supabase/supabase-jsは現時点でtest/db/helpers.tsからのみ参照されており(lib/はまだ.gitkeepのみ)、dependenciesではなくdevDependenciesの方が実態に合いそうです。lib/で実際にSupabaseクライアントを使う実装が入るタイミングでdependenciesに昇格させる形でも良いかもしれません。ブロッカーではありません。

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

レビュー総評

docs/permissions.mddocs/testing.mdの方針に沿って、test/db/にRLSの実DB検証を追加するPR。全体としてよく作られている。

良い点

  • service_roleキーを一切使わず、auth.signUpで実際に認証したクライアントのみでテストしている(test/db/helpers.ts)。docs/permissions.md「RLS検証の必須要件」の1番目を満たす
  • 否定側(弾かれること)を本体として書けている。profilesis_admin書き換え不可・削除不可、event_participantsの招待経路でのvisibility固定、参加者がいるイベントの削除拒否など、静かに失敗しうる箇所を狙って検証している
  • budgetsNULLS NOT DISTINCT制約や、イベント削除後もオーナー/支出保持者だけが閲覧できる、といった細かい回帰ポイントも押さえている
  • .github/workflows/supabase.ymlGITHUB_ENVへの値渡しでダブルクォート除去を修正しており(2つ目のコミット)、CIでの接続情報受け渡しの不具合に対処済み
  • asキャスト(型ガード迂回)・anyeslint-disable/@ts-ignoreによる黙殺は見当たらない。生成型(Database)をそのまま使っており型の二重定義もない

指摘した点(インラインコメント参照)

  1. docs/permissions.mdの「最小の検証セット」8項目のうち、「オーナー以外がイベントを削除 → 失敗」に対応する独立したテストがevents.test.tsに無い(更新の拒否テストはあるが削除は常にオーナー自身が実行している)
  2. event-participants.test.tsで、招待経路のvisibility上書き失敗は明示的にテストされているが、PR概要が言及しているparticipation_stateの上書き失敗は否定側テストが無く、正常系での値アサートのみに留まっている
  3. (nit) @supabase/supabase-jsが現状test/db/からのみ参照されているのにdependenciesに追加されている。ブロッカーではない

スコープ外として妥当と判断した点
docs/permissions.mdは権限判定を「RLS」と「アプリ層のpure関数」の二層で実装・テストすることを求めているが、common/test/unit/は現状空(.gitkeepのみ)。このPRはIssue #26の範囲(RLS層のみ)に明示的にスコープされており、アプリ層の実装は別Issueで進める前提と読めるため、今回は指摘としない。

1と2は「弾かれるはずの経路が本当に弾かれているか」を直接検証していない箇所で、docs/testing.mdの「正常系だけのテストは、権限が全開放されていても通る」という原則に照らすと埋めておきたいギャップ。追加を推奨するが、実装(RLSポリシー)自体はdocs/permissions.mdのマトリクス通りで問題ない。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

Suppressed comments (16)

test/db/budgets.test.ts:2

  • このファイル内で Database 型を参照して satisfies を使うため、生成型の import を追加してください(型アサーション as を避けられます)。
import { expect, test } from "vitest";
import { createTestUser } from "./helpers";

test/db/budgets.test.ts:90

  • as const による型アサーションは避け、satisfies で Insert 型に一致することを検証してください(テストコードでも型の握りつぶしを避けたいです)。
    period_type: "monthly" as const,
    period_start: "2026-09-01",
    amount: 10000,
  };

test/db/budgets.test.ts:26

  • 作成用 insert の結果(特に error と created.id)を検証せずに created?.id ?? "" で続行すると、前提作成が失敗してもテストが偽陽性で通る可能性があります。insert の error と作成結果を明示的に検証してください。
  const { data: created } = await self.client
    .from("budgets")
    .insert({
      user_id: self.userId,
      period_type: "monthly",

test/db/budgets.test.ts:47

  • 作成用 insert が失敗して created が null でも id = created?.id ?? "" だと update/delete が 0 件になり、意図と違う理由でテストが通る可能性があります。insert の error と created.id を先に検証してから id を使ってください。
  const { data: created } = await self.client
    .from("budgets")
    .insert({
      user_id: self.userId,
      period_type: "monthly",

test/db/budgets.test.ts:72

  • 前提となる budget 作成に失敗しても created?.id ?? "" で delete してしまうと、削除が 0 件でも error が null のままになり偽陽性になる可能性があります。insert の error/id を検証し、確実に作成できた id で削除してください。
  const { data: created } = await user.client
    .from("budgets")
    .insert({
      user_id: user.userId,
      period_type: "monthly",

test/db/ticket-entries.test.ts:28

  • 作成用 insert の結果(特に error と created.id)を検証せずに created?.id ?? "" で続行すると、前提作成が失敗してもテストが偽陽性で通る可能性があります。insert の error と作成結果を明示的に検証してください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();

test/db/ticket-entries.test.ts:50

  • 前提となる ticket_entry 作成に失敗しても id = created?.id ?? "" だと update/delete が 0 件になり、意図と違う理由でテストが通る可能性があります。insert の error/id を検証してから id を使ってください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/ticket-entries.test.ts:74

  • 前提となる ticket_entry 作成に失敗しても created?.id ?? "" で delete すると、削除 0 件でも error が null のままになり偽陽性になり得ます。insert の error/id を検証し、作成できた id で削除してください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();

test/db/expenses.test.ts:28

  • 作成用 insert の結果(特に error と created.id)を検証せずに created?.id ?? "" で続行すると、前提作成が失敗してもテストが偽陽性で通る可能性があります。insert の error と作成結果を明示的に検証してください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();

test/db/expenses.test.ts:50

  • 前提となる expense 作成に失敗しても id = created?.id ?? "" だと update/delete が 0 件になり、意図と違う理由でテストが通る可能性があります。insert の error/id を検証してから id を使ってください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/expenses.test.ts:70

  • 前提となる expense 作成に失敗しても created?.id ?? "" で delete すると、削除 0 件でも error が null のままになり偽陽性になり得ます。insert の error/id を検証し、作成できた id で削除してください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();

test/db/event-participants.test.ts:127

  • 前提となる target の参加行作成に失敗しても、その後の update/delete が 0 件になりテストが偽陽性で通る可能性があります。前提 insert の成功(error=null)を検証してください。
    createTestUser(),
  ]);
  const event = await createEvent(owner);

test/db/event-participants.test.ts:153

  • 前提となる参加登録 insert の error を無視すると、参加登録ができていない状態でも delete が 0 件で成功扱いになり、テストが偽陽性で通る可能性があります。前提 insert の成功を検証してください。
test("本人は自分の参加登録を取りやめられる", async () => {
  const [owner, self] = await Promise.all([createTestUser(), createTestUser()]);
  const event = await createEvent(owner);

test/db/event-participants.test.ts:179

  • このテストは「非公開の参加行が他ユーザーから見えない」ことを検証していますが、前提 insert が失敗して行自体が存在しない場合でも同じ期待値(0件)になり偽陽性になります。前提 insert の成功(error=null)を検証してください。
    createTestUser(),
  ]);
  const event = await createEvent(owner);

package.json:19

  • 現状 @supabase/supabase-jstest/db/helpers.ts でのみ使われており、プロダクション依存に入れる理由がありません。不要な本番依存を避けるため devDependencies へ移動してください。
  "dependencies": {
    "@supabase/supabase-js": "^2",
    "next": "16.3.0",
    "react": "19.2.8",
    "react-dom": "19.2.8"

test/db/event-participants.test.ts:66

  • このテストは「参加登録済みであること」を前提に visibility 上書きの拒否を検証しています。前提 insert の error を無視すると、前提が満たせていない(=招待権限がない)理由で失敗してもテストが通ってしまう可能性があります。前提 insert の成功を明示的に検証してください。

This issue also appears in the following locations of the same file:

  • line 125
  • line 151
  • line 177
    createTestUser(),
  ]);
  const event = await createEvent(owner);

Comment thread .github/workflows/supabase.yml
Copilot AI review requested due to automatic review settings August 6, 2026 04:45
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

レビュー結果

docs/permissions.md の権限マトリクスに沿ってRLSを実DBで検証する方針自体は適切で、service_roleキー不使用・削除ガードのsecurity definer理由の明記・招待経路のvisibility/participation_state固定など、要注意ポイントはコード上でも押さえられています。型は生成型(Database)経由で、as/any/eslint-disableの握り消しもなく、common/への判断ロジック複製もありません(このPRの範囲では判断ロジック自体が存在しないため該当なし)。

主な指摘

  1. docs/permissions.mdの「最小の検証セット」のうち1項目が未実装: オーナー以外がイベントを削除 | 失敗 がテストされていません。events.test.ts は削除ガード(参加者がいる場合)は網羅していますが、「無関係ユーザー/オーナー以外が deleted_at を更新しようとしてevents_update_owner_onlyポリシーで弾かれる」ケースが抜けています。表の×が1つでもテストされていなければ検証されていない扱い、という本文の基準に従うとここは追加が必要そうです。

  2. PR本文が「招待経路でparticipation_stateを上書きできないこと」を検証済みとしていますが、実際はvisibilityの上書き失敗のみがテストされていますevent_participants_insert_self_or_inviteポリシーはparticipation_state = 'joined'固定も要求しているので、そちらを上書きしようとする否定系テストが欠けています。

  3. 一部の否定系テストで、フィクスチャ用INSERTのエラーを確認していません(event-participants.test.tsの「他人の参加行を更新・削除できない」など)。対象行の作成自体が(無関係な理由で)失敗した場合でも toHaveLength(0) は素通りするため、docs/permissions.mdが警告する「これだけでは何も検証していない」の形になり得ます。実際、招待テストの自己登録INSERTについては同種の懸念に対応する修正(3c3b647)が入っているので、同じ配慮を他の否定系テストのセットアップにも広げるのが一貫していると思います。

  4. @supabase/supabase-jsdependencies に追加されていますが、現時点の利用箇所は test/db/helpers.ts のみです。アプリ本体(app//lib/)からはまだ参照されていないため、devDependencies の方が実態に合いそうです。

良かった点

  • service_roleを一切使わない、否定側中心の検証というdocs/permissions.mdの要求に忠実
  • 削除ガードのsecurity definerが必要な理由、招待経路の固定理由など、非自明な設計判断がコード側コメントに残っている
  • CI側のクォート除去修正(846fe65)は原因(GITHUB_ENVがクォートを解釈しない)がコミットメッセージに明記されていて良い

CI(db-testジョブ)が実際にこの環境構成で通るかは、Docker無し環境のため確認できていません。CI結果での確認をお願いします。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

Suppressed comments (10)

test/db/budgets.test.ts:87

  • as const の型アサーションは不要です(このリポジトリでは型アサーションを避けたい)。ここは文字列リテラルのままで型が通るので削除できます。
    period_type: "monthly" as const,

test/db/ticket-entries.test.ts:33

  • セットアップ用のinsertで error を検証しておらず、created?.id ?? "" のフォールバックにより「insert自体が失敗してcreatedがundefinedでも、空文字idでselectして0件になりテストが通る」状態になっています。セットアップが成功したことを明示的に検証してから、実IDで検証してください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();

  const { data, error } = await stranger.client
    .from("ticket_entries")
    .select()
    .eq("id", created?.id ?? "");

test/db/ticket-entries.test.ts:56

  • セットアップ用のinsertで error を検証しておらず、created?.id ?? "" により、insert失敗時でも空文字idに対するupdate/deleteが0件扱いになってテストが通る可能性があります。セットアップ成功を検証し、実IDで更新/削除不可を確認してください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();
  const id = created?.id ?? "";

  const { data: updated } = await stranger.client
    .from("ticket_entries")
    .update({ provider: "hijacked" })
    .eq("id", id)
    .select();

test/db/expenses.test.ts:33

  • セットアップ用のinsertで error を検証しておらず、created?.id ?? "" のフォールバックにより「insert失敗でも空文字idでselectして0件になりテストが通る」状態になっています。セットアップ成功を検証してから、実IDで0件になることを確認してください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();

  const { data, error } = await stranger.client
    .from("expenses")
    .select()
    .eq("id", created?.id ?? "");

test/db/expenses.test.ts:56

  • セットアップ用のinsertで error を検証しておらず、created?.id ?? "" により、insert失敗時でも空文字idに対するupdate/deleteが0件扱いになってテストが通る可能性があります。セットアップ成功を検証し、実IDで更新/削除不可を確認してください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();
  const id = created?.id ?? "";

  const { data: updated } = await stranger.client
    .from("expenses")
    .update({ memo: "hijacked" })
    .eq("id", id)
    .select();

test/db/budgets.test.ts:37

  • セットアップ用のinsertで error を検証しておらず、created?.id ?? "" のフォールバックにより「insert失敗でも空文字idでselectして0件になりテストが通る」状態になっています。セットアップ成功を検証してから、実IDで0件になることを確認してください。
  const { data: created } = await self.client
    .from("budgets")
    .insert({
      user_id: self.userId,
      period_type: "monthly",
      period_start: "2026-08-01",
      amount: 10000,
    })
    .select()
    .single();

  const { data, error } = await stranger.client
    .from("budgets")
    .select()
    .eq("id", created?.id ?? "");
  expect(error).toBeNull();

test/db/budgets.test.ts:60

  • セットアップ用のinsertで error を検証しておらず、created?.id ?? "" により、insert失敗時でも空文字idに対するupdate/deleteが0件扱いになってテストが通る可能性があります。セットアップ成功を検証し、実IDで更新/削除不可を確認してください。
  const { data: created } = await self.client
    .from("budgets")
    .insert({
      user_id: self.userId,
      period_type: "monthly",
      period_start: "2026-08-01",
      amount: 10000,
    })
    .select()
    .single();
  const id = created?.id ?? "";

  const { data: updated } = await stranger.client
    .from("budgets")
    .update({ amount: 1 })
    .eq("id", id)
    .select();
  expect(updated).toHaveLength(0);

.github/workflows/supabase.yml:65

  • supabase status -o envSUPABASE_SERVICE_ROLE_KEY なども出力するため、現状のパイプだと service role キーも含めて $GITHUB_ENV に書き込まれてしまいます。また --override-name はCLIのヘルプ/ドキュメント上で一般的に確認できず、バージョンによっては失敗してdb-testが落ちるリスクがあります。必要な値だけを抽出して API_URL/ANON_KEY にマップする形にすると安全です。
          supabase status -o env --override-name api.url=API_URL --override-name auth.anon_key=ANON_KEY \
            | sed 's/^\([A-Z_]*\)="\(.*\)"$/\1=\2/' >> "$GITHUB_ENV"

package.json:20

  • @supabase/supabase-js が現状 test/db/helpers.ts でのみ使われているため、ランタイム依存ではなく devDependencies に置いた方が本番バンドル/インストール対象を増やさずに済みます。
  "dependencies": {
    "@supabase/supabase-js": "^2",
    "next": "16.3.0",
    "react": "19.2.8",
    "react-dom": "19.2.8"
  },

test/db/helpers.ts:37

  • createTestUser()auth.signUp を多用していますが、supabase/config.toml[auth.rate_limit] sign_in_sign_ups = 30 に対して、このPRのdbテスト全体ではサインアップ回数が30回を大きく超えるため、CIでレートリミットに当たりテストが不安定になるリスクがあります。テストスイート内でユーザーを再利用する(各ファイルで beforeAll で必要人数だけ作って使い回す等)など、サインアップ回数を抑える構成にするのが安全です。
export const createTestUser = async (): Promise<TestUser> => {
  const client = createAnonClient();
  const email = `test-${crypto.randomUUID()}@example.com`;
  const { data, error } = await client.auth.signUp({
    email,
    password: crypto.randomUUID(),
  });
  if (error || !data.user) {
    throw new Error(`テストユーザー作成に失敗しました: ${error?.message ?? "unknown error"}`);
  }
  return { client, userId: data.user.id };

Comment thread test/db/helpers.ts
@@ -0,0 +1,63 @@
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@/supabase/types";
Copilot AI review requested due to automatic review settings August 6, 2026 04:51
event_participants_insert_self_or_inviteのEXISTSサブクエリが招待者自身の
RLS越しに評価され、参加登録済みでも「参加登録していない」と誤判定されて
正当な招待が失敗するバグがCIのdb-testで発見された。SECURITY DEFINER関数
is_event_participant()でラップし、guard_event_deletion()と同じ方式で解消する。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (12)

.github/workflows/supabase.yml:65

  • supabase status -o env--override-name を付けていますが、Supabase CLI ではそのフラグがサポートされていない可能性が高く、ここでジョブが失敗します。必要なキー(API_URL / ANON_KEY)だけをフィルタして $GITHUB_ENV に書き込む形にすると、安全に service_role も除外できます。
          supabase status -o env --override-name api.url=API_URL --override-name auth.anon_key=ANON_KEY \
            | sed 's/^\([A-Z_]*\)="\(.*\)"$/\1=\2/' >> "$GITHUB_ENV"

test/db/ticket-entries.test.ts:33

  • このテストは created が作れなかった場合でも .eq("id", created?.id ?? "") が空文字で検索して 0 件になり、RLSが正しくても/壊れていてもテストが通ってしまいます。作成エラーを明示的に検証し、created.id を使って検索してください。
    .eq("id", created?.id ?? "");

test/db/ticket-entries.test.ts:50

  • created?.id ?? "" で空文字を使うと、作成に失敗しても更新/削除が 0 件のままになりテストが意図せず通る可能性があります。作成の error を検証してから created.id を使ってください。
  const id = created?.id ?? "";

test/db/helpers.ts:2

  • Vitest(db project) 側で tsconfig の path alias(@/*) が解決されない構成だと、この @/supabase/types import が実行時に解決できずテストが落ちます。テスト側は相対パス import にしておくと確実です。
import type { Database } from "@/supabase/types";

test/db/ticket-entries.test.ts:79

  • created?.id ?? "" だと、作成に失敗しても delete が空文字条件で 0 件のまま成功扱いになりテストが通ってしまいます。作成結果を検証して created.id を使ってください。
    .eq("id", created?.id ?? "");

test/db/expenses.test.ts:33

  • このテストは created が作れなかった場合でも .eq("id", created?.id ?? "") が空文字で検索して 0 件になり、テストが意図せず通る可能性があります。作成エラーを明示的に検証し、created.id を使って検索してください。
    .eq("id", created?.id ?? "");

test/db/expenses.test.ts:50

  • created?.id ?? "" で空文字を使うと、作成に失敗しても update/delete が 0 件のままになりテストが意図せず通る可能性があります。作成の error を検証してから created.id を使ってください。
  const id = created?.id ?? "";

test/db/expenses.test.ts:72

  • created?.id ?? "" だと、作成に失敗しても delete が空文字条件で 0 件のまま成功扱いになりテストが通ってしまいます。作成結果を検証して created.id を使ってください。
  const { error } = await self.client.from("expenses").delete().eq("id", created?.id ?? "");

test/db/budgets.test.ts:36

  • このテストは created が作れなかった場合でも .eq("id", created?.id ?? "") が空文字で検索して 0 件になり、RLSが壊れていてもテストが通ってしまいます。作成エラーを明示的に検証し、created.id を使って検索してください。
    .eq("id", created?.id ?? "");

test/db/budgets.test.ts:53

  • created?.id ?? "" で空文字を使うと、作成に失敗しても update/delete が 0 件のままになりテストが意図せず通る可能性があります。作成の error を検証してから created.id を使ってください。
  const id = created?.id ?? "";

test/db/budgets.test.ts:79

  • created?.id ?? "" だと、作成に失敗しても delete が空文字条件で 0 件のまま成功扱いになりテストが通ってしまいます。作成結果を検証して created.id を使ってください。
  const { error } = await user.client.from("budgets").delete().eq("id", created?.id ?? "");

test/db/budgets.test.ts:87

  • このリポジトリでは as キャスト禁止なので、"monthly" as const は避けてください。文字列リテラル型の変数を用意すればキャスト無しで同じ意図を表現できます。
    period_type: "monthly" as const,

Copilot AI review requested due to automatic review settings August 6, 2026 04:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (13)

test/db/helpers.ts:2

  • @/* の tsconfig paths は Vitest/Vite の解決ではデフォルトで効かないため、ここが Cannot find module '@/supabase/types' で落ちる可能性が高いです(vitest.config.ts に alias 設定や tsconfig-paths 系プラグインが無い)。テスト側は相対パスで import するか、別途 Vitest 側で alias を設定してください。
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@/supabase/types";

.github/workflows/supabase.yml:60

  • supabase status -o env は通常 SERVICE_ROLE_KEY も含め多数の値を出力します。このまま >> $GITHUB_ENV すると service_role キーも環境変数として流れ込み、コメントの「エクスポートしない」と矛盾します。必要な API_URL / ANON_KEY のみに絞って書き込むようにしてください。
      # test/db/ が認証済みクライアントでRLSを検証するために接続情報を渡す。
      # service_roleキーはエクスポートしない(test/db/では使わない。docs/permissions.md)。
      - name: Export local Supabase connection info
        if: steps.check.outputs.initialized == 'true'
        # supabase status -o env は値をダブルクォートで囲んで出力するが、

test/db/ticket-entries.test.ts:28

  • このテストは insert の error を検証していないため、作成に失敗して created が null の場合でも .eq("id", "") になってしまい、後続の「0件」を満たして誤ってパスする可能性があります。セットアップ(insert)が成功したことを先に assert してください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();

test/db/ticket-entries.test.ts:51

  • このテストも insert の失敗を検知できず、id が空文字になって update/delete が「0件」で通ってしまう可能性があります。作成が成功したことを assert してから id を使ってください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/ticket-entries.test.ts:79

  • delete 対象の insert 失敗時に created?.id ?? "" になると、削除が何も起きず error=null のままテストが誤ってパスし得ます。事前に insert 成功と id を確定させてください。
  const { data: created } = await self.client
    .from("ticket_entries")
    .insert({ event_id: event.id, user_id: self.userId, entry_type: "lottery" })
    .select()
    .single();

  const { error } = await self.client
    .from("ticket_entries")
    .delete()
    .eq("id", created?.id ?? "");

test/db/expenses.test.ts:28

  • このテストは insert の error を検証していないため、作成に失敗して created が null の場合でも .eq("id", "") になり、後続の「0件」で誤ってパスする可能性があります。セットアップ(insert)の成功を先に assert してください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();

test/db/expenses.test.ts:51

  • このテストも insert 失敗時に id が空文字になって update/delete が「0件」で通ってしまう可能性があります。作成が成功したことを assert してから id を使ってください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/expenses.test.ts:73

  • delete 対象の insert 失敗時に created?.id ?? "" になると、削除が何も起きず error=null のままテストが誤ってパスし得ます。事前に insert 成功と id を確定させてください。
  const { data: created } = await self.client
    .from("expenses")
    .insert({ event_id: event.id, user_id: self.userId, category: "ticket" })
    .select()
    .single();

  const { error } = await self.client.from("expenses").delete().eq("id", created?.id ?? "");
  expect(error).toBeNull();

test/db/budgets.test.ts:31

  • このテストは insert の error を検証していないため、作成に失敗して created が null の場合でも .eq("id", "") になり、後続の「0件」で誤ってパスする可能性があります。セットアップ(insert)の成功を先に assert してください。
  const { data: created } = await self.client
    .from("budgets")
    .insert({
      user_id: self.userId,
      period_type: "monthly",
      period_start: "2026-08-01",
      amount: 10000,
    })
    .select()
    .single();

test/db/budgets.test.ts:54

  • このテストも insert 失敗時に id が空文字になって update/delete が「0件」で通ってしまう可能性があります。作成が成功したことを assert してから id を使ってください。
  const { data: created } = await self.client
    .from("budgets")
    .insert({
      user_id: self.userId,
      period_type: "monthly",
      period_start: "2026-08-01",
      amount: 10000,
    })
    .select()
    .single();
  const id = created?.id ?? "";

test/db/budgets.test.ts:80

  • delete 対象の insert 失敗時に created?.id ?? "" になると、削除が何も起きず error=null のままテストが誤ってパスし得ます。事前に insert 成功と id を確定させてください。
  const { data: created } = await user.client
    .from("budgets")
    .insert({
      user_id: user.userId,
      period_type: "monthly",
      period_start: "2026-08-01",
      amount: 10000,
    })
    .select()
    .single();

  const { error } = await user.client.from("budgets").delete().eq("id", created?.id ?? "");
  expect(error).toBeNull();

test/db/budgets.test.ts:89

  • このリポジトリでは as キャスト禁止です(CLAUDE.md 29行目)。ここは as const を使わず、型注釈や satisfies などでリテラル型を維持してください。
  const payload = {
    user_id: user.userId,
    period_type: "monthly" as const,
    period_start: "2026-09-01",
    amount: 10000,

supabase/migrations/20260806045042_fix_invite_check_rls_recursion.sql:11

  • この SECURITY DEFINER 関数は (event_id, user_id) を任意に渡せるため、RPC 経由で「特定ユーザーが特定イベントに参加しているか」を第三者が推測できる経路になります(RLSで event_participants を隠しても回避されうる)。少なくとも p_user_id を受け取らず auth.uid() のみに固定した関数にし、REVOKE/GRANT EXECUTEanon からの呼び出しも遮断する設計に寄せるのが安全です。
create function public.is_event_participant(p_event_id uuid, p_user_id uuid)
returns boolean
language sql
security definer
set search_path = public

Copilot AI review requested due to automatic review settings August 6, 2026 08:34
- events.test.ts: テスト名が「編集・削除できない」なのに削除
  (deleted_at更新)を試していなかったため追加
- events.test.ts / event-participants.test.ts: 残っていたセットアップ
  insertのerror未チェック箇所を修正
- budgets/expenses/ticket-entries.test.ts: events/event_participants
  にはある「他人になりすまして作成できない」テストを対称性のため追加
Copilot AI review requested due to automatic review settings August 6, 2026 10:00
expect(error).not.toBeNull();
});

test("本人は自分の参加行のvisibilityを変更できる", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: docs/permissions.md の権限マトリクスには「自分のステータス変更 ○」の行がありますが、これを直接検証する正常系テスト(UPDATEでstatusを変更できること)は無いようです。visibilityのUPDATE成功例はここにありますが、statusのUPDATE成功例はinsert時の初期値でしか確認していません。必須ではありませんが、マトリクスの行をそのままテストに写す方針(docs/permissions.md「3. マトリクスを表のままテストに写す」)に沿えば、statusのUPDATE成功ケースも1つ足しておくと対称性が取れそうです。

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

レビュー総評

docs/permissions.md の権限マトリクスと docs/data-model.md のRLSポリシー方針、supabase/migrations/20260806034559_rls_policies.sql を突き合わせて確認しました。結論として方針への準拠度は高く、大きな指摘はありません

確認した内容

  • test/db/*.test.ts の各テストが検証している内容を、実際のRLSポリシー(event_participants_insert_self_or_invite の2分岐、guard_event_deletion のsecurity definer、guard_is_admin_immutable トリガー等)と1つずつ突き合わせ、期待値のズレは見つかりませんでした。
  • docs/permissions.md の「最小の検証セット」8項目、権限マトリクスの×側(否定側)は全てテストでカバーされています。
  • service_role キーは使用されておらず、test/db/helpers.tsauth.signUp で作成した実セッションのみを使っています(docs/permissions.md 「1. service_roleキーを使わない」に準拠)。
  • 「RETURNINGが空 = ブロックされた」だけで終わらせず、本人視点で値が実際に変化していないことまで確認するテストになっている点(USING句破損の見逃し穴を塞ぐ意図がコミットメッセージ・コメント双方に明記)は良い設計です。
  • as キャスト・anyeslint-disable@ts-ignore は本PRの差分に見当たりません("monthly" as const はリテラル型への絞り込みで、方針が禁止している型拡張キャストとは別物と判断)。
  • CreateEventOverrides 型は生成型 Database["public"]["Tables"]["events"]["Insert"] から導出しており、手書き型の二重定義はありません。
  • 本PRは common/ に置くべき判断ロジックの追加を伴わない(テストとCI設定のみの変更な)ため、「common/ロジックの複製」観点では該当箇所なしと判断しました。
  • CIワークフローの supabase status -o env 出力の加工(grep/sedでAPI_URL/ANON_KEYのみに絞ってGITHUB_ENVへ)は、SERVICE_ROLE_KEY 等の混入を避ける配慮も含めて意図通りに見えます。

軽微な指摘(inline)

  • test/db/event-participants.test.ts: 権限マトリクスの「自分のステータス変更 ○」に対応する正常系のUPDATEテストが無いようです(visibilityのUPDATE成功例はありますが、statusはinsert時の初期値経由のみ)。必須ではない軽微な網羅漏れとしてコメントしました。

確認しておきたい点(コード上は判断できないため質問)

  • docs/testing.md / docs/permissions.md は「新しい権限テストは対象のRLSポリシー/ガードを一時的に無効化して赤くなることを確認してから戻す」ことを必須要件としています。PR説明では招待INSERTのバグ調査の過程での確認には触れていますが、他のテスト(削除ガード、is_admin保護、なりすまし系など)についても同様の確認を行ったかが差分からは分かりませんでした。実施済みであれば問題ありません。

以上、テスト内容自体はスキーマ・RLSポリシーと整合しており、否定側中心の網羅も十分だと判断しました。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (4)

test/db/budgets.test.ts:119

  • period_type: "monthly" as constas キャストは、このリポジトリのルール(例: CLAUDE.md:29)に反します。ここは値を直接 .insert() に渡してコンテキスト型付けに任せるか、型注釈でリテラル型を保持してください。
  const payload = {
    user_id: user.userId,
    period_type: "monthly" as const,
    period_start: "2026-09-01",
    amount: 10000,

test/db/events.test.ts:152

  • このテストは .update(...).eq(...)error === null しか見ておらず、RLSのUSING句で0件更新になっても成功扱いで通ってしまいます。RETURNING か再SELECTで deleted_at が実際にセットされたことまで検証してください。
  const { error } = await owner.client
    .from("events")
    .update({ deleted_at: new Date().toISOString() })
    .eq("id", event.id);
  expect(error).toBeNull();

test/db/events.test.ts:201

  • このケースも削除(update)の結果を error だけで判定しているため、RLSで0件更新になっても「削除済みイベントが支出保持者には見える」を検証できません。deleted_at が非NULLになったことを確認してから閲覧可否をテストしてください。
  const deleteResult = await owner.client
    .from("events")
    .update({ deleted_at: new Date().toISOString() })
    .eq("id", event.id);
  expect(deleteResult.error).toBeNull();

test/db/events.test.ts:162

  • 削除後の可視性をテストしていますが、削除(update)自体が0件更新で失敗しても error === null で通ってしまい、意図した前提(削除済み)が保証されません。deleted_at が実際にセットされたことを .select() 等で確認してください。

This issue also appears on line 197 of the same file.

  const deleteResult = await owner.client
    .from("events")
    .update({ deleted_at: new Date().toISOString() })
    .eq("id", event.id);
  expect(deleteResult.error).toBeNull();

Copilot AI review requested due to automatic review settings August 6, 2026 10:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (1)

test/db/budgets.test.ts:117

  • as const の型アサーションが入っていますが、このリポジトリでは as キャストを使わない運用になっています(CLAUDE.md に明記)。ここは生成型でも period_type: string のためアサーション不要なので削除してください。
    period_type: "monthly" as const,

Comment thread test/db/budgets.test.ts Outdated
const user = await createTestUser();
const payload = {
user_id: user.userId,
period_type: "monthly" as const,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as const はCLAUDE.mdの「as によるキャストを使わない」に抵触します。しかも period_type は生成型上 string(supabase/types.ts)で、check 制約由来のリテラル型ではないため、この as const は型エラー回避としても不要に見えます。実際、同PR内の ticket_entries.test.tsentry_type: "lottery"expenses.test.tscategory: "ticket" は同じ構造(CHECK制約付きtext列)でキャスト無しの素のリテラルを渡しており、一貫性のためにもここも as const を外して問題ないはずです。

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

レビュー総評

docs/permissions.md / docs/data-model.md / docs/testing.md / docs/lint-policy.md の方針に照らして確認しました。今回のPRは test/db/ の新規テストとCIワークフローの接続情報受け渡しのみで、app/ / mcp/ / common/ への変更は無いため、ロジック複製の懸念は対象外です。

良い点

  • 権限マトリクス(docs/permissions.md)の全項目・最小検証セット8項目を網羅していることを表と突き合わせて確認しました。特に「参加者がいるイベントの削除不可」を公開・非公開の参加者それぞれで検証している点、「他人の◯◯は更新・削除できない」系でRETURNINGの空だけでなく本人視点で実際に変化していないことまで確認している点(USING句破損の検出漏れ対策)はdocs/permissions.mdの要件を正確に踏まえています。
  • service_roleキーを一切使わず、auth.signUpによる実認証クライアントで検証しており、docs/permissions.md「RLS検証の必須要件」の1・2を満たしています。
  • 否定側(弾かれること)のテストが本体になっている構成で、docs/testing.mdの「優先的にテストする対象」1位(権限判定)を正しく最優先しています。
  • helpers.tsCreateEventOverridesDatabase生成型からPartialで導出しており、型を手書きしていません。asキャスト・anyeslint-disable/@ts-ignoretest/db/budgets.test.tsの1箇所を除き見当たりませんでした(下記インラインコメント参照)。
  • CIワークフローの接続情報受け渡しは、grepでAPI_URL/ANON_KEYの2行のみに絞ってservice_roleキー等の混入を防いでおり、コメントで意図も明記されています。

指摘

  • test/db/budgets.test.ts:117"monthly" as const がCLAUDE.mdの「asによるキャストを使わない」に抵触します。period_typeは生成型上stringであり型エラー回避としても不要に見え、同PR内の他ファイル(ticket_entries.test.ts/expenses.test.ts)の同構造の列では素のリテラルを使っているため一貫性の面でも外して良いはずです(nitpick、インラインコメント参照)。

確認できなかった点

  • サンドボックス環境の制約でyarn lint / yarn typecheckをこのセッションから実行できませんでした。CI(lint/typecheckジョブ)の結果を確認してください。
  • docs/permissions.md「4. 壊して赤くなることを確認する」はPR本文で実施済みと記載されていますが、実際の赤化確認はコード上から検証できないため申告どおり実施された前提でレビューしています。

総じて、権限マトリクスと最小検証セットに対する網羅性・否定側テストの質は方針に沿っており、致命的な問題は見つかりませんでした。上記1件はnitpickです。

共有payloadオブジェクトをやめ、2回とも直接リテラルを書く形にして
型アサーションを不要にした。
Copilot AI review requested due to automatic review settings August 6, 2026 10:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@reitojike

Copy link
Copy Markdown
Owner Author

レビュー指摘の総括分類(このPR全体を通して)

多数のラウンドで指摘があったため、最終まとめとして分類する。

本物の修正(実装したRLSポリシー自体の不具合をこのPRのテストで発見)

  • 招待INSERTのRETURNINGが招待者自身には読めない非公開行を返そうとしてRLSに阻まれる問題。SECURITY DEFINER化・BEFORE INSERTトリガー化まで試した末に真因(RLSではなくRETURNING)を特定し、元のWITH CHECK設計([Phase1] RLSポリシー実装(権限マトリクス準拠) #31)は変更不要と判明。デバッグ用マイグレーション4本は全て削除し、supabase/migrations/への正味の差分はゼロ

妥当な指摘(テストカバレッジ・堅牢性)→ 全て対応済み

  • 「他人の◯◯は更新・削除できない」系で、RETURNING空だけでなく本人視点で値の不変・行の存在を確認(USING句破損の見逃し穴)
  • セットアップinsert/updateのerror未チェック箇所を横展開で修正
  • 権限マトリクスの網羅漏れ(参加登録済みの他ユーザーによる編集・削除拒否、自分のステータス変更の正常系、なりすまし作成拒否)を追加
  • CI(supabase.yml)でservice_roleキーがGITHUB_ENVに混入しないようgrepで絞り込み
  • @supabase/supabase-jsをdevDependenciesに移動
  • as constキャストを除去(CLAUDE.md絶対ルール準拠)

見送り(誤検知/対応不要と判断)

  • なし(全て妥当な指摘だったため見送りは無し)

CI(lint/typecheck/unit-test/db-test/types-check/claude-review/codex-review)は最終コミットで全てgreen。db-test は50件のRLSテストが全てpassしている。

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

レビュー総評

docs/permissions.md の権限マトリクス・最小検証セット、docs/testing.md のテスト方針を基準に、test/db/ 一式・CI設定・helpers.ts を通読した。すでに多数回の自動レビューを経て指摘が出尽くしている状態だが、独立した視点で再確認した結果を記録する。

CLAUDE.md絶対ルールとの照合

  • as キャスト、anyeslint-disable / @ts-ignore は全ファイルに一切なし(grep で確認済み)。ecffd24as const を除去した経緯も履歴に残っている。
  • helpers.ts の型はすべて Database(生成型)経由。手書き型なし。
  • test/db/common/ を経由するロジックを持たない純粋なRLS検証で、判断ロジックの複製は無い。

docs/permissions.md との整合

  • 「最小の検証セット」8項目は全て test/db/ でカバーされている(他人の支出/非公開参加登録の閲覧、オーナー以外の更新・削除、他人のステータス変更、他人の参加取りやめ、参加者ありイベントの削除ガード、未参加者による招待)。
  • 否定側(×になるべきケース)が本体になっており、docs/permissions.md が名指しで警告している「RETURNINGが空なだけでは検証にならない」パターンに対して、本人視点での再SELECTによる実値確認まで踏み込んでいる(event-participants.test.ts / ticket-entries.test.ts / expenses.test.ts / budgets.test.ts で一貫)。
  • service_role キーは不使用。auth.signUp の実クライアントのみで検証しており、docs/permissions.md「RLS検証の必須要件 1」に準拠。
  • budgetsNULLS NOT DISTINCT 制約の重複回帰テストも入っている。

CI

  • .github/workflows/supabase.yml の接続情報受け渡しは API_URL/ANON_KEY の2行のみに grep で絞っており、SERVICE_ROLE_KEYGITHUB_ENV に混入しない設計になっている。
  • 最新コミットで lint / typecheck / unit-test / db-test / types-check が全てgreenであることを確認した(gh pr checks)。

1点、既出だが確認した内容

  • docs/permissions.md の「他ユーザーの招待: オーナー ○」というマトリクス表現は、実際のRLS(event_participants_insert_self_or_invite)では「招待できるのはそのイベントに参加登録済みの任意のユーザー」という条件のみで判定しており、owner_id だけでは招待できない(オーナー自身も参加登録していることが前提)。この組み合わせ(オーナーが未参加のまま招待する)は本PRでは直接テストされていないが、既に別ラウンドの指摘で「#31のRLS設計自体の話でこのPRのスコープ外、フェーズ2以降で気になればdocs/permissions.mdの記述を明確化」という判断が出ており、妥当な見送りだと考える。新規の指摘としては追加しない。

以上、独立した確認でも新たなブロッキング事項は見つからなかった。方針への準拠度は高く、承認できる水準。

@reitojike
reitojike merged commit c870c2a into main Aug 6, 2026
7 checks passed
@reitojike
reitojike deleted the 26-phase1-db-rls-tests branch August 6, 2026 10:17
reitojike added a commit that referenced this pull request Aug 8, 2026
判断ポイントは1 Issueに3個まで(5個超で分割)、PR差分は300行を目安とする。
記事の実測値をそのまま採らず、main にマージ済みのPR 26本(中央値135行、
300行以下65%)で裏を取ってから採用した。行数の数え方から生成物・ロック
ファイル・権限マトリクスを写したテスト表を除外する根拠も、超過した
PR #16 / #32 の実態から示した。

機械的ゲートにしない旨と、3回ルール(モデルを上げる) / PO確認(判断を
下せる層に上げる) / 粒度超過(Issueを分ける)の対処の違いを表で整理。
docs/roadmap.md はポインタ1行に留め、根拠は1箇所にだけ置く。

Refs #44
reitojike added a commit that referenced this pull request Aug 8, 2026
PR #68 のClaude Reviewの指摘に対応。指摘は正しかった。

誤り: 「300行を超えた5本」は事実と異なり、素の差分では9本だった
(#31 307 / #59 365 / #60 380 / #30 441 / #29 617 / #18 679 / #41 698 /
#32 1063 / #16 3528)。同じ本文が書いていた「300行以下が65%(17/26)」は
超過9本を含意しており、記述が自己矛盾していた。

原因: 母集団の統計(中央値・65%)は素の差分で数え、外れ値の説明だけに
除外規則を適用していた。数え方を混在させたうえ、超過リストを上位5本で
打ち切って全件確認しなかった。

訂正: 26本すべてを git show --numstat で数え直し、除外の段階ごとに
表で示す。素の差分(中央値135行 / 65% / 超過9本)、パス名で機械的に
判定できる除外まで(中央値128行 / 85% / 超過4本)、除外規則を最後まで
適用(超過2本 = #59#60)。落ちる7本の内訳も明記した。

あわせて、3段階目の中央値を出さない理由を書いた。supabase/config.toml は
#29 では supabase init の出力(416行)、#56 では根拠コメント付きで手で
直した6行で、同じパスでも扱いが逆になる。パス名では決まらないことが、
この節が機械的ゲートになり得ない理由そのものなので、「lintではない」の
段落の根拠もこの実測に差し替えた。

Refs #44
reitojike added a commit that referenced this pull request Aug 8, 2026
Claude Reviewの指摘どおり、#29(617→22)はsupabase/config.toml(416行)
だけでなくsupabase/types.ts(179行)も、#32(1063→79)はテスト表(924行)
だけでなくyarn.lock(60行)も除外して初めて数字が再現できる。
片方しか書いていなかったため、追試すると数値が合わなかった。

Refs #44
reitojike added a commit that referenced this pull request Aug 8, 2026
* docs: Issueの粒度とPR差分サイズの目安をCLAUDE.mdに明文化

判断ポイントは1 Issueに3個まで(5個超で分割)、PR差分は300行を目安とする。
記事の実測値をそのまま採らず、main にマージ済みのPR 26本(中央値135行、
300行以下65%)で裏を取ってから採用した。行数の数え方から生成物・ロック
ファイル・権限マトリクスを写したテスト表を除外する根拠も、超過した
PR #16 / #32 の実態から示した。

機械的ゲートにしない旨と、3回ルール(モデルを上げる) / PO確認(判断を
下せる層に上げる) / 粒度超過(Issueを分ける)の対処の違いを表で整理。
docs/roadmap.md はポインタ1行に留め、根拠は1箇所にだけ置く。

Refs #44

* docs: 判断ポイント数の境界を一本化し、裏取り済みの数値と外部実測を書き分ける

PR #68 のClaude Reviewの指摘2件に対応。

指摘1: 「3個まで、5個を超えるなら分割」で4個の扱いが未定義だった。
閾値を「3個まで。4個目が出てきたら分ける」に一本化する。機械的ゲートに
しない方針である以上、「検討」と「必ず」の二段構えは実効性のない
false precisionになるため、緩衝域を作らず単一の線にした。

指摘2: 裏取り済みの300行と、外部実測のままの3個が同じ文脈に並んでいた。
「2つの数字は裏付けの強さが違う」として段落を分け、300行はこのリポジトリの
実測(PR 26本、中央値135行/300行以下65%)で検証済み、3個は外部実測のみを
根拠とする未検証のヒューリスティックであると明示した。過去Issueの判断数は
記録がなく後から数え直せないため、このリポジトリでの裏取りが今はできない
理由も併記。採用の根拠はコストの非対称性に置いた。

あわせて、外部実測に対応値のない4個/5個を推定して線を引いていないことと、
実績が溜まったら見直す旨を記載した。

Refs #44

* docs: PR実測の集計を数え直し、超過本数の誤りを訂正する

PR #68 のClaude Reviewの指摘に対応。指摘は正しかった。

誤り: 「300行を超えた5本」は事実と異なり、素の差分では9本だった
(#31 307 / #59 365 / #60 380 / #30 441 / #29 617 / #18 679 / #41 698 /
#32 1063 / #16 3528)。同じ本文が書いていた「300行以下が65%(17/26)」は
超過9本を含意しており、記述が自己矛盾していた。

原因: 母集団の統計(中央値・65%)は素の差分で数え、外れ値の説明だけに
除外規則を適用していた。数え方を混在させたうえ、超過リストを上位5本で
打ち切って全件確認しなかった。

訂正: 26本すべてを git show --numstat で数え直し、除外の段階ごとに
表で示す。素の差分(中央値135行 / 65% / 超過9本)、パス名で機械的に
判定できる除外まで(中央値128行 / 85% / 超過4本)、除外規則を最後まで
適用(超過2本 = #59#60)。落ちる7本の内訳も明記した。

あわせて、3段階目の中央値を出さない理由を書いた。supabase/config.toml は
#29 では supabase init の出力(416行)、#56 では根拠コメント付きで手で
直した6行で、同じパスでも扱いが逆になる。パス名では決まらないことが、
この節が機械的ゲートになり得ない理由そのものなので、「lintではない」の
段落の根拠もこの実測に差し替えた。

Refs #44

* docs: PR #29/#32の除外理由が2段階なのに1段階しか書いていなかった記述漏れを修正

Claude Reviewの指摘どおり、#29(617→22)はsupabase/config.toml(416行)
だけでなくsupabase/types.ts(179行)も、#32(1063→79)はテスト表(924行)
だけでなくyarn.lock(60行)も除外して初めて数字が再現できる。
片方しか書いていなかったため、追試すると数値が合わなかった。

Refs #44
reitojike added a commit that referenced this pull request Aug 9, 2026
reitojike added a commit that referenced this pull request Aug 9, 2026
* レビュー指摘の重複による修正ラリーを減らす

同一根拠の指摘がファイルごとに個別投稿され、修正→push→再指摘のラウンドが
かさむ問題(PR #32, #60, #63, #73)への対処。指摘を直す側には横展開確認の
規律を、レビューボット側には同一根拠の指摘を1件に集約する指示を追加する。
Codexレビュー本格導入前の準備。

Closes #79

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Copilotの指摘反映: PR #32/#60の例が読点で連結され読みにくい問題を修正

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants