pdf-quiz-generator/backend/tests/test_question_detail_access.py
Daniel cc4463d42c
Some checks failed
Tests / backend (push) Failing after 7s
Tests / frontend (push) Successful in 35s
Tests / e2e (push) Failing after 31s
fix: /questions/detail hands out the answer to anybody signed in
Found by walking the permission model as a real learner account rather
than reading the guards. The bank listing has always nulled the answer
side for whoever does not write the question; this route, added for the
full-page editor, returned the row whole behind nothing but
get_current_user. Any signed-in account could ask for
/questions/detail/3869 and be handed the correct option, the
explanation, the per-option reasoning and the key points for a question
it had never sat — 2,924 questions, one URL, no attempt required.

Same rule as the listing now, via the same may_edit_question: the stem
still reads, the answer side comes back null, and explanation figures —
answer-side by definition — are filtered out with it. Nulled rather than
refused, because the stem is legitimately readable.

Three tests: a reader gets the stem and none of the answer, whoever
writes it gets all of it, and an author counts as writing it.

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

62 lines
2.4 KiB
Python

"""The answer side of a question, and who is handed it.
Run: DATABASE_URL=sqlite:///:memory: PYTHONPATH=backend python -m unittest discover -s backend/tests
"""
import unittest
import test_quiz_builder as fixtures
class QuestionDetailAccessTests(unittest.TestCase):
"""Being able to reach a question is not the same as having earned its answer.
The bank listing has always nulled the answer side for anybody who does not
write the question. /questions/detail/{id} did not: anybody signed in could
ask for a question by id and be handed the correct option, the explanation,
the per-option reasoning and the key points for a question they had never
sat. Same rule, both routes.
"""
def setUp(self):
self.bank = fixtures.BuilderTests()
self.bank.setUp()
self.client = self.bank.client
def tearDown(self):
self.bank.tearDown()
def detail(self, question_id=4):
response = self.client.get(f"/questions/detail/{question_id}")
self.assertEqual(response.status_code, 200, response.text)
return response.json()
def test_a_reader_gets_the_stem_and_none_of_the_answer(self):
# Question 4 belongs to the peer; the owner may read it in the bank and
# has no business writing it.
self.bank.user = self.bank.owner
body = self.detail(4)
self.assertEqual(body["question_text"], "Question 4")
self.assertEqual(body["options"], ["yes", "no"])
self.assertEqual(body["image_path"], "q.png")
for field in ("correct_answer", "explanation", "option_explanations",
"key_points", "attending_tip", "explanation_image_path"):
self.assertIsNone(body[field], field)
# And no explanation figure, which is answer-side by definition.
self.assertTrue(all(f["role"] == "stem" for f in body["figures"]), body["figures"])
def test_whoever_writes_it_gets_all_of_it(self):
self.bank.user = self.bank.mod
body = self.detail(4)
self.assertEqual(body["correct_answer"], "yes")
self.assertEqual(body["explanation"], "Full explanation")
self.assertEqual(body["explanation_image_path"], "answer.png")
def test_the_author_of_a_question_writes_it(self):
# Question 3 is the owner's own.
self.bank.user = self.bank.owner
body = self.detail(3)
self.assertEqual(body["correct_answer"], "yes")
if __name__ == "__main__":
unittest.main()