Compare commits

..
Author SHA1 Message Date
CC WorkerandClaude Opus 4.8 93972a62f7 fix: revert explicit apikey header (caused Kong duplicate-apikey 401)
api-ci-deploy / test-build-deploy (push) Has been cancelled
The previous commit added apikey to _create_base_client headers, but supabase-py
already sets apikey from the key arg → two apikey headers → Kong rejected every
as-user call with 401 'Duplicate API key found' (exam API 502'd on auth). Revert
to Authorization-only; fix the two header unit tests to assert the real contract
(apikey via the key arg; options.headers carries only the user Authorization).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-06 19:30:36 +00:00
CC WorkerandClaude Opus 4.8 f3da9f3b59 fix: explicit apikey header + resilient dev-stack seed-count baselines
api-ci-deploy / test-build-deploy (push) Has been cancelled
- client.py: set apikey explicitly in _create_base_client headers (Kong needs it
  on every request; for per-user clients apikey stays anon while Authorization
  carries the user JWT). Fixes the 2 stale header unit tests that asserted apikey
  in options.headers, and is robust against supabase-py default-header changes.
- test_dev_stack: exact == seed counts → >= baselines. The greenfield seed sets a
  floor; additive exam-marker fixtures (S4-4 cohort) legitimately push live .94
  counts above the old snapshot. >= still catches a broken/missing seed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-06 19:25:39 +00:00
CC WorkerandClaude Opus 4.8 49f84655f7 merge: exam-marker FastAPI backend (S4-5/6/7)
api-ci-deploy / test-build-deploy (push) Has been cancelled
Brings in the full exam-marker HTTP API on /api/exam (as-user RLS, E1/E2 fixes):
- S4-5 template CRUD (hybrid PUT + PATCH)
- S4-6 batches/scans/marks/results/CSV (A7), roster-from-class_students
- S4-7 Neo4j projection on save + neo4j-sync
Also fixes pre-existing E7: storage.py brace-doubling crash (all uploads).

Verified: 35 unit tests; live as-user RLS smoke .94 (templates 17/17, batches
20/20); live graph smoke .94+.209 (projection 17/17). Reviewed; data-loss guard
added (409 on destructive template PUT once marks recorded).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-06 19:16:52 +00:00
4 changed files with 21 additions and 11 deletions
+5 -1
View File
@@ -23,7 +23,11 @@ def _create_base_client(url: str, key: str, access_token: Optional[str] = None,
# If an access token is provided, use it for Authorization (enables per-user RLS) # If an access token is provided, use it for Authorization (enables per-user RLS)
# Otherwise fall back to the API key # Otherwise fall back to the API key
auth_header = f"Bearer {access_token}" if access_token else f"Bearer {key}" auth_header = f"Bearer {access_token}" if access_token else f"Bearer {key}"
# Only override Authorization here. apikey is supplied to create_client via the `key` arg and
# set by supabase-py itself; setting it again here sends a DUPLICATE apikey header that the
# Supabase gateway (Kong) rejects with 401 "Duplicate API key found". For a per-user client
# apikey stays the anon key (from `key`) while this Authorization carries the user JWT.
headers = { headers = {
"Authorization": auth_header, "Authorization": auth_header,
} }
+9 -5
View File
@@ -55,15 +55,19 @@ def test_dev_api_health_endpoint_is_healthy():
assert payload['services']['redis']['database'] == 0 assert payload['services']['redis']['database'] == 0
# NOTE: these are >= baselines, not exact counts. The greenfield seed produces this floor;
# additive exam-marker fixtures (S4-4 cohort adds ~10 students/memberships; ad-hoc classes) push
# the live .94 counts above it. Exact == froze a snapshot that any new fixture breaks, while >=
# still catches a broken or missing seed.
def test_supabase_dev_seed_core_counts(): def test_supabase_dev_seed_core_counts():
assert _rest_count('profiles') == 21 assert _rest_count('profiles') >= 21
assert _rest_count('institute_memberships') == 21 assert _rest_count('institute_memberships') >= 21
assert _rest_count('institutes') == 2 assert _rest_count('institutes') >= 2
def test_supabase_dev_seed_timetable_counts(): def test_supabase_dev_seed_timetable_counts():
assert _rest_count('classes') == 17 assert _rest_count('classes') >= 17
assert _rest_count('taught_lessons') == 1462 assert _rest_count('taught_lessons') >= 1462
def test_runtime_identity_does_not_expose_secret_values(): def test_runtime_identity_does_not_expose_secret_values():
+4 -4
View File
@@ -122,11 +122,11 @@ def test_supabase_client_for_user_uses_access_token_authorization(monkeypatch):
assert anon.access_token == "user-token" assert anon.access_token == "user-token"
assert captured["url"] == "http://supabase.test" assert captured["url"] == "http://supabase.test"
# apikey is supplied via the `key` positional arg (supabase-py sets the apikey header from it).
# options.headers must carry ONLY the per-user Authorization override — adding apikey here too
# produces a duplicate apikey header that Kong rejects ("Duplicate API key found").
assert captured["key"] == "anon-key" assert captured["key"] == "anon-key"
assert captured["options_kwargs"]["headers"] == { assert captured["options_kwargs"]["headers"] == {"Authorization": "Bearer user-token"}
"apikey": "anon-key",
"Authorization": "Bearer user-token",
}
def test_no_school_bootstrap_requires_school_membership_but_allows_canvas(): def test_no_school_bootstrap_requires_school_membership_but_allows_canvas():
+3 -1
View File
@@ -26,8 +26,10 @@ def test_supabase_anon_for_user_sets_user_authorization_header(monkeypatch):
client_module.SupabaseAnonClient.for_user('Bearer user-jwt') client_module.SupabaseAnonClient.for_user('Bearer user-jwt')
# apikey comes from the `key` arg (supabase-py sets the apikey header); options.headers must
# carry only the user Authorization override. A second apikey here → Kong "Duplicate API key".
assert captured['key'] == 'anon-key' assert captured['key'] == 'anon-key'
assert captured['options'].headers['apikey'] == 'anon-key' assert 'apikey' not in captured['options'].headers
assert captured['options'].headers['Authorization'] == 'Bearer user-jwt' assert captured['options'].headers['Authorization'] == 'Bearer user-jwt'