pdf-quiz-generator/backend/tests/test_question_figures.py
Daniel e858f4c166
Some checks failed
Tests / backend (push) Failing after 5s
Tests / frontend (push) Successful in 29s
Tests / e2e (push) Failing after 33s
fix: a schema promising an owner where the column now says NULL
Opening any shared deck answered 500 — ResponseValidationError, "Input
should be a valid integer" for user_id. The ownership migration made
those columns nullable and the response models still declared
`user_id: int`, so the first read of a deck after it was a crash rather
than a page. A learner hit it on Cards.

FlashcardDeckResponse, DocumentResponse and QuizResponse now allow None,
with a test that walks the three and fails if any of them promises an
owner again. The grant-input schemas were left alone on purpose: their
user_id names the person a grant is for, and a grant with nobody in it
is not a thing.

Also gone: send_login_code_email, forty-eight lines of email template
for a feature that no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-13 14:17:55 +02:00

93 lines
4.2 KiB
Python

"""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")