diff --git a/frontend/supabase/migrations/20260718020000_profiles_service_role_upsert_grants.sql b/frontend/supabase/migrations/20260718020000_profiles_service_role_upsert_grants.sql deleted file mode 100644 index 3e44d20b..00000000 --- a/frontend/supabase/migrations/20260718020000_profiles_service_role_upsert_grants.sql +++ /dev/null @@ -1,53 +0,0 @@ -begin; - -grant select, insert, update on table public.profiles to service_role; - -grant select ( - id, - email, - credits, - created_at, - updated_at, - name, - birth_date, - birth_time, - country_code, - province_code, - city_code, - district_code, - latitude, - longitude, - timezone_offset -) on public.profiles to service_role; - -grant insert ( - id, - email, - name, - birth_date, - birth_time, - country_code, - province_code, - city_code, - district_code, - latitude, - longitude, - timezone_offset, - updated_at -) on public.profiles to service_role; - -grant update ( - name, - birth_date, - birth_time, - country_code, - province_code, - city_code, - district_code, - latitude, - longitude, - timezone_offset, - updated_at -) on public.profiles to service_role; - -commit; diff --git a/frontend/supabase/migrations/20260718080000_profiles_service_role_account_upsert_selects.sql b/frontend/supabase/migrations/20260718080000_profiles_service_role_account_upsert_selects.sql new file mode 100644 index 00000000..b84d4994 --- /dev/null +++ b/frontend/supabase/migrations/20260718080000_profiles_service_role_account_upsert_selects.sql @@ -0,0 +1,9 @@ +begin; + +-- Existing-row upserts must read every submitted column to resolve the result. +grant select ( + district_code, + updated_at +) on table public.profiles to service_role; + +commit; diff --git a/frontend/supabase/migrations/20260717010000_chart_profiles.sql b/frontend/supabase/migrations/20260718100000_repair_missing_chart_profiles.sql similarity index 100% rename from frontend/supabase/migrations/20260717010000_chart_profiles.sql rename to frontend/supabase/migrations/20260718100000_repair_missing_chart_profiles.sql diff --git a/frontend/supabase/migrations/20260717020000_synastry_reports.sql b/frontend/supabase/migrations/20260718101000_repair_missing_synastry_reports.sql similarity index 100% rename from frontend/supabase/migrations/20260717020000_synastry_reports.sql rename to frontend/supabase/migrations/20260718101000_repair_missing_synastry_reports.sql diff --git a/frontend/supabase/migrations/20260718010000_recover_missing_profile_rows.sql b/frontend/supabase/migrations/20260718102000_recover_missing_profile_rows.sql similarity index 100% rename from frontend/supabase/migrations/20260718010000_recover_missing_profile_rows.sql rename to frontend/supabase/migrations/20260718102000_recover_missing_profile_rows.sql diff --git a/frontend/tests/profile-persistence.test.ts b/frontend/tests/profile-persistence.test.ts index 6694a689..8fbd5007 100644 --- a/frontend/tests/profile-persistence.test.ts +++ b/frontend/tests/profile-persistence.test.ts @@ -19,3 +19,21 @@ test("account route can fall back when coordinate columns are not deployed", () assert.match(source, /withoutCoordinates/); assert.match(source, /PGRST204|42703|schema cache|column/i); }); + +test("service role can read every column used by account profile upserts", () => { + // Given: the least-privilege grant omitted two columns submitted by /api/account. + const migration = readFileSync( + new URL( + "../supabase/migrations/20260718080000_profiles_service_role_account_upsert_selects.sql", + import.meta.url, + ), + "utf8", + ); + + // When: the corrective migration defines the account-upsert read grant. + // Then: PostgREST can read both submitted columns while resolving existing rows. + assert.match( + migration, + /grant\s+select\s*\(\s*district_code\s*,\s*updated_at\s*\)\s*on\s+table\s+public\.profiles\s+to\s+service_role/is, + ); +}); diff --git a/tests/test_supabase_migration_versions.py b/tests/test_supabase_migration_versions.py new file mode 100644 index 00000000..704ebd32 --- /dev/null +++ b/tests/test_supabase_migration_versions.py @@ -0,0 +1,23 @@ +from collections import defaultdict +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MIGRATIONS = ROOT / "frontend" / "supabase" / "migrations" + + +def test_supabase_migration_versions_are_unique() -> None: + # Given: Supabase records the timestamp prefix as the migration identity. + versions: defaultdict[str, list[str]] = defaultdict(list) + + # When: every local migration is grouped by that identity. + for migration in sorted(MIGRATIONS.glob("*.sql")): + versions[migration.name.split("_", maxsplit=1)[0]].append(migration.name) + + # Then: no migration can be silently skipped behind a duplicate identity. + duplicates = { + version: names + for version, names in versions.items() + if len(names) > 1 + } + assert duplicates == {} diff --git a/tests/test_supabase_user_data_contract.py b/tests/test_supabase_user_data_contract.py index 27d96533..4a8728c9 100644 --- a/tests/test_supabase_user_data_contract.py +++ b/tests/test_supabase_user_data_contract.py @@ -28,14 +28,14 @@ CHART_PROFILE_MIGRATION = ( / "frontend" / "supabase" / "migrations" - / "20260717010000_chart_profiles.sql" + / "20260718100000_repair_missing_chart_profiles.sql" ) SYNASTRY_REPORT_MIGRATION = ( Path(__file__).resolve().parents[1] / "frontend" / "supabase" / "migrations" - / "20260717020000_synastry_reports.sql" + / "20260718101000_repair_missing_synastry_reports.sql" ) PAGE = Path(__file__).resolve().parents[1] / "frontend" / "src" / "app" / "page.tsx" CHART_PROFILE_ROUTE = Path(__file__).resolve().parents[1] / "frontend" / "src" / "app" / "api" / "chart-profiles" / "route.ts"