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
62 lines
2.4 KiB
Python
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()
|