import json import sys import unittest from unittest.mock import Mock, patch import test_quiz_builder as fixtures from app.models.attempt import QuizAttempt from app.models.quiz import Quiz from app.routers import study_tools class StudyToolTests(unittest.TestCase): def setUp(self): self.bank = fixtures.BuilderTests() self.bank.setUp() self.client = self.bank.client self.client.app.include_router(study_tools.router, prefix='/study-tools') def tearDown(self): self.bank.tearDown() def start(self, quiz_id, mode): response = self.client.post(f'/attempts/start?quiz_id={quiz_id}&mode={mode}&fresh=true') self.assertEqual(response.status_code, 200, response.text) return response.json()['id'] def test_attempt_mode_controls_answers_and_stats_not_query_flags(self): quiz_id = self.bank.generate(is_shared=True, category_ids=[1], mode='learning').json()['id'] initial = self.client.get(f'/quizzes/{quiz_id}').json() self.assertNotIn('correct_answer', initial['questions'][0]) self.assertEqual(self.client.get(f'/quizzes/{quiz_id}?study=true').status_code, 403) exam = self.start(quiz_id, 'exam') data = self.client.get(f'/quizzes/{quiz_id}?attempt_id={exam}').json() self.assertEqual(data['attempt_mode'], 'exam') self.assertNotIn('correct_answer', data['questions'][0]) self.assertEqual(self.client.get(f'/quizzes/{quiz_id}?attempt_id={exam}&study=true').status_code, 403) stats_url = f'/study-tools/attempts/{exam}/questions/1/responses' self.assertEqual(self.client.get(stats_url).status_code, 403) redis = Mock() with patch.dict(sys.modules, {'redis': redis}): self.client.post('/attempts/progress', json={'quiz_id': quiz_id, 'attempt_id': exam, 'answers': {}, 'current_idx': 0, 'mode': 'study'}) stored = json.loads(redis.from_url.return_value.setex.call_args_list[-1].args[2]) self.assertEqual(stored['mode'], 'exam') study = self.start(quiz_id, 'study') data = self.client.get(f'/quizzes/{quiz_id}?attempt_id={study}').json() self.assertEqual(data['attempt_mode'], 'study') self.assertEqual(data['questions'][0]['correct_answer'], 'yes') self.assertTrue(data['questions'][0]['category_breadcrumbs']) self.assertEqual(self.client.get('/quizzes/1', params={'attempt_id': study}).status_code, 404) self.bank.user = self.bank.peer self.assertEqual(self.client.get(f'/quizzes/{quiz_id}?attempt_id={study}').status_code, 404) self.assertEqual(self.client.get(f'/study-tools/attempts/{study}/questions/1/responses').status_code, 404) def test_real_response_counts_exclude_skips_expiry_course_and_private_attempts(self): quiz_id = self.bank.generate(is_shared=True, category_ids=[1]).json()['id'] study = self.start(quiz_id, 'study') self.bank.answer(1, True) self.bank.answer(1, True, day=1) self.bank.answer(1, False, day=2) self.bank.answer(1, True, expired=1) self.bank.answer(1, True, quiz_id=2) self.bank.answer(1, True, completed=False) other_private = Quiz(title='Private peer test', user_id=2, is_shared=0, is_published=0) self.bank.db.add(other_private) self.bank.db.commit() self.bank.answer(1, True, quiz_id=other_private.id) response = self.client.get(f'/study-tools/attempts/{study}/questions/1/responses') self.assertEqual(response.status_code, 200, response.text) self.assertEqual(response.json()['sample_size'], 3) self.assertEqual([row['count'] for row in response.json()['options']], [2, 1]) self.assertEqual([row['percentage'] for row in response.json()['options']], [66.7, 33.3]) attempt = self.bank.db.get(QuizAttempt, study) attempt.selected_question_ids = [2] self.bank.db.commit() self.assertEqual(self.client.get(f'/study-tools/attempts/{study}/questions/1/responses').status_code, 404) empty = self.client.get(f'/study-tools/attempts/{study}/questions/2/responses').json() self.assertEqual(empty['sample_size'], 0) self.assertTrue(all(row['percentage'] == 0 for row in empty['options'])) def test_nullable_review_setting_serializes_and_course_stays_exam_only(self): self.bank.db.query(Quiz).filter(Quiz.id == 1).update({'allow_review': None}) self.bank.db.query(Quiz).filter(Quiz.id == 2).update({'allow_review': None, 'mode': 'learning', 'questions_count': 1}) self.bank.db.add(fixtures.CourseEnrollment(course_id=1, user_id=2)) self.bank.db.commit() self.assertEqual(self.client.get('/quizzes/').status_code, 200) response = self.client.get('/quizzes/1') self.assertEqual(response.status_code, 200, response.text) self.assertIsNone(response.json()['allow_review']) self.bank.user = self.bank.peer aid = self.start(2, 'study') self.assertEqual(self.bank.db.get(QuizAttempt, aid).mode, 'exam') response = self.client.get(f'/quizzes/2?attempt_id={aid}') self.assertEqual(response.status_code, 200, response.text) self.assertIsNone(response.json()['allow_review']) self.assertNotIn('correct_answer', response.json()['questions'][0]) self.assertEqual(self.client.get(f'/quizzes/2?attempt_id={aid}&study=true').status_code, 403) def test_progress_outage_returns_failure_not_empty_or_saved_success(self): quiz_id = self.bank.generate(is_shared=True, category_ids=[1]).json()['id'] aid = self.start(quiz_id, 'study') redis = Mock() redis.from_url.return_value.get.side_effect = ConnectionError('Synthetic cache outage') redis.from_url.return_value.setex.side_effect = ConnectionError('Synthetic cache outage') with patch.dict(sys.modules, {'redis': redis}): self.assertEqual(self.client.get(f'/attempts/progress?quiz_id={quiz_id}').status_code, 503) response = self.client.post('/attempts/progress', json={'quiz_id': quiz_id, 'attempt_id': aid, 'answers': {'1': 'yes'}, 'current_idx': 0, 'mode': 'study'}) self.assertEqual(response.status_code, 503, response.text) self.assertIsNone(self.bank.db.get(QuizAttempt, aid).completed_at) malformed = self.client.post('/attempts/progress', json={'quiz_id': quiz_id, 'attempt_id': aid, 'answers': {'1': {'unexpected': 'object'}}, 'current_idx': 0, 'mode': 'study'}) self.assertEqual(malformed.status_code, 422) def test_lab_reference_permissions_validation_and_publication(self): payload = dict(name='Example test', group='Blood', reference_range='Example interval', units='example units', age_group='Defined study population', specimen='Serum', source='Educator-supplied source', source_url='https://example.test/reference') self.assertEqual(self.client.post('/study-tools/lab-values', json=payload).status_code, 403) self.assertEqual(self.client.get('/study-tools/lab-values?include_drafts=true').status_code, 403) self.bank.user = self.bank.mod response = self.client.post('/study-tools/lab-values', json=payload) self.assertEqual(response.status_code, 201, response.text) entry_id = response.json()['id'] self.assertFalse(response.json()['is_published']) self.assertEqual(self.client.get('/study-tools/lab-values').json(), []) self.assertEqual(len(self.client.get('/study-tools/lab-values?include_drafts=true').json()), 1) for bad in ({'source': ' '}, {'source_url': 'javascript:alert(1)'}, {'source_url': 'https://example.com/' + 'a' * 2000}, {'units': ''}): self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}', json={**payload, **bad}).status_code, 422) self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}', json={**payload, 'is_published': True}).status_code, 200) self.bank.user = self.bank.peer rows = self.client.get('/study-tools/lab-values').json() self.assertEqual(rows[0]['source'], payload['source']) self.assertEqual(rows[0]['age_group'], payload['age_group']) self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}', json=payload).status_code, 403) self.assertEqual(self.client.delete(f'/study-tools/lab-values/{entry_id}').status_code, 403) self.bank.user = self.bank.mod self.assertEqual(self.client.delete(f'/study-tools/lab-values/{entry_id}').status_code, 204) self.assertEqual(self.client.get('/study-tools/lab-values').json(), []) if __name__ == '__main__': unittest.main()