Files
collective-lists/supabase/tests/008_auth_user_role_default.sql
Oier Bravo Urtasun 2294a31942 fix(auth): default empty role/aud on auth.users → 'authenticated'
Found on ambrosio after the first real self-registration: GoTrue v2.158.1
persists OIDC-provisioned users with empty-string role and aud. PostgREST
then dies with `role "" does not exist` on every REST call because its
per-request `SET LOCAL role = $claim` can't resolve ''. Dev never caught
this because seed.sql hardcodes both columns.

- supabase/migrations/013_auth_users_role_default.sql: backfill existing
  rows + BEFORE INSERT trigger on auth.users that fills empty/NULL role +
  aud with 'authenticated'. Idempotent, preserves explicit values.
- supabase/tests/008_auth_user_role_default.sql: pgTAP coverage — function
  + trigger exist, backfill left nothing empty, empty-string and NULL
  values get defaulted, explicit non-empty role is preserved.
- CLAUDE.md: status line updated to 255 tests (was 248), new gotcha #19
  documenting the GoTrue quirk + that users in existing sessions must
  re-login once for a fresh JWT after the fix lands on any given env.

Total: 41 pgTAP (was 34) + 140 integration + 15 unit + 58 Playwright + 1
gated rate-limit. Full `just test-all` green.
2026-04-15 00:23:00 +02:00

90 lines
3.2 KiB
PL/PgSQL

-- pgTAP: auth.users role/aud defaulting (migration 013)
-- Run with: psql -U postgres -d postgres -f supabase/tests/008_auth_user_role_default.sql
--
-- Covers the bug found on ambrosio (2026-04-15): GoTrue v2.158.1 inserts
-- OIDC-created users into auth.users with empty-string `role` and `aud`,
-- which later makes PostgREST's `SET LOCAL role = ''` fail with
-- `role "" does not exist`. Migration 013 adds a BEFORE INSERT trigger +
-- backfills the existing rows.
CREATE EXTENSION IF NOT EXISTS pgtap;
BEGIN;
SELECT plan(7);
-- ── Existence assertions ────────────────────────────────────────────────
SELECT has_function(
'auth', 'ensure_user_role_default', ARRAY[]::text[],
'ensure_user_role_default trigger function exists in the auth schema'
);
SELECT has_trigger(
'auth', 'users', 'ensure_role_default',
'BEFORE INSERT trigger ensure_role_default exists on auth.users'
);
-- ── Backfill assertion ──────────────────────────────────────────────────
SELECT ok(
(SELECT count(*) FROM auth.users WHERE role IS NULL OR role = '' OR aud IS NULL OR aud = '') = 0,
'No pre-existing auth.users row has an empty role or aud'
);
-- ── Trigger behavior ────────────────────────────────────────────────────
-- Insert a synthetic user with empty role/aud; the trigger should fill both.
-- Drop the AFTER trigger locally so we don't exercise handle_new_user's
-- side-effects (tested elsewhere, not the focus here).
ALTER TABLE auth.users DISABLE TRIGGER on_auth_user_created;
-- NEW row with empty strings
INSERT INTO auth.users (instance_id, id, email, role, aud)
VALUES (
'00000000-0000-0000-0000-000000000000',
'f0000000-0000-0000-0000-000000000001',
'empty-role@test.local',
'',
''
);
SELECT is(
(SELECT role FROM auth.users WHERE id = 'f0000000-0000-0000-0000-000000000001'),
'authenticated',
'Empty role on INSERT is defaulted to authenticated'
);
SELECT is(
(SELECT aud FROM auth.users WHERE id = 'f0000000-0000-0000-0000-000000000001'),
'authenticated',
'Empty aud on INSERT is defaulted to authenticated'
);
-- NEW row with NULL values
INSERT INTO auth.users (instance_id, id, email, role, aud)
VALUES (
'00000000-0000-0000-0000-000000000000',
'f0000000-0000-0000-0000-000000000002',
'null-role@test.local',
NULL,
NULL
);
SELECT is(
(SELECT role FROM auth.users WHERE id = 'f0000000-0000-0000-0000-000000000002'),
'authenticated',
'NULL role on INSERT is defaulted to authenticated'
);
-- Explicit non-empty role is preserved (trigger should only fill when empty).
INSERT INTO auth.users (instance_id, id, email, role, aud)
VALUES (
'00000000-0000-0000-0000-000000000000',
'f0000000-0000-0000-0000-000000000003',
'custom-role@test.local',
'service_role',
'authenticated'
);
SELECT is(
(SELECT role FROM auth.users WHERE id = 'f0000000-0000-0000-0000-000000000003'),
'service_role',
'Explicit role is preserved by the trigger'
);
SELECT * FROM finish();
ROLLBACK;