File size: 4,699 Bytes
66fd359 a7ffb22 66fd359 a7ffb22 66fd359 a7ffb22 66fd359 a7ffb22 66fd359 a7ffb22 66fd359 a7ffb22 66fd359 a7ffb22 66fd359 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | import io
import json
import urllib.error
import pytest
from fastapi.testclient import TestClient
from mathly2 import auth
from mathly2.auth import AuthUnavailable, DevAuthenticator, SupabaseAuthenticator
from mathly2.config import Settings
from mathly2.main import create_app
TEACHER = "teacher@school.test"
@pytest.fixture
def client():
return TestClient(create_app(Settings(teachers=frozenset({TEACHER}))))
def sign(email):
return {"Authorization": f"Bearer dev:{email}"}
def test_no_token_is_refused(client):
assert client.get("/api/me").status_code == 401
assert client.get("/api/me", headers={"Authorization": "Bearer nonsense"}).status_code == 401
assert client.get("/api/me", headers={"Authorization": "Bearer dev:not-an-email"}).status_code == 401
def me(client, email):
return client.get("/api/me", headers=sign(email)).json()
def test_a_listed_teacher_is_a_teacher(client):
body = me(client, TEACHER)
assert body["email"] == TEACHER and body["roles"] == ["teacher"] and body["classes"] == [] and body["teaching"] == []
assert "class.teach" in body["permissions"] and "classes.manage" not in body["permissions"]
assert me(client, "Teacher@School.test")["roles"] == ["teacher"] # case does not matter
def test_everyone_else_is_a_student(client):
body = me(client, "kid@school.test")
assert body["roles"] == ["student"] and body["permissions"] == ["path.use"] and body["classes"] == []
def test_developers_and_first_admins_come_from_the_settings():
app = create_app(Settings(developers=frozenset({"dev@school.test"}), admins=frozenset({"head@school.test"})))
client = TestClient(app)
assert me(client, "dev@school.test")["roles"] == ["developer"] and me(client, "dev@school.test")["permissions"] == ["*"]
assert me(client, "head@school.test")["roles"] == ["admin"] and me(client, "head@school.test")["permissions"] == ["roles.manage"]
def test_page_carries_only_public_settings():
page = TestClient(create_app(Settings())).get("/").text
assert '"devSignIn": true' in page and "__CONFIG__" not in page
page = TestClient(create_app(Settings(supabase_url="https://p.test", supabase_key="pub", supabase_secret="SECRET-VALUE"))).get("/").text
assert '"supabaseKey": "pub"' in page and '"devSignIn": false' in page and "SECRET-VALUE" not in page
def test_half_configured_supabase_does_not_fall_back_to_dev_sign_in():
with pytest.raises(ValueError, match="all of SUPABASE_URL"):
create_app(Settings(supabase_url="https://p.test"))
def test_dev_sign_in_is_off_when_supabase_is_on():
app = create_app(Settings(supabase_url="https://p.test", supabase_key="pub", supabase_secret="sec"))
assert isinstance(app.state.authenticator, SupabaseAuthenticator)
assert TestClient(app).get("/api/me", headers=sign("kid@school.test")).status_code in (401, 503)
def supabase_returns(monkeypatch, payload=None, error=None):
"""Stand in for the HTTP call to Supabase's /auth/v1/user: the calls made are returned as (url, headers)."""
calls = []
def fake(method, url, headers=None, body=None, timeout=0):
calls.append((url, headers or {}))
if error:
raise error
return 200, json.dumps(payload).encode()
monkeypatch.setattr(auth.pooled, "request", fake)
return calls
def test_supabase_user_is_checked_with_the_token_and_cached(monkeypatch):
calls = supabase_returns(monkeypatch, {"id": "u1", "email": "Kid@School.test", "email_confirmed_at": "2026-01-01"})
a = SupabaseAuthenticator("https://p.test", "pub")
assert a.user("tok") == {"id": "u1", "email": "kid@school.test"}
assert a.user("tok")["id"] == "u1"
assert len(calls) == 1 and calls[0][1]["Authorization"] == "Bearer tok" and calls[0][0].endswith("/auth/v1/user")
def test_an_unconfirmed_email_is_not_trusted(monkeypatch):
supabase_returns(monkeypatch, {"id": "u1", "email": TEACHER})
assert SupabaseAuthenticator("https://p.test", "pub").user("tok") is None
def test_rejected_token_and_outage(monkeypatch):
supabase_returns(monkeypatch, error=urllib.error.HTTPError("u", 401, "no", {}, None))
assert SupabaseAuthenticator("https://p.test", "pub").user("bad") is None
supabase_returns(monkeypatch, error=urllib.error.URLError("down"))
with pytest.raises(AuthUnavailable):
SupabaseAuthenticator("https://p.test", "pub").user("tok")
def test_outage_gives_503(monkeypatch):
class Down:
def user(self, token): raise AuthUnavailable("down")
app = create_app(Settings(), authenticator=Down())
assert TestClient(app).get("/api/me", headers={"Authorization": "Bearer x"}).status_code == 503
|