"""What a question says about its own figures — and what it must not say. Run: DATABASE_URL=sqlite:///:memory: PYTHONPATH=backend python -m unittest discover -s backend/tests """ import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") import unittest from app.models.media import MediaAsset from app.models.question_media import QuestionMedia from app.services.question_figures import figure_json class FigureCaptionTests(unittest.TestCase): """A caption beside a stem is read before the question is answered. The library's description of an image is written to catalogue it — "an X-ray of a child's pelvis and hips, showing abnormalities in the right hip joint" is a perfectly good catalogue entry and a complete giveaway under a stem about a limping five-year-old. Every one of the bank's 343 figures was inheriting one; not one had a caption of its own. """ def setUp(self): self.asset = MediaAsset(id=5, path="uploads/hip.png", title="Right SCFE, AP pelvis", caption="An X-ray of a child's pelvis and hips, " "showing abnormalities in the right hip joint.") def test_the_library_description_never_reaches_a_question(self): link = QuestionMedia(id=1, question_id=9, media_id=5, role="stem", label=None, caption=None, position=0) figure = figure_json(link, self.asset) self.assertIsNone(figure["caption"]) self.assertIsNone(figure["label"]) # Nor by another name: the catalogue title is a giveaway of its own, # and nothing renders it. self.assertNotIn("title", figure) self.assertEqual(figure["path"], "uploads/hip.png") def test_the_library_record_rides_only_on_an_explanation_figure(self): """Once the answer is in, the catalogue entry is worth having. The server already withholds explanation figures until answers are revealed, so hanging the library record on those rows — and only those — puts it in front of a reader who has finished the question and nowhere near one who has not. """ self.asset.source = "Teaching file" self.asset.source_url = "https://example.org/x" stem = QuestionMedia(id=3, question_id=9, media_id=5, role="stem", position=0) back = QuestionMedia(id=4, question_id=9, media_id=5, role="explanation", position=0) self.assertIsNone(figure_json(stem, self.asset)["library"]) shelf = figure_json(back, self.asset)["library"] self.assertEqual(shelf["title"], "Right SCFE, AP pelvis") self.assertIn("right hip joint", shelf["caption"]) self.assertEqual(shelf["source"], "Teaching file") def test_what_the_question_itself_says_is_shown(self): link = QuestionMedia(id=2, question_id=9, media_id=5, role="stem", label="Figure 1", caption="AP pelvis at presentation.", position=0) figure = figure_json(link, self.asset) self.assertEqual(figure["caption"], "AP pelvis at presentation.") self.assertEqual(figure["label"], "Figure 1") if __name__ == "__main__": unittest.main() class OwnerlessSerialisationTests(unittest.TestCase): """A schema that promises an owner where the column says NULL is a 500. Every deck, category, document, media library and shared quiz is ownerless now. The response models still declared `user_id: int`, so the first read of a shared deck after the migration answered ResponseValidationError — a crash, not a refusal, on a page any learner opens. """ def test_every_bank_schema_allows_no_owner(self): from app.routers.flashcards import FlashcardDeckResponse from app.schemas.document import DocumentResponse from app.schemas.quiz import QuizResponse for model in (FlashcardDeckResponse, DocumentResponse, QuizResponse): field = model.model_fields.get("user_id") self.assertIsNotNone(field, model.__name__) self.assertTrue( type(None) in getattr(field.annotation, "__args__", ()), f"{model.__name__}.user_id must allow None")