import base64 # pyrefly: ignore [missing-import] from odoo.tests.common import TransactionCase # pyrefly: ignore [missing-import] from odoo.exceptions import AccessError, ValidationError from datetime import date, timedelta # pyrefly: ignore [missing-import] from odoo import fields class TestEducationLMS(TransactionCase): def setUp(self): super(TestEducationLMS, self).setUp() self.env.user.tz = 'UTC' # Groups self.group_faculty = self.env.ref('education_lms.group_education_lms_faculty') self.group_student = self.env.ref('education_lms.group_education_lms_student') self.group_admin = self.env.ref('education_lms.group_education_lms_admin') # Create Users self.user_faculty_a = self.env['res.users'].create({ 'name': 'Faculty A', 'login': 'faculty_a', 'group_ids': [(4, self.group_faculty.id)] }) self.user_faculty_b = self.env['res.users'].create({ 'name': 'Faculty B', 'login': 'faculty_b', 'group_ids': [(4, self.group_faculty.id)] }) self.user_student_a = self.env['res.users'].create({ 'name': 'Student A', 'login': 'student_a', 'group_ids': [(4, self.group_student.id), (4, self.env.ref('education_student.group_education_student_user').id)] }) self.user_student_b = self.env['res.users'].create({ 'name': 'Student B', 'login': 'student_b', 'group_ids': [(4, self.group_student.id), (4, self.env.ref('education_student.group_education_student_user').id)] }) # Academic structure self.campus = self.env['education.campus'].create({'name': 'Campus A', 'code': 'CA'}) self.department = self.env['education.department'].create({ 'name': 'Dept A', 'code': 'DA', 'campus_id': self.campus.id }) self.level = self.env['education.academic.level'].create({'name': 'Level A', 'code': 'LA'}) self.program = self.env['education.program'].create({ 'name': 'Program A', 'code': 'PA', 'department_id': self.department.id, 'level_id': self.level.id }) self.academic_year = self.env['education.academic.year'].create({ 'name': 'Year 2024', 'code': 'Y24', 'start_date': date(2024, 1, 1), 'end_date': date(2024, 12, 31) }) self.term = self.env['education.academic.term'].create({ 'name': 'Term 1', 'code': 'T1', 'academic_year_id': self.academic_year.id, 'start_date': date(2024, 1, 1), 'end_date': date(2024, 6, 30) }) self.batch = self.env['education.batch'].create({ 'name': 'Batch A', 'code': 'BA' }) self.semester = self.env['education.semester'].create({ 'name': 'Sem 1', 'code': 'S1' }) self.section = self.env['education.section'].create({'name': 'Section A', 'code': 'SECA'}) self.subject = self.env['education.subject'].create({ 'name': 'Subject A', 'code': 'SUB_A', 'program_id': self.program.id }) # Create Students self.student_a = self.env['education.student'].create({ 'first_name': 'Student', 'last_name': 'A', 'portal_user_id': self.user_student_a.id, 'campus_id': self.campus.id, 'department_id': self.department.id, 'program_id': self.program.id, 'level_id': self.level.id, 'academic_year_id': self.academic_year.id, 'academic_term_id': self.term.id, 'batch_id': self.batch.id, 'semester_id': self.semester.id, 'section_id': self.section.id, 'admission_type': 'regular' }) self.student_b = self.env['education.student'].create({ 'first_name': 'Student', 'last_name': 'B', 'portal_user_id': self.user_student_b.id, 'campus_id': self.campus.id, 'department_id': self.department.id, 'program_id': self.program.id, 'level_id': self.level.id, 'academic_year_id': self.academic_year.id, 'academic_term_id': self.term.id, 'batch_id': self.batch.id, 'semester_id': self.semester.id, 'section_id': self.section.id, 'admission_type': 'regular' }) # Courses self.course_faculty_a = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course A', 'subject_id': self.subject.id, 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'term_id': self.term.id, 'batch_id': self.batch.id, 'state': 'draft' }) self.course_faculty_b = self.env['education.lms.course'].with_user(self.user_faculty_b).create({ 'name': 'Course B', 'subject_id': self.subject.id, 'faculty_id': self.user_faculty_b.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'state': 'draft' }) self.course_faculty_a.write({'state': 'published'}) self.course_faculty_b.write({'state': 'published'}) # 1. Faculty A creates assignment on Faculty A course -> PASS def test_01_faculty_creates_own_assignment(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'assigned_date': fields.Date.today(), 'due_date': fields.Date.today() + timedelta(days=5), 'maximum_marks': 100 }) self.assertTrue(assignment.id) # 2. Faculty A creates assignment on Faculty B course -> REJECT def test_02_faculty_creates_other_assignment(self): with self.assertRaises(AccessError): self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign B', 'course_id': self.course_faculty_b.id, 'assigned_date': fields.Date.today(), 'due_date': fields.Date.today() + timedelta(days=5) }) # 3. Faculty B edits Faculty A assignment -> REJECT def test_03_faculty_edits_other_assignment(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) with self.assertRaises(AccessError): assignment.with_user(self.user_faculty_b).write({'name': 'Hacked'}) # 4. Faculty B deletes Faculty A assignment -> REJECT def test_04_faculty_deletes_other_assignment(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) with self.assertRaises(AccessError): assignment.with_user(self.user_faculty_b).unlink() # 5. Faculty A publishes assignment under Draft course -> REJECT def test_05_publish_under_draft_course(self): self.course_faculty_a.with_user(self.user_faculty_a).write({'state': 'draft'}) assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) with self.assertRaises(ValidationError): assignment.with_user(self.user_faculty_a).write({'state': 'published'}) # 6. Faculty A publishes assignment under Published course -> PASS def test_06_publish_under_published_course(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.with_user(self.user_faculty_a).write({'state': 'published'}) self.assertEqual(assignment.state, 'published') # 7. due_date before assigned_date -> REJECT def test_07_invalid_dates(self): with self.assertRaises(ValidationError): self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'assigned_date': fields.Date.today(), 'due_date': fields.Date.today() - timedelta(days=1) }) # 8. maximum_marks <= 0 -> REJECT def test_08_invalid_marks(self): with self.assertRaises(ValidationError): self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'maximum_marks': 0 }) # 9. Section from another course selected -> REJECT def test_09_invalid_section(self): section_b = self.env['education.lms.section'].create({ 'name': 'Section B', 'course_id': self.course_faculty_b.id }) with self.assertRaises(ValidationError): self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'section_id': section_b.id, 'due_date': fields.Date.today() + timedelta(days=5) }) # 10. Lesson from another section/course selected -> REJECT def test_10_invalid_lesson(self): section_a = self.env['education.lms.section'].create({ 'name': 'Section A', 'course_id': self.course_faculty_a.id }) section_b = self.env['education.lms.section'].create({ 'name': 'Section B', 'course_id': self.course_faculty_b.id }) lesson_b = self.env['education.lms.lesson'].create({ 'name': 'Lesson B', 'section_id': section_b.id, 'course_id': self.course_faculty_b.id }) with self.assertRaises(ValidationError): self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'section_id': section_a.id, 'lesson_id': lesson_b.id, 'due_date': fields.Date.today() + timedelta(days=5) }) # 11. Student A creates submission for eligible assignment -> PASS def test_11_student_creates_submission(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) self.assertTrue(submission.id) # 12. Student A creates submission for Student B -> REJECT def test_12_student_creates_for_other(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) with self.assertRaises(AccessError): self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_b.id }) # 13. Student A submits assignment for an ineligible course -> REJECT def test_13_student_ineligible_course(self): # Create course for different program program2 = self.env['education.program'].create({ 'name': 'Program 2', 'code': 'P2', 'department_id': self.department.id, 'level_id': self.level.id }) course_ineligible = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course 2', 'subject_id': self.subject.id, 'faculty_id': self.user_faculty_a.id, 'program_id': program2.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'state': 'published' }) assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 2', 'course_id': course_ineligible.id, 'due_date': fields.Date.today() + timedelta(days=5) }) with self.assertRaises(ValidationError): self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) # 14. Student A submits with no text and no attachment -> REJECT def test_14_submit_without_content(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) with self.assertRaises(ValidationError): submission.with_user(self.user_student_a).write({'status': 'submitted'}) # 15. Student A submits before due date -> status Submitted def test_15_submit_before_due_date(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) submission.with_user(self.user_student_a).write({'status': 'submitted', 'text_submission': 'Done'}) self.assertEqual(submission.status, 'submitted') self.assertFalse(submission.is_late) # 16. Student A submits after due date -> status Late def test_16_submit_after_due_date(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'assigned_date': fields.Date.today() - timedelta(days=2), 'due_date': fields.Date.today() - timedelta(days=1) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) submission.with_user(self.user_student_a).write({'status': 'submitted', 'text_submission': 'Done'}) self.assertEqual(submission.status, 'late') self.assertTrue(submission.is_late) # 17. Student A cannot edit another student's submission -> REJECT def test_17_student_edit_other_submission(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) with self.assertRaises(AccessError): submission.with_user(self.user_student_b).write({'text_submission': 'Hacked'}) # 18. Faculty A can view/evaluate Student A submission for Faculty A course -> PASS def test_18_faculty_evaluates_submission(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) submission.with_user(self.user_faculty_a).write({'status': 'evaluated', 'marks': 90}) self.assertEqual(submission.status, 'evaluated') self.assertEqual(submission.marks, 90) self.assertTrue(submission.evaluation_date) # 19. Faculty B cannot view/evaluate that submission -> REJECT def test_19_faculty_b_evaluates_faculty_a_submission(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) with self.assertRaises(AccessError): submission.with_user(self.user_faculty_b).write({'status': 'evaluated', 'marks': 90}) # 20. Faculty evaluates with marks = 0 -> PASS def test_20_faculty_evaluate_zero_marks(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) submission.with_user(self.user_faculty_a).write({'status': 'evaluated', 'marks': 0.0}) self.assertEqual(submission.status, 'evaluated') self.assertEqual(submission.marks, 0.0) # 21. Faculty evaluates with marks > maximum_marks -> REJECT def test_21_faculty_evaluate_exceed_max_marks(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'maximum_marks': 100 }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) with self.assertRaises(ValidationError): submission.with_user(self.user_faculty_a).write({'status': 'evaluated', 'marks': 105}) # 22. Faculty evaluates with negative marks -> REJECT def test_22_faculty_evaluate_negative_marks(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) with self.assertRaises(ValidationError): submission.with_user(self.user_faculty_a).write({'status': 'evaluated', 'marks': -5}) # 23. Empty marks remain unevaluated; never convert empty to 0. def test_23_empty_marks(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) self.assertFalse(submission.marks) # 24. Duplicate submission for same student + assignment -> REJECT def test_24_duplicate_submission(self): assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) # Try to create another one with self.assertRaises(Exception): self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) self.env.flush_all() # 25. Admin can manage all assignments and submissions -> PASS def test_25_admin_access(self): user_admin = self.env['res.users'].create({ 'name': 'Admin User', 'login': 'lms_admin', 'group_ids': [(4, self.group_admin.id)] }) assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign 1', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.with_user(user_admin).write({'title': 'Admin Edited'}) self.assertEqual(assignment.title, 'Admin Edited') submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) submission.with_user(user_admin).write({'marks': 100, 'status': 'evaluated'}) self.assertEqual(submission.marks, 100) # PHASE G TESTS # 26. Draft -> Published def test_26_draft_to_published(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) self.assertEqual(assignment.state, 'draft') assignment.action_publish() self.assertEqual(assignment.state, 'published') # 27. Published -> Closed def test_27_published_to_closed(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() assignment.action_close() self.assertEqual(assignment.state, 'closed') # 28. Closed -> Published rejected def test_28_closed_to_published_rejected(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() assignment.action_close() with self.assertRaises(ValidationError): assignment.write({'state': 'published'}) # 29. Closed -> Draft rejected def test_29_closed_to_draft_rejected(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() assignment.action_close() with self.assertRaises(ValidationError): assignment.write({'state': 'draft'}) # 30. Closed assignment rejects new submission def test_30_closed_assignment_rejects_new_submission(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() assignment.action_close() with self.assertRaises(ValidationError): self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id }) # 31. Faculty evaluation succeeds on own course def test_31_faculty_evaluation_own_course(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) submission.with_user(self.user_faculty_a).write({'marks': 10, 'status': 'evaluated'}) self.assertEqual(submission.status, 'evaluated') # 32. Cross-faculty evaluation rejected def test_32_cross_faculty_evaluation_rejected(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) with self.assertRaises(AccessError): submission.with_user(self.user_faculty_b).write({'marks': 10, 'status': 'evaluated'}) # 33. Student cannot modify evaluation fields def test_33_student_cannot_modify_evaluation(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', }) with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'marks': 10}) # 34. Faculty cannot modify another faculty's evaluation def test_34_faculty_cannot_modify_other_faculty_eval(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) submission.with_user(self.user_faculty_a).write({'marks': 10, 'status': 'evaluated'}) with self.assertRaises(AccessError): submission.with_user(self.user_faculty_b).write({'marks': 15}) # 35. 0 marks accepted def test_35_0_marks_accepted(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) submission.with_user(self.user_faculty_a).write({'marks': 0.0, 'status': 'evaluated'}) self.assertEqual(submission.marks, 0.0) # 36. empty marks rejected for Evaluated status def test_36_empty_marks_rejected(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Phase G', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5) }) assignment.action_publish() submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Done', 'status': 'submitted' }) with self.assertRaises(ValidationError): submission.with_user(self.user_faculty_a).write({'marks': False, 'status': 'evaluated'}) # ========================================== # PHASE H SECURITY TESTS # ========================================== # 37. Faculty A cannot read/modify Faculty B courses/content def test_37_faculty_isolation(self): # Faculty B course course_b = self.env['education.lms.course'].with_user(self.user_faculty_b).create({ 'name': 'Course B', 'faculty_id': self.user_faculty_b.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, }) # Faculty A tries to read -> AccessError with self.assertRaises(AccessError): course_b.with_user(self.user_faculty_a).read(['name']) # Faculty A tries to modify with self.assertRaises(AccessError): course_b.with_user(self.user_faculty_a).write({'name': 'Hacked'}) # Faculty A tries to delete with self.assertRaises(AccessError): course_b.with_user(self.user_faculty_a).unlink() # Faculty A tries to create section under Course B -> AccessError with self.assertRaises(AccessError): self.env['education.lms.section'].with_user(self.user_faculty_a).create({ 'name': 'Sec B', 'course_id': course_b.id }) # 38. Student Isolation (cannot read/modify other student submission) def test_38_student_isolation(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign H', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'state': 'published' }) submission_a = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Student A Work', }) # Student B tries to read with self.assertRaises(AccessError): submission_a.with_user(self.user_student_b).read(['text_submission']) # Student B tries to write with self.assertRaises(AccessError): submission_a.with_user(self.user_student_b).write({'text_submission': 'Hacked'}) # Student B tries to create submission for Student A assignment_new = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign H New', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'state': 'published' }) with self.assertRaises(AccessError): self.env['education.lms.submission'].with_user(self.user_student_b).create({ 'assignment_id': assignment_new.id, 'student_id': self.student_a.id, 'text_submission': 'Student B Work', }) # 39. Course Eligibility Security def test_39_course_eligibility(self): # A. Matching course_a = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course A', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'term_id': self.term.id, 'batch_id': self.batch.id, 'state': 'published' }) # Student A matches program, year, term, batch self.env.invalidate_all() res = course_a.with_user(self.user_student_a).read(['name']) self.assertTrue(len(res) > 0) self.assertTrue(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_a.id)]))) # B. Different Program program_2 = self.env['education.program'].create({ 'name': 'Program 2', 'code': 'P2', 'department_id': self.department.id, 'level_id': self.level.id }) course_b = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course B', 'faculty_id': self.user_faculty_a.id, 'program_id': program_2.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'term_id': self.term.id, 'batch_id': self.batch.id, 'state': 'published' }) with self.assertRaises(AccessError): course_b.with_user(self.user_student_a).read(['name']) self.assertFalse(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_b.id)]))) # C. Different Year year_2 = self.env['education.academic.year'].create({ 'name': 'Year 2025', 'code': 'Y25', 'start_date': date(2025, 1, 1), 'end_date': date(2025, 12, 31) }) course_c = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course C', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': year_2.id, 'subject_id': self.subject.id, 'term_id': self.term.id, 'batch_id': self.batch.id, 'state': 'published' }) with self.assertRaises(AccessError): course_c.with_user(self.user_student_a).read(['name']) self.assertFalse(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_c.id)]))) # D. Different Term term_2 = self.env['education.academic.term'].create({ 'name': 'Term 2', 'code': 'T2', 'academic_year_id': self.academic_year.id, 'start_date': date(2024, 7, 1), 'end_date': date(2024, 12, 31), 'sequence': 2 }) course_d = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course D', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'term_id': term_2.id, 'batch_id': self.batch.id, 'state': 'published' }) with self.assertRaises(AccessError): course_d.with_user(self.user_student_a).read(['name']) self.assertFalse(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_d.id)]))) # E. Course with no term -> Matches course_e = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course E', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'batch_id': self.batch.id, 'state': 'published' }) res = course_e.with_user(self.user_student_a).read(['name']) self.assertTrue(len(res) > 0) self.assertTrue(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_e.id)]))) # F. Different batch batch_2 = self.env['education.batch'].create({'name': 'Batch B', 'code': 'BB'}) course_f = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course F', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'term_id': self.term.id, 'batch_id': batch_2.id, 'state': 'published' }) with self.assertRaises(AccessError): course_f.with_user(self.user_student_a).read(['name']) self.assertFalse(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_f.id)]))) # G. Course with no batch course_g = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course G', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'term_id': self.term.id, 'state': 'published' }) res = course_g.with_user(self.user_student_a).read(['name']) self.assertTrue(len(res) > 0) self.assertTrue(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_g.id)]))) # H. Course with no term AND no batch (should match) course_h = self.env['education.lms.course'].with_user(self.user_faculty_a).create({ 'name': 'Course H', 'faculty_id': self.user_faculty_a.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, 'state': 'published' }) res = course_h.with_user(self.user_student_a).read(['name']) self.assertTrue(len(res) > 0) self.assertTrue(bool(self.env['education.lms.course'].with_user(self.user_student_a).search([('id', '=', course_h.id)]))) # 40. Assignment access and states def test_40_assignment_access(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign Draft', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'state': 'draft' }) self.env.invalidate_all() # Draft assignment -> Student A cannot read with self.assertRaises(AccessError): assignment.with_user(self.user_student_a).read(['title']) self.assertFalse(bool(self.env['education.lms.assignment'].with_user(self.user_student_a).search([('id', '=', assignment.id)]))) # Published -> Student A can read assignment.with_user(self.user_faculty_a).action_publish() self.env.invalidate_all() res = assignment.with_user(self.user_student_a).read(['title']) self.assertTrue(len(res) > 0) self.assertTrue(bool(self.env['education.lms.assignment'].with_user(self.user_student_a).search([('id', '=', assignment.id)]))) # Closed -> Student A can read, but cannot submit assignment.with_user(self.user_faculty_a).action_close() self.env.invalidate_all() res = assignment.with_user(self.user_student_a).read(['title']) self.assertTrue(len(res) > 0) with self.assertRaises(ValidationError): self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Student A Work', }) # 41. Submission edge cases def test_41_submission_security_edge_cases(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign H2', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'state': 'published' }) submission = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'Student A Work', }) # Spoof student_id with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'student_id': self.student_b.id}) # Spoof assignment_id assignment_2 = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign H3', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'state': 'published' }) with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'assignment_id': assignment_2.id}) submission.with_user(self.user_student_a).action_submit() # Change marks with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'marks': 100}) # Change feedback with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'feedback': 'Good job me'}) # Change status with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'status': 'evaluated'}) # Change evaluation metadata with self.assertRaises(AccessError): submission.with_user(self.user_student_a).write({'evaluated_by': self.user_student_a.id}) # Faculty B cannot evaluate Faculty A's submissions with self.assertRaises(AccessError): submission.with_user(self.user_faculty_b).write({'marks': 50}) # Faculty A CAN evaluate submission.with_user(self.user_faculty_a).write({'marks': 50, 'status': 'evaluated'}) self.assertEqual(submission.marks, 50) # 42. Attachment Security Direct Test def test_42_attachment_security(self): self.course_faculty_a.state = 'published' assignment = self.env['education.lms.assignment'].with_user(self.user_faculty_a).create({ 'title': 'Assign H Attachments', 'course_id': self.course_faculty_a.id, 'due_date': fields.Date.today() + timedelta(days=5), 'state': 'published' }) # Student A submission sub_a = self.env['education.lms.submission'].with_user(self.user_student_a).create({ 'assignment_id': assignment.id, 'student_id': self.student_a.id, 'text_submission': 'A', }) attach_a = self.env['ir.attachment'].with_user(self.user_student_a).create({ 'name': 'file_a.txt', 'datas': base64.b64encode(b'student A file'), 'res_model': 'education.lms.submission', 'res_id': sub_a.id, }) sub_b = self.env['education.lms.submission'].with_user(self.user_student_b).create({ 'assignment_id': assignment.id, 'student_id': self.student_b.id, 'text_submission': 'B', }) attach_b = self.env['ir.attachment'].with_user(self.user_student_b).create({ 'name': 'file_b.txt', 'datas': base64.b64encode(b'student B file'), 'res_model': 'education.lms.submission', 'res_id': sub_b.id, }) # Verify A can read attach A binary data res_a = attach_a.with_user(self.user_student_a).read(['raw', 'datas']) self.assertTrue(len(res_a) > 0) # Verify A cannot read attach B binary data with self.assertRaises(AccessError): attach_b.with_user(self.user_student_a).read(['raw', 'datas']) # Verify Faculty A can read both res_fa1 = attach_a.with_user(self.user_faculty_a).read(['raw']) self.assertTrue(len(res_fa1) > 0) res_fa2 = attach_b.with_user(self.user_faculty_a).read(['raw']) self.assertTrue(len(res_fa2) > 0) # Verify Faculty B cannot read either with self.assertRaises(AccessError): attach_a.with_user(self.user_faculty_b).read(['raw']) with self.assertRaises(AccessError): attach_b.with_user(self.user_faculty_b).read(['raw']) # 43. Admin override def test_43_admin_override(self): user_admin = self.env['res.users'].create({ 'name': 'Admin User 2', 'login': 'lms_admin2', 'group_ids': [(4, self.group_admin.id)] }) # Admin can read faculty B's course course_b = self.env['education.lms.course'].with_user(self.user_faculty_b).create({ 'name': 'Course B', 'faculty_id': self.user_faculty_b.id, 'program_id': self.program.id, 'academic_year_id': self.academic_year.id, 'subject_id': self.subject.id, }) res = course_b.with_user(user_admin).read(['name']) self.assertTrue(len(res) > 0) course_b.with_user(user_admin).write({'name': 'Admin changed'}) self.assertEqual(course_b.name, 'Admin changed') # 44. Portal User Mapping def test_44_portal_user_mapping(self): # A user without student_id should not see Student A's courses/submissions in LMS Context user_no_student = self.env['res.users'].create({ 'name': 'No Student', 'login': 'no_student', 'group_ids': [(4, self.group_student.id)] }) self.course_faculty_a.state = 'published' with self.assertRaises(AccessError): self.course_faculty_a.with_user(user_no_student).read(['name'])