Files
penracourses/generic/tests/test_exam_submit_security.py
T

235 lines
7.7 KiB
Python

import json
import pytest
from django.urls import reverse
from django.contrib.auth.models import User
from django.test import Client
from django.utils import timezone
from anatomy.models import Exam as AnatomyExam, AnatomyQuestion, QuestionType, Structure
from generic.models import CidUser, CidUserGroup
@pytest.fixture
def logged_in_user(db):
user = User.objects.create_user(username="user1", password="password", email="user1@test.com")
return user
@pytest.fixture
def logged_in_client(logged_in_user):
client = Client()
client.force_login(logged_in_user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def exam_data(db):
exam = AnatomyExam.objects.create(
name="Anatomy Exam Test",
exam_mode=True,
active=True,
open_access=False,
exam_open_access=True, # allows open access so we don't have to populate cid/user lists for candidate test
)
qtype = QuestionType.objects.create(id=1, text="Test Type")
structure = Structure.objects.create(name="Test Structure")
question = AnatomyQuestion.objects.create(
question_type=qtype,
structure=structure,
image="test.jpg",
description="Test Question description",
)
question.exams.set([exam])
return exam, question
@pytest.mark.django_db
def test_authenticated_user_submit_success(logged_in_client, logged_in_user, exam_data):
exam, question = exam_data
payload = {
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk}",
"start_time": timezone.now().timestamp(),
"answers": json.dumps([
{
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk}",
"qid": question.pk,
"ans": "Normal",
"qidn": "1",
}
])
}
response = logged_in_client.post(reverse("global_exam_answers_submit"), payload)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["question_count"] == 1
@pytest.mark.django_db
def test_authenticated_user_submit_mismatch_other_user(logged_in_client, logged_in_user, exam_data):
exam, question = exam_data
payload = {
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk + 999}", # root cid doesn't match authenticated user
"start_time": timezone.now().timestamp(),
"answers": json.dumps([
{
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk}",
"qid": question.pk,
"ans": "Normal",
"qidn": "1",
}
])
}
response = logged_in_client.post(reverse("global_exam_answers_submit"), payload)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert "user / cid mismatch" in data["error"]
@pytest.mark.django_db
def test_authenticated_user_submit_mismatch_answer_level(logged_in_client, logged_in_user, exam_data):
exam, question = exam_data
payload = {
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk}",
"start_time": timezone.now().timestamp(),
"answers": json.dumps([
{
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk + 999}", # answer level cid doesn't match authenticated user
"qid": question.pk,
"ans": "Normal",
"qidn": "1",
}
])
}
response = logged_in_client.post(reverse("global_exam_answers_submit"), payload)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert "user / uid mismatch" in data["error"]
@pytest.mark.django_db
def test_anonymous_user_submit_user_prefix_fails(anonymous_client, exam_data):
exam, question = exam_data
payload = {
"eid": f"anatomy/{exam.pk}",
"cid": "u-1", # prefix u- is forbidden for anonymous users
"start_time": timezone.now().timestamp(),
"answers": json.dumps([
{
"eid": f"anatomy/{exam.pk}",
"cid": "u-1",
"qid": question.pk,
"ans": "Normal",
"qidn": "1",
}
])
}
response = anonymous_client.post(reverse("global_exam_answers_submit"), payload)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert "authentication required" in data["error"]
@pytest.mark.django_db
def test_candidate_submit_success(anonymous_client, exam_data):
exam, question = exam_data
cid_user = CidUser.objects.create(
cid=8888,
name="Candidate 1",
email="candidate1@test.com",
passcode="candidatepass",
)
# Associate CID user with the exam
exam.exam_open_access = False
group = CidUserGroup.objects.create(name="Test Group")
group.ciduser_set.add(cid_user)
exam.cid_user_groups.add(group)
exam.valid_cid_users.add(cid_user)
payload = {
"eid": f"anatomy/{exam.pk}",
"cid": "8888",
"start_time": timezone.now().timestamp(),
"answers": json.dumps([
{
"eid": f"anatomy/{exam.pk}",
"cid": "8888",
"passcode": "candidatepass",
"qid": question.pk,
"ans": "Normal",
"qidn": "1",
}
])
}
response = anonymous_client.post(reverse("global_exam_answers_submit"), payload)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["question_count"] == 1
@pytest.fixture
def cid_manager_client(db):
from django.contrib.auth.models import Group
user = User.objects.create_user(username="manager1", password="password", email="manager1@test.com")
g, _ = Group.objects.get_or_create(name="cid_user_manager")
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.mark.django_db
def test_import_exam_answers_unauthorized(anonymous_client):
# Anonymous or non-manager users should be redirected or blocked
url = reverse("generic:import_exam_answers")
response = anonymous_client.get(url)
assert response.status_code == 403
@pytest.mark.django_db
def test_import_exam_answers_get(cid_manager_client):
url = reverse("generic:import_exam_answers")
response = cid_manager_client.get(url)
assert response.status_code == 200
assert b"Import Exam Answers" in response.content
@pytest.mark.django_db
def test_import_exam_answers_post_success(cid_manager_client, exam_data, logged_in_user):
exam, question = exam_data
from io import BytesIO
from anatomy.models import UserAnswer as AnatomyUserAnswer
import_data = {
"eid": f"anatomy/{exam.pk}",
"cid": f"u-{logged_in_user.pk}",
"start_time": timezone.now().timestamp(),
"answers": [
{
"qid": question.pk,
"ans": "Imported Test Answer",
"qidn": "1",
}
]
}
file_content = json.dumps(import_data).encode("utf-8")
answers_file = BytesIO(file_content)
answers_file.name = "backup.json"
url = reverse("generic:import_exam_answers")
response = cid_manager_client.post(url, {"answers_file": answers_file}, format="multipart")
assert response.status_code == 302 # redirects to manage_cids on success
# Verify answers were saved
answers = AnatomyUserAnswer.objects.filter(exam=exam, user=logged_in_user)
assert answers.count() == 1
assert answers.first().answer == "Imported Test Answer"