"""Tests for the question-bank + custom-paper assembly router (/api/exam/bank, /custom-papers) — mode 3. Same FakeSupabase + dependency-override pattern as test_exam_templates.py; the as-user RLS itself is verified against the live dev DB (.94), not here. """ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient import routers.exam.bank as bank_mod from routers.exam.bank import router from routers.exam.dependencies import ExamContext, get_exam_context TEACHER = "00000000-0000-0000-0000-000000000001" INST_A = "10000000-0000-0000-0000-000000000001" @pytest.fixture(autouse=True) def _stub_projection(monkeypatch): monkeypatch.setattr(bank_mod, "project_template_safe", lambda tid: None) class FakeResult: def __init__(self, data): self.data = data class FakeQuery: def __init__(self, store, table): self.store = store self.table = table self.rows = list(store.get(table, [])) self._op = None self._payload = None def select(self, *_a, **_k): self._op = "select" return self def insert(self, payload): self._op = "insert" self._payload = payload return self def eq(self, key, value): self.rows = [r for r in self.rows if r.get(key) == value] return self def in_(self, key, values): values = set(values) self.rows = [r for r in self.rows if r.get(key) in values] return self def execute(self): backing = self.store.setdefault(self.table, []) if self._op == "insert": payloads = self._payload if isinstance(self._payload, list) else [self._payload] inserted = [] for p in payloads: row = dict(p) row.setdefault("id", f"gen-{self.table}-{len(backing)}") backing.append(row) inserted.append(row) return FakeResult(inserted) return FakeResult(self.rows) class FakeSupabase: def __init__(self, store): self.store = store def table(self, name): return FakeQuery(self.store, name) def make_client(store, user_id=TEACHER, institute_ids=(INST_A,)): app = FastAPI() app.include_router(router, prefix="/api/exam") app.dependency_overrides[get_exam_context] = lambda: ExamContext(user_id, "tok", FakeSupabase(store), list(institute_ids)) return TestClient(app), store def test_bank_lists_leaf_questions_with_facets_excluding_containers(): store = {"exam_questions": [ {"id": "c1", "template_id": "t1", "label": "Q1", "is_container": True, "spec_ref": None, "exam_templates": {"id": "t1", "title": "Physics 1H", "subject": "physics", "exam_code": "AQA-PHYS-8463"}}, {"id": "q1", "template_id": "t1", "label": "01.1", "is_container": False, "max_marks": 3, "spec_ref": "4.2", "exam_templates": {"id": "t1", "title": "Physics 1H", "subject": "physics", "exam_code": "AQA-PHYS-8463"}}, {"id": "q2", "template_id": "t1", "label": "01.2", "is_container": False, "max_marks": 2, "spec_ref": "4.2", "exam_templates": {"id": "t1", "title": "Physics 1H", "subject": "physics", "exam_code": "AQA-PHYS-8463"}}, ]} c, _ = make_client(store) body = c.get("/api/exam/bank").json() labels = {q["label"] for q in body["questions"]} assert labels == {"01.1", "01.2"} # container excluded assert body["facets"]["spec_ref"] == {"4.2": 2} assert body["questions"][0]["paper"]["exam_code"] == "AQA-PHYS-8463" def test_bank_filters_by_spec_ref(): store = {"exam_questions": [ {"id": "q1", "template_id": "t1", "label": "a", "is_container": False, "spec_ref": "4.2", "exam_templates": {"subject": "physics"}}, {"id": "q2", "template_id": "t1", "label": "b", "is_container": False, "spec_ref": "4.5", "exam_templates": {"subject": "physics"}}, ]} c, _ = make_client(store) body = c.get("/api/exam/bank?spec_ref=4.2").json() assert [q["id"] for q in body["questions"]] == ["q1"] def test_custom_paper_copies_selected_questions_in_order(): store = {"exam_questions": [ {"id": "q1", "template_id": "t1", "label": "01.1", "is_container": False, "max_marks": 3, "spec_ref": "4.2", "answer_type": "short"}, {"id": "q2", "template_id": "t2", "label": "05.1", "is_container": False, "max_marks": 6, "spec_ref": "4.5", "answer_type": "written"}, ]} c, store = make_client(store) r = c.post("/api/exam/custom-papers", json={"title": "Electricity mini-test", "subject": "physics", "question_ids": ["q2", "q1"]}) assert r.status_code == 200 body = r.json() assert body["n_questions"] == 2 # a new template row was created, owned + institute-scoped new_tpls = [t for t in store["exam_templates"] if t["id"] == body["id"]] assert new_tpls and new_tpls[0]["teacher_id"] == TEACHER and new_tpls[0]["institute_id"] == INST_A # two COPIES exist under the new template, order preserving the requested [q2, q1], with fresh ids copies = sorted([q for q in store["exam_questions"] if q["template_id"] == body["id"]], key=lambda q: q["order"]) assert [q["label"] for q in copies] == ["05.1", "01.1"] assert all(q["id"] not in ("q1", "q2") for q in copies) # not the originals assert copies[0]["max_marks"] == 6 and copies[0]["spec_ref"] == "4.5" def test_custom_paper_requires_accessible_questions(): c, _ = make_client({"exam_questions": []}) assert c.post("/api/exam/custom-papers", json={"title": "x", "question_ids": ["nope"]}).status_code == 404 assert c.post("/api/exam/custom-papers", json={"title": "x", "question_ids": []}).status_code == 400