# RLS pgTAP smoke test

Parky の Supabase Postgres の RLS ポリシーを最小限の不変条件として CI でガードするための仕組み。

## 何を守るか

- **service_role は RLS bypass** (Workers / Edge Function 経由)
- **authenticated は他人の行を見られない** (`user_push_tokens` 等の user-scoped テーブル)
- **admin-only テーブル** (`admins`, `admin_tasks`, `role_permissions` …) は一般 authenticated から SELECT 0 件 / INSERT 拒否
- **anon は公開コンテンツのみ可視** (`parking_lots` など)

完全網羅ではなく、**代表 5〜10 件の assertion で smoke test** する。テーブル/policy 追加時に該当 test を増やしていく。

```mermaid
flowchart LR
  PR[PR / push] --> CI[api-rls-tests.yml]
  CI --> ROLES[Create Supabase-equiv roles<br/>authenticated / anon / service_role]
  ROLES --> APPLY[apply baseline]
  APPLY --> EXT[CREATE EXTENSION pgtap]
  EXT --> RUN[run-rls-tests.sh]
  RUN --> H[_helpers.sql]
  H --> T1[parking_lots.test.sql]
  H --> T2[user_notifications.test.sql]
  H --> T3[admin_only_tables.test.sql]
  H --> T4[bff_only_isolation / anomalies / ...]
  T1 & T2 & T3 & T4 --> TAP[TAP 出力]
  TAP -->|not ok| FAIL[CI fail]
  TAP -->|all ok| PASS[CI pass]
```

## ファイル構成

```
infra/supabase/
├── baseline/                                 # snapshot 戦略 (CLAUDE.md §DB スキーマ変更フロー)
│   └── ...                                   # pgtap extension は baseline 内で CREATE
└── tests/
    ├── run-rls-tests.sh                      # ローカル / CI 共通ランナー
    └── rls/
        ├── _helpers.sql                      # set_auth(uid) / set_anon() / set_service_role()
        ├── admin_only_tables.test.sql        # admins / admin_tasks / role_permissions
        ├── anomalies.test.sql                # 例外的なテーブル群の RLS 検査
        ├── auth_uid_initplan.test.sql        # auth.uid() initplan 性能 / 存在検査
        ├── bff_only_isolation.test.sql       # bff_only schema は client から直接 EXECUTE 不可
        ├── exposed_schemas.test.sql          # admin / marketing / analytics は anon/auth から SELECT 不可
        ├── main_tables_rls_smoke.test.sql    # public 主要テーブルの所有者単位スコープ
        ├── parking_lots.test.sql             # anon 読取・authenticated DELETE 不可
        ├── security_definer_search_path.test.sql  # SECURITY DEFINER 関数の search_path lock
        └── user_notifications.test.sql       # admin-only / 他人 push_token 不可視
```

2026-05-03 時点で 10 ファイル。新規 RLS policy / `bff_only` 関数を増やしたら
対応する `*.test.sql` を 1 件追加する運用。

## ローカル実行

```bash
# Supabase CLI で立ち上げた local DB に対して:
PGHOST=127.0.0.1 PGPORT=54322 PGUSER=postgres PGPASSWORD=postgres PGDATABASE=postgres \
  bash infra/supabase/tests/run-rls-tests.sh
```

事前に baseline を全件適用しておくこと (`supabase db reset` か
`bash scripts/db/apply-baseline.sh` で OK)。

`authenticated` / `anon` / `service_role` の role が無い素の postgres にあてる場合は、CI workflow `Create Supabase-equivalent roles` ステップと同じ DDL を流してから実行する。

## test の追加方法

1. `infra/supabase/tests/rls/{table}.test.sql` を新規作成。
2. 冒頭テンプレ:

```sql
BEGIN;
SET search_path TO extensions, public, pg_temp;
SELECT plan(N);
SELECT test_rls.ensure_auth_schema();

SELECT test_rls.set_auth('11111111-1111-1111-1111-111111111111');
-- ok() / is() / cmp_ok() / throws_ok() で assertion
SELECT test_rls.reset_auth();

SELECT * FROM finish();
ROLLBACK;
```

3. 必ず `BEGIN; ... ROLLBACK;` で囲む — 本番テーブルに永続変更を残さない。
4. fixture が必要なら `test_rls.set_service_role()` 中に INSERT。

## CI 統合

`.github/workflows/api-rls-tests.yml` が以下のパス変更で発火する:

- `infra/supabase/baseline/**.sql`
- `infra/supabase/tests/rls/**`
- `infra/supabase/tests/run-rls-tests.sh`

`postgres:15` を service container に立て、`postgresql-15-pgtap` を apt で追加してから baseline 適用 → tests を流す。1 件でも `not ok` が出れば job が fail。

## 既知の制限

- pgTAP は **postgres:15-alpine では apt 提供されない**ため、`postgres:15` (Debian 系) を使っている。
- `auth.uid()` / `auth.jwt()` の正確な再現は Supabase 本番環境に依存するため、`_helpers.sql` の `ensure_auth_schema()` が薄い shim を作る (素の postgres 用)。
- `current_owner_id()` は本番では owners テーブル + auth.uid() で解決されるため、本格的な owner RLS テストは fixture (owners + parking_lot_owners 行) を service_role で作ってから set_auth する設計。今回はゴールに含めず後続で追加する。
