"""Exams are selectable data, and the choice scopes the bank. Run: DATABASE_URL=sqlite:///:memory: PYTHONPATH=backend python -m unittest discover -s backend/tests """ import os os.environ["DATABASE_URL"] = "sqlite:///:memory:" import unittest from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool from app.database import Base, get_db from app.models.exam import Exam, QuestionExamLink from app.models.question import Question from app.models.user import User from app.routers import exams, questions from app.utils.auth import get_current_user class ExamTests(unittest.TestCase): def setUp(self): self.engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) Base.metadata.create_all(self.engine) self.db = Session(self.engine) self.user = User(id=1, name="Learner", email="learner@example.test", hashed_password="unused") self.mod = User(id=2, name="Mod", email="mod@example.test", hashed_password="unused", role="moderator") self.admin = User(id=4, name="Admin", email="admin@example.test", hashed_password="unused", role="admin") self.db.add_all([self.user, self.mod, self.admin]) self.db.add_all([ Exam(id=1, slug="pediatrics-boards", name="Pediatrics Boards", sort_order=10), Exam(id=2, slug="usmle-step-2-ck", name="USMLE Step 2 CK", sort_order=20), Exam(id=3, slug="retired", name="Retired exam", sort_order=30, is_active=0), ]) self.db.flush() for qid in (1, 2, 3): self.db.add(Question(id=qid, user_id=1, question_text=f"Question {qid}", question_type="mcq", options=["yes", "no"], correct_answer="yes")) self.db.flush() # 1 → boards, 2 → step 2, 3 → unlinked (unclassified, not excluded) self.db.add_all([QuestionExamLink(question_id=1, exam_id=1), QuestionExamLink(question_id=2, exam_id=2)]) self.db.commit() app = FastAPI() app.include_router(exams.router, prefix="/exams") app.include_router(questions.router, prefix="/questions") app.dependency_overrides[get_db] = lambda: self.db app.dependency_overrides[get_current_user] = lambda: self.user self.client = TestClient(app) def tearDown(self): self.client.close() self.db.close() self.engine.dispose() def bank_ids(self): return {q["id"] for q in self.client.get("/questions/bank").json()["questions"]} def test_lists_active_exams_with_their_question_counts(self): body = self.client.get("/exams/").json() self.assertEqual([(e["name"], e["question_count"]) for e in body["exams"]], [("Pediatrics Boards", 1), ("USMLE Step 2 CK", 1)]) self.assertNotIn("Retired exam", [e["name"] for e in body["exams"]]) self.assertIsNone(body["active_exam_id"]) def test_the_choice_is_stored_on_the_user(self): response = self.client.put("/exams/active", json={"exam_id": 2}) self.assertEqual(response.status_code, 200, response.text) self.assertEqual(self.db.get(User, 1).active_exam_id, 2) self.assertEqual(self.client.get("/exams/").json()["active_exam_id"], 2) # And can be cleared again. self.client.put("/exams/active", json={"exam_id": None}) self.assertIsNone(self.db.get(User, 1).active_exam_id) def test_an_unknown_or_inactive_exam_is_refused(self): self.assertEqual(self.client.put("/exams/active", json={"exam_id": 999}).status_code, 404) self.assertEqual(self.client.put("/exams/active", json={"exam_id": 3}).status_code, 404) def test_the_active_exam_scopes_the_bank(self): self.assertEqual(self.bank_ids(), {1, 2, 3}) # no selection yet: everything self.user.active_exam_id = 1 self.db.commit() # Question 2 belongs to another exam; 3 is unlinked so stays visible. self.assertEqual(self.bank_ids(), {1, 3}) self.user.active_exam_id = 2 self.db.commit() self.assertEqual(self.bank_ids(), {2, 3}) def test_creating_an_exam_is_an_administrator_s(self): # An objective appears in everyone's picker and scopes the whole bank, # so it is site configuration rather than content — and it lives with # the other site switches, which a moderator cannot reach either. body = {"name": "USMLE Step 1", "slug": "usmle-step-1"} self.assertEqual(self.client.post("/exams/", json=body).status_code, 403) self.user = self.mod self.assertEqual(self.client.post("/exams/", json=body).status_code, 403) self.user = self.admin self.assertEqual(self.client.post("/exams/", json=body).status_code, 201) self.assertEqual(self.client.post("/exams/", json=body).status_code, 409) if __name__ == "__main__": unittest.main()