fix: update exam test logic to handle CSRF token extraction and unanswered question counts

This commit is contained in:
Ross
2026-07-06 10:45:41 +01:00
parent 5139346d28
commit 8f7ec905a6
2 changed files with 67 additions and 23 deletions
+33 -7
View File
@@ -140,10 +140,36 @@ def test_exams(db, client):
cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
# Extract csrftoken
csrftoken = cid_take_soup.find(
"input", attrs={"name": "csrfmiddlewaretoken"}
)["value"]
# The question form controls (including CSRF token) are loaded via
# the HTMX fragment. Request that fragment and parse it for the
# form elements instead of relying on the main container page.
if exam_tester.testing_cid_user:
frag_url = reverse(
f"{APP_NAME}:exam_take_fragment",
kwargs={
"pk": exam.pk,
"sk": n,
"cid": exam_tester.current_user.cid,
"passcode": exam_tester.current_user.passcode,
},
)
else:
frag_url = reverse(
f"{APP_NAME}:exam_take_fragment_user",
kwargs={
"pk": exam.pk,
"sk": n,
},
)
frag_res = client.get(frag_url)
assert frag_res.status_code == HTTPStatus.OK
frag_soup = BeautifulSoup(frag_res.content, "html.parser")
# Extract csrftoken from the fragment
csrftoken_el = frag_soup.find("input", attrs={"name": "csrfmiddlewaretoken"})
assert csrftoken_el is not None
csrftoken = csrftoken_el["value"]
# Send answers (all TRUE)
post_json = {
"csrfmiddlewaretoken": csrftoken,
@@ -156,7 +182,7 @@ def test_exams(db, client):
}
res = client.post(question_url, data=post_json)
next_button = cid_take_soup.find("button", attrs={"name": "next"})
next_button = frag_soup.find("button", attrs={"name": "next"})
# Our final question will trigger a bad request (it shouldn't show a next button)
# We currently do still save the form though....
@@ -167,10 +193,10 @@ def test_exams(db, client):
assert next_button
assert res.status_code == HTTPStatus.FOUND
overview_button = cid_take_soup.find("button", attrs={"name": "finish"})
overview_button = frag_soup.find("button", attrs={"name": "finish"})
assert overview_button
previous_button = cid_take_soup.find("button", attrs={"name": "previous"})
previous_button = frag_soup.find("button", attrs={"name": "previous"})
if n > 0:
assert previous_button
+34 -16
View File
@@ -327,14 +327,23 @@ class ExamTester:
).text
)
assert (
len(
# Different apps render unanswered markers differently:
# - `sbas` uses <button class="unanswered"> nodes
# - `physics` uses anchor tiles with a title attribute for unanswered
if self.app_name == "sbas":
count_unanswered = len(
cid_take_overview_soup.find_all(
"button", attrs={"class": "unanswered"}
)
)
== total - answered
)
else:
# Count overview tiles that include the "not answered" title
overview_tiles = cid_take_overview_soup.find_all(
class_="overview-question-btn"
)
count_unanswered = sum(1 for t in overview_tiles if t.has_attr("title"))
assert count_unanswered == total - answered
def check_cid_scores_page(self):
if self.testing_cid_user:
@@ -457,17 +466,20 @@ class ExamTester:
alert = cid_exam_scores_soup.find("div", {"class": "alert"})
assert "Results are not currently published." in str(alert)
# Find any element with the submitted-user-answer class (div or span)
submitted_answers = cid_exam_scores_soup.find_all(
"span", {"class": "submitted-user-answer"}
class_="submitted-user-answer"
)
# Check that we are showing enough questions / answers
match self.app_name:
case "physics":
answer_lis = cid_exam_scores_soup.find_all(
"li", {"class": "question-part"}
)
assert len(answer_lis) == 5 * len(self.generated_questions)
# Physics renders per-part answers inside an ordered list
# within each question block. Count li elements inside those
# lists to get the total number of parts shown.
ols = cid_exam_scores_soup.find_all("ol", class_="mb-0")
answer_li_count = sum(len(ol.find_all("li")) for ol in ols)
assert answer_li_count == 5 * len(self.generated_questions)
assert len(submitted_answers) == 5 * len(self.generated_questions)
for ans in submitted_answers:
assert ans.text.strip() == "True"
@@ -497,10 +509,9 @@ class ExamTester:
)
match self.app_name:
case "physics":
answer_lis = cid_exam_scores_soup.find_all(
"li", {"class": "question-part"}
)
assert len(answer_lis) == 5 * len(self.generated_questions)
ols = cid_exam_scores_soup.find_all("ol", class_="mb-0")
answer_li_count = sum(len(ol.find_all("li")) for ol in ols)
assert answer_li_count == 5 * len(self.generated_questions)
assert len(submitted_answers) == 5 * len(self.generated_questions)
for ans in submitted_answers[:-5]:
@@ -541,9 +552,16 @@ class ExamTester:
) # Check our warning banner is gone
# Check that we are displaying the correct answers (correctly)
correct_answers = cid_exam_scores_soup.find_all(
"span", {"class": "correct-answer"}
)
# Different apps render correct answers differently. For physics the
# template uses a div containing the text "Correct answer: ...".
if self.app_name == "physics":
correct_answers = [
t for t in cid_exam_scores_soup.find_all(string=True) if "Correct answer:" in t
]
else:
correct_answers = cid_exam_scores_soup.find_all(
"span", {"class": "correct-answer"}
)
assert len(correct_answers) == self.answers_per_question * len(
self.generated_questions