Repository navigation
[Phase1] test/db/ RLS検証(最小8項目 + 権限マトリクス全項目) #32
Changes from all commits
d5f67ef
846fe65
3c3b647
9a41e75
c6717ec
7095964
65469c6
fdfdee3
3dd91ee
acb99bb
128be3c
e255bb0
a1326cb
cdfaea4
057b6d2
78386b2
0107d38
638c2cb
e6999bc
ecffd24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,21 @@ jobs: | |
| - run: supabase start | ||
| if: steps.check.outputs.initialized == 'true' | ||
|
|
||
| # 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 は値をダブルクォートで囲んで出力するが、 | ||
| # GITHUB_ENV はクォートを解釈しないため、そのまま渡すと値にクォートが | ||
| # 含まれてしまう(例: API_URL が `"http://127.0.0.1:54321"` になる)。 | ||
| # --override-nameは指定したキーの出力名を変えるだけで、SERVICE_ROLE_KEY等の | ||
| # 他のキーはそのまま出力に残る。API_URL/ANON_KEY以外がGITHUB_ENVに | ||
| # 紛れ込まないよう、grepで対象の2行だけに絞ってから渡す。 | ||
| run: | | ||
| supabase status -o env --override-name api.url=API_URL --override-name auth.anon_key=ANON_KEY \ | ||
| | grep -E '^(API_URL|ANON_KEY)=' \ | ||
| | sed 's/^\([A-Z_]*\)="\(.*\)"$/\1=\2/' >> "$GITHUB_ENV" | ||
|
Comment on lines
+56
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. コメントでは「service_roleキーはエクスポートしない」とありますが、 ローカルSupabaseの鍵は固定値で実害は小さいですが、コメントの主張(「エクスポートしない」)と実際の挙動が食い違っており、 |
||
|
|
||
| - run: yarn test:db | ||
|
|
||
| types-check: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import { expect, test } from "vitest"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ナイトピック:
|
||
| import { createTestUser } from "./helpers"; | ||
|
|
||
| test("本人はbudgetsを作成・閲覧できる", async () => { | ||
| const user = await createTestUser(); | ||
| const { data, error } = await user.client | ||
| .from("budgets") | ||
| .insert({ | ||
| user_id: user.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-08-01", | ||
| amount: 10000, | ||
| }) | ||
| .select() | ||
| .single(); | ||
| expect(error).toBeNull(); | ||
| expect(data?.user_id).toBe(user.userId); | ||
| }); | ||
|
|
||
| test("他人になりすましてbudgetsを作成できない", async () => { | ||
| const [actor, victim] = await Promise.all([createTestUser(), createTestUser()]); | ||
| const { error } = await actor.client.from("budgets").insert({ | ||
| user_id: victim.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-08-01", | ||
| amount: 10000, | ||
| }); | ||
| expect(error).not.toBeNull(); | ||
| }); | ||
|
|
||
| test("他人のbudgetsは見えない", async () => { | ||
| const [self, stranger] = await Promise.all([createTestUser(), createTestUser()]); | ||
| const { data: created, error: createError } = await self.client | ||
| .from("budgets") | ||
| .insert({ | ||
| user_id: self.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-08-01", | ||
| amount: 10000, | ||
| }) | ||
| .select() | ||
| .single(); | ||
| expect(createError).toBeNull(); | ||
|
|
||
| const { data, error } = await stranger.client | ||
| .from("budgets") | ||
| .select() | ||
| .eq("id", created?.id ?? ""); | ||
| expect(error).toBeNull(); | ||
| expect(data).toHaveLength(0); | ||
| }); | ||
|
|
||
| test("他人のbudgetsは更新・削除できない", async () => { | ||
| const [self, stranger] = await Promise.all([createTestUser(), createTestUser()]); | ||
| const { data: created, error: createError } = await self.client | ||
| .from("budgets") | ||
| .insert({ | ||
| user_id: self.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-08-01", | ||
| amount: 10000, | ||
| }) | ||
| .select() | ||
| .single(); | ||
| expect(createError).toBeNull(); | ||
| const id = created?.id ?? ""; | ||
|
|
||
| const { data: updated } = await stranger.client | ||
| .from("budgets") | ||
| .update({ amount: 1 }) | ||
| .eq("id", id) | ||
| .select(); | ||
| expect(updated).toHaveLength(0); | ||
|
|
||
| // strangerは他人のbudgetsをSELECTできないため、RETURNINGが空なだけでは | ||
| // USING句が効いているか分からない。本人視点で実際に変化していないことを確認する。 | ||
| const { data: afterUpdate } = await self.client | ||
| .from("budgets") | ||
| .select("amount") | ||
| .eq("id", id) | ||
| .single(); | ||
| expect(afterUpdate?.amount).toBe(10000); | ||
|
|
||
| const { data: deleted } = await stranger.client.from("budgets").delete().eq("id", id).select(); | ||
| expect(deleted).toHaveLength(0); | ||
|
Comment on lines
+53
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このテストは実質的にUPDATE/DELETEポリシーを検証できていない可能性があります。
これはこのPR自身が招待INSERTで踏んだ落とし穴(コミット「招待テストの真因を修正: RLSではなくRETURNINGの読み戻し不可が原因だった」)と同じ構造で、
|
||
|
|
||
| const { data: stillExists } = await self.client.from("budgets").select().eq("id", id); | ||
| expect(stillExists).toHaveLength(1); | ||
| }); | ||
|
|
||
| test("本人は自分のbudgetsを削除できる", async () => { | ||
| const user = await createTestUser(); | ||
| const { data: created, error: createError } = await user.client | ||
| .from("budgets") | ||
| .insert({ | ||
| user_id: user.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-08-01", | ||
| amount: 10000, | ||
| }) | ||
| .select() | ||
| .single(); | ||
| expect(createError).toBeNull(); | ||
| const id = created?.id ?? ""; | ||
|
|
||
| const { error } = await user.client.from("budgets").delete().eq("id", id); | ||
| expect(error).toBeNull(); | ||
|
|
||
| const { data: gone } = await user.client.from("budgets").select().eq("id", id); | ||
| expect(gone).toHaveLength(0); | ||
| }); | ||
|
|
||
| test("同一期間の全ジャンル合算枠(genre NULL)は重複作成できない", async () => { | ||
| const user = await createTestUser(); | ||
| const first = await user.client.from("budgets").insert({ | ||
| user_id: user.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-09-01", | ||
| amount: 10000, | ||
| }); | ||
| expect(first.error).toBeNull(); | ||
|
|
||
| const second = await user.client.from("budgets").insert({ | ||
| user_id: user.userId, | ||
| period_type: "monthly", | ||
| period_start: "2026-09-01", | ||
| amount: 10000, | ||
| }); | ||
| expect(second.error).not.toBeNull(); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.