import os from fastapi import FastAPI from fastapi.testclient import TestClient from routers.me.bootstrap_router import router, auth_scheme from modules.database.services.bootstrap_service import BootstrapService USER_ID = "00000000-0000-0000-0000-000000000001" INST_A = "10000000-0000-0000-0000-000000000001" INST_B = "10000000-0000-0000-0000-000000000002" class FakeResult: def __init__(self, data): self.data = data class FakeQuery: def __init__(self, rows): self.rows = list(rows) self._single = False self._limit = None def select(self, *_args, **_kwargs): return self def eq(self, key, value): self.rows = [row for row in self.rows if row.get(key) == value] return self def in_(self, key, values): values = set(values) self.rows = [row for row in self.rows if row.get(key) in values] return self def order(self, *_args, **_kwargs): return self def limit(self, count): self._limit = count return self def single(self): self._single = True return self def execute(self): rows = self.rows[: self._limit] if self._limit is not None else self.rows if self._single: return FakeResult(rows[0] if rows else None) return FakeResult(rows) class FakeSupabase: def __init__(self, tables): self.tables = tables def table(self, name): return FakeQuery(self.tables.get(name, [])) def profile(user_type="teacher", school_id=None): return { "id": USER_ID, "email": "teacher@example.test", "full_name": "Example Teacher", "display_name": None, "user_type": user_type, "school_id": school_id, } def institute(id_=INST_A, name="Example School", status="active"): return { "id": id_, "name": name, "urn": "123456", "website": "https://school.example", "address": {"town": "Testville"}, "metadata": {"internal_note": "should remain institute metadata only"}, "status": status, "neo4j_uuid_string": "neo-a" if id_ == INST_A else "neo-b", } def membership(institute_id=INST_A, role="teacher"): return {"profile_id": USER_ID, "institute_id": institute_id, "role": role} def service(tables, graph_probe=None): return BootstrapService(FakeSupabase(tables), graph_probe=graph_probe or (lambda **_: {"available": True, "projection_state": "ready"})) def build(credentials=None, tables=None, graph_probe=None): credentials = credentials or {"sub": USER_ID, "email": "teacher@example.test"} tables = tables or {"profiles": [profile()]} return service(tables, graph_probe).build(credentials) def test_supabase_client_for_user_uses_access_token_authorization(monkeypatch): from modules.database.supabase.utils import client as client_module captured = {} class FakeOptions: def __init__(self, **kwargs): captured["options_kwargs"] = kwargs def fake_create_client(url, key, options=None): captured["url"] = url captured["key"] = key captured["options"] = options return {"ok": True} monkeypatch.setenv("SUPABASE_URL", "http://supabase.test") monkeypatch.setenv("ANON_KEY", "anon-key") monkeypatch.setattr(client_module, "SyncClientOptions", FakeOptions) monkeypatch.setattr(client_module, "create_client", fake_create_client) anon = client_module.SupabaseAnonClient.for_user("user-token") assert anon.access_token == "user-token" assert captured["url"] == "http://supabase.test" assert captured["key"] == "anon-key" assert captured["options_kwargs"]["headers"] == {"Authorization": "Bearer user-token"} def test_no_school_bootstrap_requires_school_membership_but_allows_canvas(): payload = build(tables={"profiles": [profile()]}) assert payload["school_status"] == "no_school" assert payload["active_institute"]["source"] == "none" assert payload["memberships"] == [] assert payload["permissions"]["can_create_school"] is True assert payload["permissions"]["can_use_canvas"] is True assert payload["onboarding"]["next_step"] == "create_or_join_school" assert "school_membership" in payload["onboarding"]["required"] def test_single_membership_becomes_active_and_uses_supabase_calendar_timetable_status(): payload = build(tables={ "profiles": [profile()], "institute_memberships": [membership()], "institutes": [institute()], "academic_years": [{"id": "ay-1", "institute_id": INST_A, "is_current": True}], "academic_terms": [{"id": "term-1", "institute_id": INST_A}], "school_timetables": [{"id": "school-tt-1", "institute_id": INST_A}], "teacher_timetables": [{"id": "teacher-tt-1", "institute_id": INST_A, "teacher_profile_id": USER_ID}], "teacher_timetable_slots": [ {"id": "slot-1", "teacher_timetable_id": "teacher-tt-1"}, {"id": "slot-2", "teacher_timetable_id": "teacher-tt-1"}, ], }) assert payload["school_status"] == "member" assert payload["active_institute"] == {"id": INST_A, "source": "single_membership", "membership_role": "teacher"} assert payload["calendar_status"] == { "available": True, "academic_year_count": 1, "term_count": 1, "current_academic_year_id": "ay-1", "needs_setup": False, } assert payload["timetable_status"] == { "available": True, "teacher_timetable_id": "teacher-tt-1", "slot_count": 2, "needs_setup": False, } assert payload["onboarding"]["next_step"] == "ready" def test_multiple_memberships_require_selection_when_no_profile_school_id_matches(): payload = build(tables={ "profiles": [profile()], "institute_memberships": [membership(INST_A, "teacher"), membership(INST_B, "department_head")], "institutes": [institute(INST_A, "Alpha"), institute(INST_B, "Beta")], }) assert payload["school_status"] == "multi_school_needs_selection" assert payload["active_institute"] == {"id": None, "source": "none", "membership_role": None} assert payload["onboarding"]["next_step"] == "select_school" assert "active_school_selection" in payload["onboarding"]["required"] def test_school_admin_permissions_and_onboarding_invite_staff_after_calendar_timetable_ready(): payload = build(tables={ "profiles": [profile(user_type="school_admin", school_id=INST_A)], "institute_memberships": [membership(INST_A, "school_admin")], "institutes": [institute()], "academic_years": [{"id": "ay-1", "institute_id": INST_A, "is_current": True}], "academic_terms": [{"id": "term-1", "institute_id": INST_A}], "teacher_timetables": [{"id": "teacher-tt-1", "institute_id": INST_A, "teacher_profile_id": USER_ID}], "teacher_timetable_slots": [{"id": "slot-1", "teacher_timetable_id": "teacher-tt-1"}], }) assert payload["school_status"] == "school_admin" perms = payload["permissions"] assert perms["can_manage_school"] is True assert perms["can_manage_calendar"] is True assert perms["can_manage_timetable"] is True assert perms["can_invite_staff"] is True assert perms["can_view_student_data"] is True assert payload["onboarding"]["next_step"] == "invite_staff" def test_platform_admin_uses_admin_profiles_without_relying_on_profile_super_admin_type(): payload = build(credentials={"sub": USER_ID, "email": "admin@example.test"}, tables={ "profiles": [profile(user_type="teacher")], "admin_profiles": [{"id": USER_ID, "admin_role": "owner", "is_super_admin": True}], }) assert payload["profile"]["user_type"] == "platform_admin" assert payload["school_status"] == "platform_admin" assert payload["permissions"]["platform_admin"] is True assert payload["permissions"]["platform_super_admin"] is True assert payload["permissions"]["can_create_school"] is True def test_neo4j_probe_failure_does_not_block_supabase_bootstrap_state(): def failing_probe(**_kwargs): raise RuntimeError("connection refused with host details that must not leak") payload = build(tables={ "profiles": [profile()], "institute_memberships": [membership()], "institutes": [institute()], }, graph_probe=failing_probe) assert payload["school_status"] == "member" assert payload["active_institute"]["id"] == INST_A assert payload["graph_status"]["available"] is False assert payload["graph_status"]["projection_state"] == "error" assert payload["graph_status"]["needs_rebuild"] is True assert payload["graph_status"]["error_code"] == "neo4j_unavailable" assert "connection refused" not in str(payload) def test_route_is_authenticated_and_does_not_return_raw_auth_metadata(monkeypatch): app = FastAPI() app.include_router(router, prefix="/me") app.dependency_overrides[auth_scheme] = lambda: {"sub": USER_ID, "email": "teacher@example.test", "app_metadata": {"secret": "nope"}} monkeypatch.setattr( "routers.me.bootstrap_router.build_bootstrap_response", lambda credentials: { "profile": {"id": USER_ID, "email": credentials["email"], "display_name": "Example", "user_type": "teacher", "school_id": None}, "memberships": [], "active_institute": {"id": None, "source": "none", "membership_role": None}, "permissions": {"platform_admin": False}, "school_status": "no_school", "onboarding": {"next_step": "create_or_join_school", "required": [], "optional": [], "message": "Create or join a school."}, "calendar_status": {"available": False, "academic_year_count": 0, "term_count": 0, "current_academic_year_id": None, "needs_setup": True}, "timetable_status": {"available": False, "teacher_timetable_id": None, "slot_count": 0, "needs_setup": True}, "graph_status": {"available": False, "user_db": None, "institute_db": None, "projection_state": "unknown", "needs_rebuild": True, "last_checked_at": "2026-05-28T00:00:00Z", "error_code": None}, }, ) response = TestClient(app).get("/me/bootstrap") assert response.status_code == 200 assert "app_metadata" not in response.text assert "secret" not in response.text