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
93 lines
4.2 KiB
Python
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")
|