Download mathly2/tests/test_classes.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/tests/test_classes.py
- Command line
-
hf download hf://spaces/wzender/mathly/mathly2/tests/test_classes.py
-
curl -L -o test_classes.py https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/tests/test_classes.py
12.6 kB
| import pytest | |
| from fastapi.testclient import TestClient | |
| from mathly2.classes import parse_emails | |
| from mathly2.config import Settings | |
| from mathly2.main import create_app | |
| from mathly2.store import InMemoryStore | |
| MGR, T1, T2, DEV = "mgr@school.test", "t1@school.test", "t2@school.test", "dev@school.test" | |
| def auth(email): | |
| return {"Authorization": f"Bearer dev:{email}"} | |
| def world(): | |
| store = InMemoryStore() | |
| store.give_role(MGR, "manager", "setup") | |
| client = TestClient(create_app(Settings(teachers=frozenset({T1, T2}), developers=frozenset({DEV})), store=store)) | |
| return client, store | |
| def body(name="讟'3", **kw): | |
| return {"name": name, "lessons": kw.pop("lessons", ["t", "t2"]), **kw} | |
| def make(client, who=MGR, **kw): | |
| return client.post("/api/classes", json=body(**kw), headers=auth(who)) | |
| def test_parse_emails_handles_pasted_lists(): | |
| valid, invalid = parse_emails(" A@x.test, b@x.test;\nc@x.test a@x.test not-an-email ") | |
| assert valid == ["a@x.test", "b@x.test", "c@x.test"] and invalid == ["not-an-email"] | |
| # ---- the manager makes classes (spec: Manager creates classes) | |
| def test_a_manager_creates_a_class_with_teacher_and_students_and_it_studies_every_lesson(world): | |
| client, _ = world | |
| created = make(client, teacher_email=T1, students="s1@school.test\ns2@school.test not-an-email") | |
| assert created.status_code == 201 | |
| row = created.json() | |
| assert row["name"] == "讟'3" and len(row["lessons"]) == 24 and row["teacher_email"] == T1 | |
| assert row["ai_enabled"] is False and row["ai_daily_checks"] == 150 | |
| assert row["students"] == 2 and row["student_emails"] == ["s1@school.test", "s2@school.test"] and row["invalid"] == ["not-an-email"] | |
| assert [c["name"] for c in client.get("/api/classes", headers=auth(MGR)).json()] == ["讟'3"] | |
| def test_a_developer_can_manage_classes_too(world): | |
| client, _ = world | |
| assert make(client, who=DEV).status_code == 201 | |
| def test_a_teacher_a_student_and_a_stranger_cannot_create_classes(world): | |
| client, _ = world | |
| assert make(client, who=T1).status_code == 403 | |
| assert make(client, who="kid@school.test").status_code == 403 | |
| assert client.post("/api/classes", json=body()).status_code == 401 | |
| assert client.get("/api/classes", headers=auth(T1)).status_code == 403 | |
| def test_class_name_is_trimmed_checked_and_unique_ignoring_case(world): | |
| client, _ = world | |
| assert make(client, name=" 讞' 1 ").json()["name"] == "讞' 1" | |
| assert make(client, name="讞' 1").status_code == 409 | |
| assert make(client, name=" ").status_code == 422 | |
| assert make(client, name="x" * 81).status_code == 422 | |
| assert make(client, name="x" * 80).status_code == 201 | |
| def test_lessons_in_a_request_are_ignored_every_class_studies_every_lesson(world): | |
| client, _ = world | |
| assert len(make(client, lessons=[]).json()["lessons"]) == 24 | |
| assert len(make(client, name="B", lessons=["nope"]).json()["lessons"]) == 24 | |
| def test_the_teacher_must_be_an_email_and_not_also_a_student(world): | |
| client, _ = world | |
| assert make(client, teacher_email="not-an-email").status_code == 422 | |
| clash = make(client, teacher_email=T1, students=f"s@school.test {T1}") | |
| assert clash.status_code == 422 and "转诇诪讬讚" in clash.json()["detail"] | |
| assert client.get("/api/classes", headers=auth(MGR)).json() == [] # nothing was saved | |
| def test_the_ai_budget_is_limited(world): | |
| client, _ = world | |
| assert make(client, ai_daily_checks=-1).status_code == 422 | |
| assert make(client, name="B", ai_daily_checks=10001).status_code == 422 | |
| assert make(client, name="C", ai_daily_checks=40, ai_enabled=True).json()["ai_enabled"] is True | |
| def test_there_is_no_lesson_list_to_choose_from(world): | |
| client, _ = world | |
| assert client.get("/api/classes/lessons", headers=auth(MGR)).status_code in (404, 405, 422) | |
| # ---- editing and deleting (spec: Editing and deleting a class) | |
| def test_a_manager_edits_everything_and_the_roster_follows(world): | |
| client, store = world | |
| cid = make(client, teacher_email=T1, students="a@school.test b@school.test").json()["id"] | |
| edited = client.put(f"/api/classes/{cid}", json=body("讟'4", teacher_email=T2, students="b@school.test c@school.test", | |
| ai_enabled=True, ai_daily_checks=60), headers=auth(MGR)).json() | |
| assert (edited["name"], edited["lessons"], edited["teacher_email"], edited["ai_enabled"], edited["ai_daily_checks"]) == ("讟'4", edited["lessons"], T2, True, 60) | |
| assert edited["student_emails"] == ["b@school.test", "c@school.test"] | |
| def test_leaving_the_students_out_of_an_edit_keeps_the_roster(world): | |
| client, _ = world | |
| cid = make(client, students="a@school.test").json()["id"] | |
| kept = client.put(f"/api/classes/{cid}", json=body("讟'3"), headers=auth(MGR)).json() | |
| assert kept["student_emails"] == ["a@school.test"] | |
| def test_removing_a_student_through_an_edit_keeps_their_progress(world): | |
| client, store = world | |
| cid = make(client, students="a@school.test").json()["id"] | |
| store.save_progress("u-a", "t_ext", started_at="then") | |
| client.put(f"/api/classes/{cid}", json=body(students=""), headers=auth(MGR)) | |
| assert client.get(f"/api/classes/{cid}", headers=auth(MGR)).json()["students"] == 0 | |
| assert store.get_progress("u-a", "t_ext")["started_at"] == "then" | |
| def test_editing_cannot_clash_with_another_class_or_make_a_student_the_teacher(world): | |
| client, _ = world | |
| make(client, name="A") | |
| cid = make(client, name="B", students="s@school.test").json()["id"] | |
| assert client.put(f"/api/classes/{cid}", json=body("a"), headers=auth(MGR)).status_code == 409 | |
| assert client.put(f"/api/classes/{cid}", json=body("B", teacher_email="s@school.test"), headers=auth(MGR)).status_code == 422 | |
| assert client.put("/api/classes/999", json=body("Z"), headers=auth(MGR)).status_code == 404 | |
| def test_deleting_a_class_asks_nothing_of_the_students_progress(world): | |
| client, store = world | |
| cid = make(client, students="a@school.test").json()["id"] | |
| store.save_progress("u-a", "t_ext", started_at="then") | |
| assert client.delete(f"/api/classes/{cid}", headers=auth(MGR)).status_code == 204 | |
| assert client.delete(f"/api/classes/{cid}", headers=auth(MGR)).status_code == 404 | |
| assert client.get("/api/classes", headers=auth(MGR)).json() == [] | |
| assert store.get_progress("u-a", "t_ext")["started_at"] == "then" | |
| # ---- the roster (spec: Teacher adds students by email) | |
| def test_the_classes_teacher_adds_a_list_of_students(world): | |
| client, _ = world | |
| cid = make(client, teacher_email=T1).json()["id"] | |
| emails = "\n".join(f"s{i}@school.test" for i in range(25)) + "\ndana@" | |
| result = client.post(f"/api/classes/{cid}/students", json={"emails": emails}, headers=auth(T1)).json() | |
| assert len(result["added"]) == 25 and result["invalid"] == ["dana@"] | |
| assert client.get(f"/api/classes/{cid}", headers=auth(MGR)).json()["students"] == 25 | |
| def test_a_student_can_be_added_to_a_second_class(world): | |
| client, store = world | |
| a = make(client, name="A", teacher_email=T1).json()["id"] | |
| b = make(client, name="B", teacher_email=T2).json()["id"] | |
| client.post(f"/api/classes/{a}/students", json={"emails": "s@school.test"}, headers=auth(T1)) | |
| again = client.post(f"/api/classes/{b}/students", json={"emails": "s@school.test"}, headers=auth(T2)).json() | |
| assert again["added"] == ["s@school.test"] | |
| assert [c["name"] for c in store.classes_of_student("s@school.test")] == ["A", "B"] | |
| twice = client.post(f"/api/classes/{a}/students", json={"emails": "s@school.test"}, headers=auth(T1)).json() | |
| assert twice["already_here"] == ["s@school.test"] and not twice["added"] | |
| def test_a_teacher_cannot_add_themselves_as_a_student(world): | |
| client, _ = world | |
| cid = make(client, teacher_email=T1).json()["id"] | |
| result = client.post(f"/api/classes/{cid}/students", json={"emails": f"{T1} kid@school.test"}, headers=auth(T1)).json() | |
| assert result["teacher"] == [T1] and result["added"] == ["kid@school.test"] | |
| def test_a_manager_can_use_any_roster_and_another_teacher_cannot(world): | |
| client, _ = world | |
| cid = make(client, teacher_email=T1).json()["id"] | |
| assert client.post(f"/api/classes/{cid}/students", json={"emails": "s@school.test"}, headers=auth(MGR)).status_code == 200 | |
| assert client.post(f"/api/classes/{cid}/students", json={"emails": "s@school.test"}, headers=auth(T2)).status_code == 404 | |
| assert client.delete(f"/api/classes/{cid}/students/s@school.test", headers=auth(T2)).status_code == 404 | |
| assert client.post(f"/api/classes/{cid}/students", json={"emails": "s@school.test"}, headers=auth("kid@school.test")).status_code == 403 | |
| def test_the_teacher_removes_a_student_and_the_progress_stays(world): | |
| client, store = world | |
| cid = make(client, teacher_email=T1).json()["id"] | |
| client.post(f"/api/classes/{cid}/students", json={"emails": "s@school.test"}, headers=auth(T1)) | |
| store.save_progress("u-s", "t_ext", started_at="then") | |
| assert client.delete(f"/api/classes/{cid}/students/S@school.test", headers=auth(T1)).status_code == 204 | |
| assert client.delete(f"/api/classes/{cid}/students/s@school.test", headers=auth(T1)).status_code == 404 | |
| assert store.get_progress("u-s", "t_ext")["started_at"] == "then" | |
| def test_a_teacher_cannot_change_the_class_itself(world): | |
| client, _ = world | |
| cid = make(client, teacher_email=T1).json()["id"] | |
| assert client.put(f"/api/classes/{cid}", json=body("other"), headers=auth(T1)).status_code == 403 | |
| assert client.delete(f"/api/classes/{cid}", headers=auth(T1)).status_code == 403 | |
| assert client.get(f"/api/classes/{cid}", headers=auth(T1)).status_code == 403 | |
| def test_too_many_emails_in_one_go(world): | |
| client, _ = world | |
| cid = make(client, teacher_email=T1).json()["id"] | |
| emails = " ".join(f"s{i}@school.test" for i in range(201)) | |
| assert client.post(f"/api/classes/{cid}/students", json={"emails": emails}, headers=auth(T1)).status_code == 422 | |
| # ---- the teacher's home and the manager's students (spec: Teacher home; Class and student lists) | |
| def test_a_teacher_sees_only_the_classes_they_teach_with_their_students(world): | |
| client, _ = world | |
| make(client, name="A", teacher_email=T1, students="a@school.test") | |
| make(client, name="B", teacher_email=T1, students="b@school.test c@school.test") | |
| make(client, name="C", teacher_email=T2) | |
| mine = client.get("/api/teaching", headers=auth(T1)).json() | |
| assert [c["name"] for c in mine] == ["A", "B"] and [len(c["students"]) for c in mine] == [1, 2] | |
| assert mine[0]["students"][0] == {"email": "a@school.test", "signed_in": False} | |
| assert client.get("/api/teaching", headers=auth("kid@school.test")).status_code == 403 | |
| assert client.get("/api/teaching", headers=auth(MGR)).status_code == 403 # a manager does not teach | |
| def test_a_class_teacher_who_is_in_no_setting_still_gets_the_teacher_home(world): | |
| client, _ = world | |
| make(client, teacher_email="newcomer@school.test", students="a@school.test") | |
| assert [c["name"] for c in client.get("/api/teaching", headers=auth("newcomer@school.test")).json()] == ["讟'3"] | |
| assert client.get("/api/me", headers=auth("newcomer@school.test")).json()["teaching"][0]["name"] == "讟'3" | |
| def test_a_developer_sees_every_class_as_a_teacher(world): | |
| client, _ = world | |
| make(client, name="A", teacher_email=T1) | |
| make(client, name="B", teacher_email=T2) | |
| assert [c["name"] for c in client.get("/api/teaching", headers=auth(DEV)).json()] == ["A", "B"] | |
| def test_the_manager_sees_every_student_with_their_classes(world): | |
| client, _ = world | |
| make(client, name="A", students="x@school.test y@school.test") | |
| make(client, name="B", students="y@school.test") | |
| assert client.get("/api/students", headers=auth(MGR)).json() == [ | |
| {"email": "x@school.test", "classes": ["A"]}, {"email": "y@school.test", "classes": ["A", "B"]}] | |
| assert client.get("/api/students", headers=auth(T1)).status_code == 403 | |
| def test_a_database_that_refuses_a_save_gives_a_clear_503_not_a_name_clash(world): | |
| from mathly2.store import StoreUnavailable | |
| client, store = world | |
| cid = make(client).json()["id"] | |
| def refuse(*args, **kwargs): | |
| raise StoreUnavailable("supabase refused (400): column lessons does not exist") | |
| store.update_class = refuse | |
| answer = client.put(f"/api/classes/{cid}", json=body("讟'3"), headers=auth(MGR)) | |
| assert answer.status_code == 503 and "讛诪讬讙专爪讬讜转" in answer.json()["detail"] and "砖诐" not in answer.json()["detail"] | |