Download tests/native_api/test_supabase_auth.py from hunian003/message: direct link, hf CLI and curl.
- Browser
- Download file 16.3 kB
-
https://huggingface.co/spaces/hunian003/message/resolve/main/tests/native_api/test_supabase_auth.py
- Command line
-
hf download hf://spaces/hunian003/message/tests/native_api/test_supabase_auth.py
-
curl -L -o test_supabase_auth.py https://huggingface.co/spaces/hunian003/message/resolve/main/tests/native_api/test_supabase_auth.py
16.3 kB
| """只用官方 HTTP 形状的 fixture 验证 Supabase 适配器,不创建外部账号或凭据。""" | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| from pathlib import Path | |
| import sys | |
| import traceback | |
| import unittest | |
| import httpx | |
| ROOT = Path(__file__).resolve().parents[2] | |
| sys.path.insert(0, str(ROOT / "apps/market-api")) | |
| from app.native.auth_supabase import AuthDenied, AuthUnavailable, AuthRateLimited, RECOVERY_CALLBACK, RECOVERY_MESSAGE, SupabaseAuth | |
| URL = "https://fixtureproject.supabase.co" | |
| KEY = "sb_publishable_fixture_not_a_secret" | |
| ORIGIN = "https://message.example" | |
| USER_ID = "123e4567-e89b-42d3-a456-426614174000" | |
| EMAIL = "fixture@example.invalid" | |
| ACCESS = "fixture.access.token" | |
| REFRESH = "fixture-refresh-token" | |
| PASSWORD = "fixture-password-only" | |
| VERIFIER = "v" * 64 | |
| CHALLENGE = "c" * 43 | |
| def identity(**changes): | |
| return {"id": USER_ID, "email": EMAIL, "role": "authenticated", "is_anonymous": False, **changes} | |
| def session(**changes): | |
| return {"access_token": ACCESS, "refresh_token": REFRESH, "expires_in": 3600, "token_type": "bearer", "user": identity(), **changes} | |
| class SupabaseAuthTests(unittest.IsolatedAsyncioTestCase): | |
| def adapter(self, handler): | |
| client = httpx.AsyncClient(transport=httpx.MockTransport(handler), follow_redirects=True, | |
| headers={"Authorization": "Bearer unrelated-client-secret", "X-Unrelated": "private"}, | |
| cookies={"unrelated": "private"}, auth=("other-user", "other-password")) | |
| self.addAsyncCleanup(client.aclose) | |
| return SupabaseAuth(client, URL, KEY, ORIGIN) | |
| async def test_login_uses_official_endpoint_and_drops_unrelated_client_credentials(self): | |
| requests = [] | |
| def handler(request): | |
| requests.append(request) | |
| self.assertEqual(request.method, "POST") | |
| self.assertEqual(str(request.url), URL + "/auth/v1/token?grant_type=password") | |
| self.assertEqual(json.loads(request.content), {"email": EMAIL, "password": "short"}) | |
| self.assertEqual(request.headers["apikey"], KEY) | |
| for name in ("authorization", "cookie", "x-unrelated"): | |
| self.assertNotIn(name, request.headers) | |
| return httpx.Response(200, json=session(provider_token="must-drop", user=identity(user_metadata={"admin": True}))) | |
| result = await self.adapter(handler).login(EMAIL, "short") | |
| self.assertEqual(result, {"access_token": ACCESS, "refresh_token": REFRESH, "expires_in": 3600, "user": {"id": USER_ID, "email": EMAIL}}) | |
| self.assertEqual(len(requests), 1) | |
| async def test_identity_and_admin_membership_are_checked_on_every_request(self): | |
| requests, checks = [], [] | |
| def handler(request): | |
| requests.append(request) | |
| self.assertEqual(request.headers["Authorization"], "Bearer " + ACCESS) | |
| self.assertEqual(request.headers["apikey"], KEY) | |
| if request.url.path == "/auth/v1/user": | |
| return httpx.Response(200, json=identity(user_metadata={"admin": True}, app_metadata={"role": "admin"})) | |
| self.assertEqual(request.url.path, "/rest/v1/rpc/message_current_admin") | |
| self.assertEqual(request.method, "POST") | |
| self.assertEqual(json.loads(request.content), {}) | |
| checks.append(True) | |
| return httpx.Response(200, json=len(checks) == 1) | |
| auth = self.adapter(handler) | |
| self.assertEqual(await auth.user(ACCESS), {"id": USER_ID, "email": EMAIL}) | |
| self.assertIsNone(await auth.user(ACCESS), "撤销 membership 或会话必须在下一次请求拒绝") | |
| self.assertEqual(len(requests), 4) | |
| async def test_anonymous_and_service_identities_are_never_admins(self): | |
| for changed in ({"is_anonymous": True}, {"is_anonymous": None}, {"role": "anon"}, {"role": "service_role"}): | |
| requests = [] | |
| def handler(request): | |
| requests.append(request) | |
| return httpx.Response(200, json=identity(**changed, user_metadata={"admin": True})) | |
| with self.subTest(changed=changed): | |
| self.assertIsNone(await self.adapter(handler).user(ACCESS)) | |
| self.assertEqual(len(requests), 1) | |
| async def test_invalid_or_revoked_session_is_denied_without_leaking_response(self): | |
| for status in (400, 401, 403, 422): | |
| with self.subTest(status=status): | |
| auth = self.adapter(lambda _: httpx.Response(status, json={"message": ACCESS, "error_code": "session_not_found"})) | |
| self.assertIsNone(await auth.user(ACCESS)) | |
| async def test_false_membership_cannot_be_overridden_by_metadata(self): | |
| def handler(request): | |
| if request.url.path == "/auth/v1/user": | |
| return httpx.Response(200, json=identity(user_metadata={"role": "admin"}, app_metadata={"role": "admin"})) | |
| return httpx.Response(200, json=False) | |
| self.assertIsNone(await self.adapter(handler).user(ACCESS)) | |
| async def test_recovery_identity_uses_separate_permission_rpc(self): | |
| requested = [] | |
| def handler(request): | |
| requested.append(request.url.path) | |
| if request.url.path == "/auth/v1/user": | |
| return httpx.Response(200, json=identity()) | |
| return httpx.Response(200, json=request.url.path == "/rest/v1/rpc/message_recovery_admin") | |
| auth = self.adapter(handler) | |
| self.assertIsNone(await auth.user(ACCESS)) | |
| self.assertEqual(await auth.user(ACCESS, recovery=True), {"id": USER_ID, "email": EMAIL}) | |
| self.assertEqual(requested, ["/auth/v1/user", "/rest/v1/rpc/message_current_admin", "/auth/v1/user", "/rest/v1/rpc/message_recovery_admin"]) | |
| async def test_malformed_membership_never_becomes_truthy_authorization(self): | |
| for value in (None, 1, "true", {}, [{"message_current_admin": True}]): | |
| def handler(request): | |
| return httpx.Response(200, json=identity() if request.url.path == "/auth/v1/user" else value) | |
| with self.subTest(value=value), self.assertRaises(AuthUnavailable): | |
| await self.adapter(handler).user(ACCESS) | |
| async def test_missing_rpc_is_an_unavailable_service_not_a_non_admin(self): | |
| def handler(request): | |
| return httpx.Response(200, json=identity()) if request.url.path == "/auth/v1/user" else httpx.Response(404, text="function absent") | |
| with self.assertRaises(AuthUnavailable): | |
| await self.adapter(handler).user(ACCESS) | |
| async def test_identity_requires_canonical_nonzero_uuid(self): | |
| for value in ("first-user", "00000000-0000-0000-0000-000000000000", USER_ID.upper(), None, 3): | |
| with self.subTest(value=value), self.assertRaises(AuthUnavailable): | |
| await self.adapter(lambda _: httpx.Response(200, json=identity(id=value))).user(ACCESS) | |
| async def test_refresh_uses_single_request_and_returns_only_session_fields(self): | |
| requests = [] | |
| def handler(request): | |
| requests.append(request) | |
| self.assertEqual(request.url.params["grant_type"], "refresh_token") | |
| self.assertEqual(json.loads(request.content), {"refresh_token": REFRESH}) | |
| self.assertNotIn("authorization", request.headers) | |
| return httpx.Response(200, json=session(refresh_token="fixture-rotated-refresh")) | |
| result = await self.adapter(handler).refresh(REFRESH) | |
| self.assertEqual(result["refresh_token"], "fixture-rotated-refresh") | |
| self.assertEqual(len(requests), 1) | |
| async def test_invalid_upstream_sessions_are_rejected(self): | |
| for changed in ({"access_token": ""}, {"access_token": "bad\nheader"}, {"refresh_token": None}, {"expires_in": 0}, {"expires_in": True}, {"expires_in": 86401}, {"token_type": "MAC"}, {"user": {}}): | |
| with self.subTest(changed=changed), self.assertRaises((AuthUnavailable, AuthDenied)): | |
| await self.adapter(lambda _: httpx.Response(200, json=session(**changed))).login(EMAIL, PASSWORD) | |
| async def test_recovery_uses_s256_and_fixed_public_origin_callback(self): | |
| requests = [] | |
| def handler(request): | |
| requests.append(request) | |
| self.assertEqual(request.url.path, "/auth/v1/recover") | |
| self.assertEqual(request.url.params["redirect_to"], ORIGIN + RECOVERY_CALLBACK) | |
| self.assertEqual(json.loads(request.content), {"email": EMAIL, "code_challenge": CHALLENGE, "code_challenge_method": "s256"}) | |
| self.assertNotIn(EMAIL, str(request.url)) | |
| self.assertNotIn(VERIFIER, str(request.url)) | |
| return httpx.Response(200, json={}) | |
| self.assertEqual(await self.adapter(handler).recover(EMAIL, CHALLENGE), RECOVERY_MESSAGE) | |
| self.assertEqual(len(requests), 1) | |
| async def test_recovery_code_and_verifier_are_only_in_post_body(self): | |
| code = "fixture-single-use-code" | |
| def handler(request): | |
| self.assertEqual(request.method, "POST") | |
| self.assertEqual(str(request.url), URL + "/auth/v1/token?grant_type=pkce") | |
| self.assertEqual(json.loads(request.content), {"auth_code": code, "code_verifier": VERIFIER}) | |
| self.assertNotIn(code, str(request.url)) | |
| self.assertNotIn(VERIFIER, str(request.url)) | |
| return httpx.Response(200, json=session()) | |
| self.assertEqual((await self.adapter(handler).exchange_recovery(code, VERIFIER))["user"]["id"], USER_ID) | |
| async def test_password_update_is_only_a_supabase_user_update(self): | |
| def handler(request): | |
| self.assertEqual(request.method, "PUT") | |
| self.assertEqual(request.url.path, "/auth/v1/user") | |
| self.assertEqual(json.loads(request.content), {"password": PASSWORD}) | |
| self.assertEqual(request.headers["authorization"], "Bearer " + ACCESS) | |
| return httpx.Response(200, json=identity()) | |
| self.assertEqual(await self.adapter(handler).update_password(ACCESS, PASSWORD), {"id": USER_ID, "email": EMAIL}) | |
| async def test_logout_is_scoped_to_current_supabase_session(self): | |
| def handler(request): | |
| self.assertEqual(request.method, "POST") | |
| self.assertEqual(str(request.url), URL + "/auth/v1/logout?scope=local") | |
| self.assertEqual(request.headers["authorization"], "Bearer " + ACCESS) | |
| return httpx.Response(204) | |
| self.assertIsNone(await self.adapter(handler).logout(ACCESS)) | |
| async def test_redirects_never_receive_credentials(self): | |
| requests = [] | |
| def handler(request): | |
| requests.append(request) | |
| return httpx.Response(307, headers={"location": "https://attacker.invalid/collect"}) | |
| with self.assertRaises(AuthUnavailable): | |
| await self.adapter(handler).login(EMAIL, PASSWORD) | |
| self.assertEqual(len(requests), 1) | |
| self.assertEqual(requests[0].url.host, "fixtureproject.supabase.co") | |
| async def test_denial_rate_limit_and_network_errors_never_expose_secrets(self): | |
| leaked = " ".join((PASSWORD, ACCESS, REFRESH, EMAIL, VERIFIER)) | |
| for status in (400, 401, 403, 422, 429, 500): | |
| expected = AuthDenied if status in {400, 401, 403, 422} else AuthUnavailable | |
| with self.subTest(status=status): | |
| auth = self.adapter(lambda _: httpx.Response(status, json={"error": leaked})) | |
| try: | |
| await auth.login(EMAIL, PASSWORD) | |
| except expected as error: | |
| self.assertNotIn(leaked, "".join(traceback.format_exception(error))) | |
| else: | |
| self.fail("上游失败必须拒绝") | |
| calls = [] | |
| def timeout(request): | |
| calls.append(request) | |
| raise httpx.ReadTimeout(leaked, request=request) | |
| try: | |
| await self.adapter(timeout).refresh(REFRESH) | |
| except AuthUnavailable as error: | |
| self.assertNotIn(leaked, "".join(traceback.format_exception(error))) | |
| else: | |
| self.fail("刷新超时必须失败关闭") | |
| self.assertEqual(len(calls), 1, "不得自动重复刷新单次令牌") | |
| async def test_malformed_json_and_logout_failure_have_generic_errors(self): | |
| with self.assertRaises(AuthUnavailable) as raised: | |
| await self.adapter(lambda _: httpx.Response(200, text=ACCESS)).login(EMAIL, PASSWORD) | |
| self.assertNotIn(ACCESS, "".join(traceback.format_exception(raised.exception))) | |
| with self.assertRaises(AuthDenied) as raised: | |
| await self.adapter(lambda _: httpx.Response(403, json={"message": ACCESS})).logout(ACCESS) | |
| self.assertNotIn(ACCESS, str(raised.exception)) | |
| async def test_secret_bodies_do_not_appear_in_http_client_logs(self): | |
| with self.assertLogs("httpx", level=logging.INFO) as logs: | |
| await self.adapter(lambda _: httpx.Response(200, json=session())).login(EMAIL, PASSWORD) | |
| emitted = "\n".join(logs.output) | |
| for secret in (PASSWORD, EMAIL, ACCESS, REFRESH): | |
| self.assertNotIn(secret, emitted) | |
| async def test_invalid_inputs_do_not_make_network_requests(self): | |
| requests = [] | |
| def handler(request): | |
| requests.append(request) | |
| return httpx.Response(200, json=session()) | |
| auth = self.adapter(handler) | |
| for operation in (auth.login(EMAIL, ""), auth.login(EMAIL, "x" * 257), auth.refresh("bad\r\nheader"), auth.exchange_recovery("code", "short"), auth.recover(EMAIL, "bad"), auth.update_password(ACCESS, "short"), auth.update_password(ACCESS, "x" * 257)): | |
| with self.assertRaises(AuthDenied): | |
| await operation | |
| self.assertIsNone(await auth.user("bad\nheader")) | |
| self.assertEqual(requests, []) | |
| async def test_constructor_rejects_credential_destination_confusion(self): | |
| client = httpx.AsyncClient(transport=httpx.MockTransport(lambda _: httpx.Response(200))) | |
| self.addAsyncCleanup(client.aclose) | |
| for url in ("http://fixture.supabase.co", "https://supabase.co", "https://fixture.supabase.co.evil", "https://nested.fixture.supabase.co", "https://user@fixture.supabase.co", URL + "/auth/v1", URL + "?key=secret", URL + "#fragment", URL + ":443"): | |
| with self.subTest(url=url), self.assertRaises(ValueError): | |
| SupabaseAuth(client, url, KEY, ORIGIN) | |
| for key in ("sb_secret_fixture", "service_role", "sb_publishable_bad\nheader", ""): | |
| with self.subTest(key=key), self.assertRaises(ValueError): | |
| SupabaseAuth(client, URL, key, ORIGIN) | |
| for origin in ("http://external.example", ORIGIN + "/path", ORIGIN + "?next=bad", "https://user:password@message.example", "https://message.example\\evil", "https://message.example:bad"): | |
| with self.subTest(origin=origin), self.assertRaises(ValueError): | |
| SupabaseAuth(client, URL, KEY, origin) | |
| for origin in (ORIGIN, ORIGIN + "/", "http://127.0.0.1:8000", "http://localhost:8000", "http://[::1]:8000"): | |
| SupabaseAuth(client, URL + "/", KEY, origin) | |
| async def test_recovery_rate_limit_has_clear_safe_message_and_retry_header(self): | |
| adapter = self.adapter(lambda _request: httpx.Response(429, headers={'Retry-After': '120'}, json={'code':'over_email_send_rate_limit', 'msg':'fixture-password-and-secret-must-not-leak'})) | |
| with self.assertRaises(AuthRateLimited) as caught: | |
| await adapter.recover(EMAIL, CHALLENGE) | |
| self.assertEqual(caught.exception.status_code, 429) | |
| self.assertEqual(caught.exception.retry_after, '120') | |
| self.assertIn('邮件发送过于频繁', str(caught.exception)) | |
| self.assertNotIn('fixture-password', str(caught.exception)) | |
| async def test_rate_limit_does_not_invent_wait_time(self): | |
| adapter = self.adapter(lambda _request: httpx.Response(429, json={'code':'over_email_send_rate_limit'})) | |
| with self.assertRaises(AuthRateLimited) as caught: | |
| await adapter.recover(EMAIL, CHALLENGE) | |
| self.assertIsNone(caught.exception.retry_after) | |
| for header in ('99999999', 'invalid', '-1', '0'): | |
| self.assertIsNone(AuthRateLimited(retry_after=header).retry_after) | |
| if __name__ == "__main__": | |
| unittest.main() | |