class ProgramSendTests(SiteMixin, TestCase): USERNAME = "******" def setUp(self): super().setUp() self.user = UserFactory(username=self.USERNAME) self.client.login(username=self.user.username, password=USER_PASSWORD) self.program = ProgramFactory(site=self.site) self.pathway = PathwayFactory(site=self.site, programs=[self.program]) self.pc = ProgramCertificateFactory(site=self.site, program_uuid=self.program.uuid) self.user_credential = UserCredentialFactory(username=self.USERNAME, credential=self.pc) self.data = {"username": self.USERNAME, "pathway_id": self.pathway.id} self.url = reverse("records:send_program", kwargs={"uuid": self.program.uuid.hex}) mail.outbox = [] def post(self): jdata = json.dumps(self.data).encode("utf-8") return self.client.post(self.url, data=jdata, content_type=JSON_CONTENT_TYPE) def test_login_required(self): """Verify no access without a login""" self.client.logout() response = self.post() self.assertEqual(response.status_code, 302) # redirect to a login page self.assertTrue(response.url.startswith("/login/?next=")) def test_creates_cert_record(self): """ Verify that the view creates a ProgramCertRecord as needed. """ with self.assertRaises(ProgramCertRecord.DoesNotExist): ProgramCertRecord.objects.get(user=self.user, program=self.program) response = self.post() self.assertEqual(response.status_code, 200) ProgramCertRecord.objects.get(user=self.user, program=self.program) def test_different_user(self): """ Verify that the view rejects a User attempting to send a program """ diff_username = "******" UserFactory(username=diff_username) self.data["username"] = diff_username response = self.post() self.assertEqual(response.status_code, 403) @patch("credentials.apps.records.views.ace") def test_from_address_set(self, mock_ace): """ Verify that the email uses the proper from address """ response = self.post() self.assertEqual(response.status_code, 200) self.assertEqual( mock_ace.send.call_args[0][0].options["from_address"], self.site_configuration.partner_from_address ) @patch("credentials.apps.records.views.ace") def test_no_full_name(self, mock_ace): """ Verify that the email uses the username as a backup for the full name. """ self.user.full_name = "" self.user.first_name = "" self.user.last_name = "" self.user.save() response = self.post() self.assertEqual(response.status_code, 200) self.assertEqual(mock_ace.send.call_args[0][0].context["user_full_name"], self.user.username) @patch("credentials.apps.records.views.ace") def test_from_address_unset(self, mock_ace): """ Verify that the email uses the proper default from address """ self.site_configuration.partner_from_address = None self.site_configuration.save() response = self.post() self.assertEqual(response.status_code, 200) self.assertEqual(mock_ace.send.call_args[0][0].options["from_address"], "no-reply@" + self.site.domain) def test_email_content_complete(self): """Verify an email is actually sent""" response = self.post() self.assertEqual(response.status_code, 200) public_record = ProgramCertRecord.objects.get(user=self.user, program=self.program) record_path = reverse("records:public_programs", kwargs={"uuid": public_record.uuid.hex}) record_link = "http://" + self.site.domain + record_path csv_link = urllib.parse.urljoin(record_link, "csv") # Check output and make sure it seems correct self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] message = str(email.message()) self.assertIn(self.program.title + " Credit Request for", email.subject) self.assertIn( self.user.get_full_name() + " would like to apply for credit in the " + self.pathway.name, message ) self.assertIn("has sent their completed program record for", message) self.assertIn('<a href="' + record_link + '">View Program Record</a>', message) self.assertIn('<a href="' + csv_link + '">Download Record (CSV)</a>', message) self.assertEqual(self.site_configuration.partner_from_address, email.from_email) self.assertEqual(self.user.email, email.reply_to[0]) self.assertListEqual([self.pathway.email], email.to) def test_email_content_incomplete(self): """Verify an email is actually sent""" self.user_credential.delete() response = self.post() self.assertEqual(response.status_code, 200) # Check output and make sure it seems correct self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertIn("has sent their partially completed program record for", str(email.message())) def prevent_sending_second_email(self): """ Verify that an email can't be sent twice """ UserCreditPathwayFactory(pathway=self.pathway, user=self.user) response = self.post() self.assertEqual(response.status_code, 400) def test_resend_email(self): """ Verify that a manually updated email status can be resent """ UserCreditPathwayFactory(pathway=self.pathway, user=self.user, status="") response = self.post() self.assertEqual(response.status_code, 200) user_credit_pathway = UserCreditPathway.objects.get(user=self.user, pathway=self.pathway) self.assertEqual(user_credit_pathway.status, UserCreditPathwayStatus.SENT)
class ProgramRecordViewTests(SiteMixin, TestCase): MOCK_USER_DATA = { "username": "******", "name": "Test User", "email": "*****@*****.**", } def setUp(self): super().setUp() dump_random_state() self.user = UserFactory(username=self.MOCK_USER_DATA["username"]) self.client.login(username=self.user.username, password=USER_PASSWORD) self.course = CourseFactory(site=self.site) self.course_runs = CourseRunFactory.create_batch(3, course=self.course) self.user_grade_low = UserGradeFactory( username=self.MOCK_USER_DATA["username"], course_run=self.course_runs[0], letter_grade="A", percent_grade=0.70, ) self.user_grade_high = UserGradeFactory( username=self.MOCK_USER_DATA["username"], course_run=self.course_runs[1], letter_grade="C", percent_grade=1.00, ) self.user_grade_revoked_cert = UserGradeFactory( username=self.MOCK_USER_DATA["username"], course_run=self.course_runs[2], letter_grade="B", percent_grade=0.80, ) self.course_certs = [ CourseCertificateFactory(course_id=course_run.key, site=self.site) for course_run in self.course_runs ] self.credential_content_type = ContentType.objects.get(app_label="credentials", model="coursecertificate") self.program_cert = ProgramCertificateFactory(site=self.site) self.program_content_type = ContentType.objects.get(app_label="credentials", model="programcertificate") self.user_credentials = [ UserCredentialFactory( username=self.MOCK_USER_DATA["username"], credential_content_type=self.credential_content_type, credential=course_cert, ) for course_cert in self.course_certs ] self.user_credentials[2].status = UserCredential.REVOKED self.user_credentials[2].save() self.org_names = ["CCC", "AAA", "BBB"] self.orgs = [OrganizationFactory(name=name, site=self.site) for name in self.org_names] self.program = ProgramFactory( course_runs=self.course_runs, authoring_organizations=self.orgs, site=self.site, uuid=self.program_cert.program_uuid, ) self.pcr = ProgramCertRecordFactory(program=self.program, user=self.user) self.pathway = PathwayFactory(site=self.site) self.pathway.programs.set([self.program]) def _render_program_record(self, record_data=None, status_code=200): """ Helper method to mock rendering a user certificate.""" if record_data is None: record_data = {} with patch("credentials.apps.records.views.ProgramRecordView._get_record") as get_record: get_record.return_value = record_data response = self.client.get(reverse("records:private_programs", kwargs={"uuid": uuid.uuid4().hex})) self.assertEqual(response.status_code, status_code) return response def assert_matching_template_origin(self, actual, expected_template_name): expected = select_template([expected_template_name]) self.assertEqual(actual.origin, expected.origin) def test_no_anonymous_access_private(self): """ Verify that the private view rejects non-logged-in users. """ self.client.logout() response = self._render_program_record(status_code=302) self.assertRegex(response.url, "^/login/.*") def test_anonymous_access_public(self): """ Verify that the public view does not reject non-logged-in users""" self.client.logout() response = self.client.get(reverse("records:public_programs", kwargs={"uuid": self.pcr.uuid.hex})) self.assertContains(response, "Record") @ddt.data(True, False) def test_access_to_empty_record(self, is_superuser): """ Verify that the an empty record rejects non-superusers. """ # Make sure no credentials exist self.user.is_superuser = is_superuser self.user.save() # Get rid of all credentials for credential in self.user_credentials: credential.status = UserCredential.REVOKED credential.save() expected_code = 200 if is_superuser else 404 response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) self.assertEqual(response.status_code, expected_code) # Confirm it is indeed reported as empty if is_superuser: program_data = json.loads(response.context_data["record"])["program"] self.assertTrue(program_data["empty"]) def test_normal_access(self): """ Verify that the view works in default case. """ response = self._render_program_record() response_context_data = response.context_data self.assertContains(response, "Record") actual_child_templates = response_context_data["child_templates"] self.assert_matching_template_origin(actual_child_templates["footer"], "_footer.html") self.assert_matching_template_origin(actual_child_templates["header"], "_header.html") self.assert_matching_template_origin(actual_child_templates["masquerade"], "_masquerade.html") def test_public_access(self): """ Verify that the public view instructs front end to be public """ response = self.client.get(reverse("records:public_programs", kwargs={"uuid": self.pcr.uuid.hex})) is_public = response.context_data["is_public"] self.assertTrue(is_public) def test_private_access(self): """ Verify that the private view instructs front end to be private """ response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) is_public = response.context_data["is_public"] self.assertFalse(is_public) def test_public_private_data(self): """ Verify that the public and private views return the same record data """ response = self.client.get(reverse("records:public_programs", kwargs={"uuid": self.pcr.uuid.hex})) public_data = json.loads(response.context_data["record"]) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) private_data = json.loads(response.context_data["record"]) self.assertEqual(private_data, public_data) def test_highest_grades(self): """Verify that the view only shows the highest *percentage* grade Also verified that the attempts are counted correctly, even with revoked certs """ response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) grades = json.loads(response.context_data["record"])["grades"] self.assertEqual(len(grades), 1) grade = grades[0] expected_grade = { "name": self.course_runs[1].title, "school": "", "attempts": 3, "course_id": self.course_runs[1].key, "issue_date": self.user_credentials[1].created.isoformat(), "percent_grade": self.user_grade_high.percent_grade, "letter_grade": self.user_grade_high.letter_grade, } self.assertEqual(grade, expected_grade) def test_visible_date_as_issue_date(self): """ Verify that we show visible_date when available """ UserCredentialAttributeFactory( user_credential=self.user_credentials[1], name="visible_date", value="2017-07-31T09:32:46Z" ) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) grades = json.loads(response.context_data["record"])["grades"] self.assertEqual(len(grades), 1) self.assertEqual(grades[0]["issue_date"], "2017-07-31T09:32:46+00:00") def test_future_visible_date_not_shown(self): """ Verify that we don't show certificates with a visible_date in the future """ UserCredentialAttributeFactory( user_credential=self.user_credentials[1], name="visible_date", value=datetime.datetime.max.strftime("%Y-%m-%dT%H:%M:%SZ"), ) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) grades = json.loads(response.context_data["record"])["grades"] self.assertEqual(len(grades), 1) self.assertEqual(grades[0]["course_id"], self.course_runs[0].key) # 0 instead of 1 now that 1 is in future self.assertEqual(grades[0]["issue_date"], self.user_credentials[0].created.isoformat()) @ddt.data( ("9999-01-01T01:01:01Z", False), ("1970-01-01T01:01:01Z", True), (None, True), ) @ddt.unpack def test_program_visible_date(self, date, completed): """ Test that the program's visible_date is considered """ program_credential = UserCredentialFactory( username=self.MOCK_USER_DATA["username"], credential_content_type=self.program_content_type, credential=self.program_cert, ) if date: UserCredentialAttributeFactory( user_credential=program_credential, name="visible_date", value=date, ) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) self.assertEqual(json.loads(response.context_data["record"])["program"]["completed"], completed) def test_organization_order(self): """ Test that the organizations are returned in the order they were added """ self.course.owners.set(self.orgs) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) program_data = json.loads(response.context_data["record"])["program"] grade = json.loads(response.context_data["record"])["grades"][0] self.assertEqual(program_data["school"], ", ".join(self.org_names)) self.assertEqual(grade["school"], ", ".join(self.org_names)) def test_course_run_order(self): """ Test that the course_runs are returned in the program order """ new_course_run = CourseRunFactory() self.program.course_runs.add(new_course_run) UserGradeFactory( username=self.MOCK_USER_DATA["username"], course_run=new_course_run, letter_grade="C", percent_grade=0.70 ) new_course_cert = CourseCertificateFactory(course_id=new_course_run.key, site=self.site) UserCredentialFactory( username=self.MOCK_USER_DATA["username"], credential_content_type=self.credential_content_type, credential=new_course_cert, ) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) grades = json.loads(response.context_data["record"])["grades"] expected_course_run_keys = [course_run.key for course_run in [self.course_runs[1], new_course_run]] actual_course_run_keys = [grade["course_id"] for grade in grades] self.assertEqual(expected_course_run_keys, actual_course_run_keys) def test_course_run_no_credential(self): """ Adds a course run with no credential and tests that it does appear in the results """ new_course_run = CourseRunFactory() self.program.course_runs.add(new_course_run) UserGradeFactory( username=self.MOCK_USER_DATA["username"], course_run=new_course_run, letter_grade="F", percent_grade=0.05 ) CourseCertificateFactory(course_id=new_course_run.key, site=self.site) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) grades = json.loads(response.context_data["record"])["grades"] self.assertEqual(len(grades), 2) self.assertEqual(new_course_run.course.title, grades[1]["name"]) def test_multiple_attempts_no_cert(self): """Adds a course with two failed course_run attempts (no cert) and verifies that the course only shows up once""" # Only superusers can view an empty program (we could add a real cert too here, but this is a more direct test) self.user.is_superuser = True self.user.save() new_course = CourseFactory(site=self.site) new_course_runs = CourseRunFactory.create_batch(2, course=new_course) _ = [ UserGradeFactory( username=self.MOCK_USER_DATA["username"], course_run=course_run, letter_grade="F", percent_grade=0.20 ) for course_run in new_course_runs ] self.program.course_runs.set(new_course_runs) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) grades = json.loads(response.context_data["record"])["grades"] self.assertEqual(len(grades), 1) self.assertEqual(new_course.title, grades[0]["name"]) def test_learner_data(self): """ Test that the learner data is returned successfully """ response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) learner_data = json.loads(response.context_data["record"])["learner"] expected = {"full_name": self.user.get_full_name(), "username": str(self.user), "email": self.user.email} self.assertEqual(learner_data, expected) def test_program_data(self): """ Test that the program data is returned successfully """ response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) program_data = json.loads(response.context_data["record"])["program"] expected = { "name": self.program.title, "type": slugify(self.program.type), "type_name": self.program.type, "completed": False, "empty": False, "last_updated": UserCredential.objects.last().created.isoformat(), "school": ", ".join(self.org_names), } self.assertEqual(program_data, expected) def test_pathway_data(self): """ Test that the pathway data is returned successfully """ response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) pathway_data = json.loads(response.context_data["record"])["pathways"] expected = [ { "name": self.pathway.name, "id": self.pathway.id, "status": "", "is_active": True, "pathway_type": PathwayType.CREDIT.value, } ] self.assertEqual(pathway_data, expected) def test_pathway_no_email(self): """ Test that a pathway without an email is inactive """ self.pathway.email = "" self.pathway.save() response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) pathway_data = json.loads(response.context_data["record"])["pathways"] expected = [ { "name": self.pathway.name, "id": self.pathway.id, "status": "", "is_active": False, "pathway_type": PathwayType.CREDIT.value, } ] self.assertEqual(pathway_data, expected) def test_sent_pathway_status(self): """ Test that a user credit pathway pathway that has already been sent includes a pathway """ UserCreditPathwayFactory(pathway=self.pathway, user=self.user) response = self.client.get(reverse("records:private_programs", kwargs={"uuid": self.program.uuid.hex})) pathway_data = json.loads(response.context_data["record"])["pathways"] expected = [ { "name": self.pathway.name, "id": self.pathway.id, "status": "sent", "is_active": True, "pathway_type": PathwayType.CREDIT.value, } ] self.assertEqual(pathway_data, expected) def test_xss(self): """ Verify that the view protects against xss in translations. """ response = self._render_program_record( { "name": "<xss>", "program": { "name": "<xss>", "school": "XSS School", }, "uuid": "uuid", } ) # Test that the data is parsed from an escaped string self.assertContains( response, "JSON.parse('{\\u0022name\\u0022: \\u0022\\u003Cxss\\u003E\\u0022, " + "\\u0022program\\u0022: {\\u0022name\\u0022: \\u0022\\u003Cxss\\u003E\\u0022, " + "\\u0022school\\u0022: \\u0022XSS School\\u0022}, \\u0022uuid\\u0022: " + "\\u0022uuid\\u0022}')", ) self.assertNotContains(response, "<xss>")
class ProgramRecordViewTests(SiteMixin, TestCase): MOCK_USER_DATA = {'username': '******', 'name': 'Test User', 'email': '*****@*****.**', } def setUp(self): super().setUp() dump_random_state() self.user = UserFactory(username=self.MOCK_USER_DATA['username']) self.client.login(username=self.user.username, password=USER_PASSWORD) self.course = CourseFactory(site=self.site) self.course_runs = CourseRunFactory.create_batch(3, course=self.course) self.user_grade_low = UserGradeFactory(username=self.MOCK_USER_DATA['username'], course_run=self.course_runs[0], letter_grade='A', percent_grade=0.70) self.user_grade_high = UserGradeFactory(username=self.MOCK_USER_DATA['username'], course_run=self.course_runs[1], letter_grade='C', percent_grade=1.00) self.user_grade_revoked_cert = UserGradeFactory(username=self.MOCK_USER_DATA['username'], course_run=self.course_runs[2], letter_grade='B', percent_grade=.80) self.course_certs = [CourseCertificateFactory(course_id=course_run.key, site=self.site) for course_run in self.course_runs] self.credential_content_type = ContentType.objects.get(app_label='credentials', model='coursecertificate') self.program_cert = ProgramCertificateFactory(site=self.site) self.program_content_type = ContentType.objects.get(app_label='credentials', model='programcertificate') self.user_credentials = [UserCredentialFactory(username=self.MOCK_USER_DATA['username'], credential_content_type=self.credential_content_type, credential=course_cert) for course_cert in self.course_certs] self.user_credentials[2].status = UserCredential.REVOKED self.user_credentials[2].save() self.org_names = ['CCC', 'AAA', 'BBB'] self.orgs = [OrganizationFactory(name=name, site=self.site) for name in self.org_names] self.program = ProgramFactory(course_runs=self.course_runs, authoring_organizations=self.orgs, site=self.site, uuid=self.program_cert.program_uuid) self.pcr = ProgramCertRecordFactory(program=self.program, user=self.user) self.pathway = PathwayFactory(site=self.site) self.pathway.programs = [self.program] def _render_program_record(self, record_data=None, status_code=200): """ Helper method to mock rendering a user certificate.""" if record_data is None: record_data = {} with patch('credentials.apps.records.views.ProgramRecordView._get_record') as get_record: get_record.return_value = record_data response = self.client.get(reverse('records:private_programs', kwargs={'uuid': uuid.uuid4().hex})) self.assertEqual(response.status_code, status_code) return response def assert_matching_template_origin(self, actual, expected_template_name): expected = select_template([expected_template_name]) self.assertEqual(actual.origin, expected.origin) def test_no_anonymous_access_private(self): """ Verify that the private view rejects non-logged-in users. """ self.client.logout() response = self._render_program_record(status_code=302) self.assertRegex(response.url, '^/login/.*') # pylint: disable=deprecated-method def test_anonymous_access_public(self): """ Verify that the public view does not reject non-logged-in users""" self.client.logout() response = self.client.get(reverse('records:public_programs', kwargs={'uuid': self.pcr.uuid.hex})) self.assertContains(response, 'Record') @ddt.data(True, False) def test_access_to_empty_record(self, is_superuser): """ Verify that the an empty record rejects non-superusers. """ # Make sure no credentials exist self.user.is_superuser = is_superuser self.user.save() # Get rid of all credentials for credential in self.user_credentials: credential.status = UserCredential.REVOKED credential.save() expected_code = 200 if is_superuser else 404 response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) self.assertEqual(response.status_code, expected_code) # Confirm it is indeed reported as empty if is_superuser: program_data = json.loads(response.context_data['record'])['program'] self.assertTrue(program_data['empty']) def test_normal_access(self): """ Verify that the view works in default case. """ response = self._render_program_record() response_context_data = response.context_data self.assertContains(response, 'Record') actual_child_templates = response_context_data['child_templates'] self.assert_matching_template_origin(actual_child_templates['footer'], '_footer.html') self.assert_matching_template_origin(actual_child_templates['header'], '_header.html') self.assert_matching_template_origin(actual_child_templates['masquerade'], '_masquerade.html') def test_public_access(self): """ Verify that the public view instructs front end to be public """ response = self.client.get(reverse('records:public_programs', kwargs={'uuid': self.pcr.uuid.hex})) is_public = response.context_data['is_public'] self.assertTrue(is_public) def test_private_access(self): """ Verify that the private view instructs front end to be private """ response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) is_public = response.context_data['is_public'] self.assertFalse(is_public) def test_public_private_data(self): """ Verify that the public and private views return the same record data """ response = self.client.get(reverse('records:public_programs', kwargs={'uuid': self.pcr.uuid.hex})) public_data = json.loads(response.context_data['record']) response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) private_data = json.loads(response.context_data['record']) self.assertEqual(private_data, public_data) def test_highest_grades(self): """ Verify that the view only shows the highest *percentage* grade Also verified that the attempts are counted correctly, even with revoked certs """ response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) grades = json.loads(response.context_data['record'])['grades'] self.assertEqual(len(grades), 1) grade = grades[0] expected_grade = {'name': self.course_runs[1].title, 'school': '', 'attempts': 3, 'course_id': self.course_runs[1].key, 'issue_date': self.user_credentials[1].created.isoformat(), 'percent_grade': self.user_grade_high.percent_grade, 'letter_grade': self.user_grade_high.letter_grade, } self.assertEqual(grade, expected_grade) def test_visible_date_as_issue_date(self): """ Verify that we show visible_date when available """ UserCredentialAttributeFactory(user_credential=self.user_credentials[1], name='visible_date', value='2017-07-31T09:32:46Z') response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) grades = json.loads(response.context_data['record'])['grades'] self.assertEqual(len(grades), 1) self.assertEqual(grades[0]['issue_date'], '2017-07-31T09:32:46+00:00') def test_future_visible_date_not_shown(self): """ Verify that we don't show certificates with a visible_date in the future """ UserCredentialAttributeFactory(user_credential=self.user_credentials[1], name='visible_date', value=datetime.datetime.max.strftime('%Y-%m-%dT%H:%M:%SZ')) response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) grades = json.loads(response.context_data['record'])['grades'] self.assertEqual(len(grades), 1) self.assertEqual(grades[0]['course_id'], self.course_runs[0].key) # 0 instead of 1 now that 1 is in future self.assertEqual(grades[0]['issue_date'], self.user_credentials[0].created.isoformat()) @ddt.data( ('9999-01-01T01:01:01Z', False), ('1970-01-01T01:01:01Z', True), (None, True), ) @ddt.unpack def test_program_visible_date(self, date, completed): """ Test that the program's visible_date is considered """ program_credential = UserCredentialFactory( username=self.MOCK_USER_DATA['username'], credential_content_type=self.program_content_type, credential=self.program_cert) if date: UserCredentialAttributeFactory( user_credential=program_credential, name='visible_date', value=date, ) response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) self.assertEqual(json.loads(response.context_data['record'])['program']['completed'], completed) def test_organization_order(self): """ Test that the organizations are returned in the order they were added """ self.course.owners = self.orgs response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) program_data = json.loads(response.context_data['record'])['program'] grade = json.loads(response.context_data['record'])['grades'][0] self.assertEqual(program_data['school'], ', '.join(self.org_names)) self.assertEqual(grade['school'], ', '.join(self.org_names)) def test_course_run_order(self): """ Test that the course_runs are returned in the program order """ new_course_run = CourseRunFactory() self.program.course_runs.add(new_course_run) UserGradeFactory(username=self.MOCK_USER_DATA['username'], course_run=new_course_run, letter_grade='C', percent_grade=.70) new_course_cert = CourseCertificateFactory(course_id=new_course_run.key, site=self.site) UserCredentialFactory(username=self.MOCK_USER_DATA['username'], credential_content_type=self.credential_content_type, credential=new_course_cert) response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) grades = json.loads(response.context_data['record'])['grades'] expected_course_run_keys = [course_run.key for course_run in [self.course_runs[1], new_course_run]] actual_course_run_keys = [grade['course_id'] for grade in grades] self.assertEqual(expected_course_run_keys, actual_course_run_keys) def test_course_run_no_credential(self): """ Adds a course run with no credential and tests that it does appear in the results """ new_course_run = CourseRunFactory() self.program.course_runs.add(new_course_run) UserGradeFactory(username=self.MOCK_USER_DATA['username'], course_run=new_course_run, letter_grade='F', percent_grade=.05) CourseCertificateFactory(course_id=new_course_run.key, site=self.site) response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) grades = json.loads(response.context_data['record'])['grades'] self.assertEqual(len(grades), 2) self.assertEqual(new_course_run.course.title, grades[1]['name']) def test_multiple_attempts_no_cert(self): """ Adds a course with two failed course_run attempts (no cert) and verifies that the course only shows up once """ # Only superusers can view an empty program (we could add a real cert too here, but this is a more direct test) self.user.is_superuser = True self.user.save() new_course = CourseFactory(site=self.site) new_course_runs = CourseRunFactory.create_batch(2, course=new_course) _ = [UserGradeFactory(username=self.MOCK_USER_DATA['username'], course_run=course_run, letter_grade='F', percent_grade=0.20) for course_run in new_course_runs] self.program.course_runs = new_course_runs response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) grades = json.loads(response.context_data['record'])['grades'] self.assertEqual(len(grades), 1) self.assertEqual(new_course.title, grades[0]['name']) def test_learner_data(self): """ Test that the learner data is returned succesfully """ response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) learner_data = json.loads(response.context_data['record'])['learner'] expected = {'full_name': self.user.get_full_name(), 'username': str(self.user), 'email': self.user.email} self.assertEqual(learner_data, expected) def test_program_data(self): """ Test that the program data is returned successfully """ response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) program_data = json.loads(response.context_data['record'])['program'] expected = {'name': self.program.title, 'type': slugify(self.program.type), 'type_name': self.program.type, 'completed': False, 'empty': False, 'last_updated': UserCredential.objects.last().created.isoformat(), 'school': ', '.join(self.org_names)} self.assertEqual(program_data, expected) def test_pathway_data(self): """ Test that the pathway data is returned successfully """ response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) pathway_data = json.loads(response.context_data['record'])['pathways'] expected = [{'name': self.pathway.name, 'id': self.pathway.id, 'status': '', 'is_active': True, 'pathway_type': PathwayType.CREDIT.value}] self.assertEqual(pathway_data, expected) def test_pathway_no_email(self): """ Test that a pathway without an email is inactive """ self.pathway.email = '' self.pathway.save() response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) pathway_data = json.loads(response.context_data['record'])['pathways'] expected = [{'name': self.pathway.name, 'id': self.pathway.id, 'status': '', 'is_active': False, 'pathway_type': PathwayType.CREDIT.value}] self.assertEqual(pathway_data, expected) def test_sent_pathway_status(self): """ Test that a user credit pathway pathway that has already been sent includes a pathway """ UserCreditPathwayFactory(pathway=self.pathway, user=self.user) response = self.client.get(reverse('records:private_programs', kwargs={'uuid': self.program.uuid.hex})) pathway_data = json.loads(response.context_data['record'])['pathways'] expected = [{'name': self.pathway.name, 'id': self.pathway.id, 'status': 'sent', 'is_active': True, 'pathway_type': PathwayType.CREDIT.value}] self.assertEqual(pathway_data, expected) def test_xss(self): """ Verify that the view protects against xss in translations. """ response = self._render_program_record({ "name": "<xss>", 'program': { 'name': '<xss>', 'school': 'XSS School', }, 'uuid': 'uuid', }) # Test that the data is parsed from an escaped string self.assertContains(response, "JSON.parse(\'{\\u0022name\\u0022: \\u0022\\u003Cxss\\u003E\\u0022, " + "\\u0022program\\u0022: {\\u0022name\\u0022: \\u0022\\u003Cxss\\u003E\\u0022, " + "\\u0022school\\u0022: \\u0022XSS School\\u0022}, \\u0022uuid\\u0022: " + "\\u0022uuid\\u0022}\')") self.assertNotContains(response, '<xss>')
class ProgramListingViewTests(SiteMixin, TestCase): MOCK_USER_DATA = { "username": "******", "name": "Test User", "email": "*****@*****.**", } def setUp(self): super().setUp() dump_random_state() self.user = UserFactory(username=self.MOCK_USER_DATA["username"], is_staff=True) self.orgs = [OrganizationFactory.create(name=name, site=self.site) for name in ["TestOrg1", "TestOrg2"]] self.course = CourseFactory.create(site=self.site) self.course_runs = CourseRunFactory.create_batch(2, course=self.course) self.program = ProgramFactory( title="TestProgram1", course_runs=self.course_runs, authoring_organizations=self.orgs, site=self.site ) self.course_certs = [ CourseCertificateFactory.create( course_id=course_run.key, site=self.site, ) for course_run in self.course_runs ] self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) self.course_credential_content_type = ContentType.objects.get( app_label="credentials", model="coursecertificate" ) self.program_credential_content_type = ContentType.objects.get( app_label="credentials", model="programcertificate" ) self.course_user_credentials = [ UserCredentialFactory.create( username=self.user.username, credential_content_type=self.course_credential_content_type, credential=course_cert, ) for course_cert in self.course_certs ] self.program_user_credentials = UserCredentialFactory.create( username=self.user.username, credential_content_type=self.program_credential_content_type, credential=self.program_cert, ) self.client.login(username=self.user.username, password=USER_PASSWORD) def _render_listing(self, expected_program_data=None, status_code=200): """ Helper method to mock and render a user certificate.""" response = self.client.get(reverse("program_listing")) self.assertEqual(response.status_code, status_code) if expected_program_data is not None: program_data = json.loads(response.context_data["programs"]) self.assertListEqual(program_data, expected_program_data) return response def _default_program_data(self, overrides=None): # if nothing is adjusted, this is the expected listing data = [ { "name": self.program.title, "partner": "TestOrg1, TestOrg2", "uuid": self.program.uuid.hex, "type": slugify(self.program.type), "completed": True, "empty": False, }, ] if overrides is not None: data[0].update(overrides) return data def _verify_normal_access(self): response = self._render_listing() response_context_data = response.context_data self.assertContains(response, "Program Listing View") actual_child_templates = response_context_data["child_templates"] self.assert_matching_template_origin(actual_child_templates["footer"], "_footer.html") self.assert_matching_template_origin(actual_child_templates["header"], "_header.html") self.assertNotIn("masquerade", actual_child_templates) # no masquerading on this view def assert_matching_template_origin(self, actual, expected_template_name): expected = select_template([expected_template_name]) self.assertEqual(actual.origin, expected.origin) def test_no_anonymous_access(self): """ Verify that the view rejects non-logged-in users. """ self.client.logout() response = self._render_listing(status_code=302) self.assertRegex(response.url, "^/login/.*") def test_non_superuser_access(self): """ Verify that the view rejects non-superuser users. """ self.user.is_superuser = False self.user.is_staff = False self.user.save() self._render_listing(status_code=404) def test_only_staff_access(self): """ Verify that the view rejects non-staff users. """ self.user.is_staff = False self.user.save() self._render_listing(status_code=404) def test_normal_access_superuser(self): """ Verify that the view works with only superuser, no staff. """ self.user.is_superuser = True self.user.is_staff = False self._verify_normal_access() def test_normal_access_as_staff(self): """ Verify that the view works in default case. Staff is set in the setup method.""" self._verify_normal_access() @ddt.data( (Program.ACTIVE, True), (Program.RETIRED, False), # this is different from RecordsView (Program.DELETED, False), (Program.UNPUBLISHED, False), ) @ddt.unpack def test_completed_render_from_db(self, status, visible): """ Verify that a program cert that is completed is returned correctly, with different statuses """ self.program.status = status self.program.save() data = self._default_program_data() if visible else [] self._render_listing(expected_program_data=data) def test_in_progress_from_db(self): """ Verify that no program cert, but course certs results in an In Progress program """ # Delete the program cert self.program_cert.delete() data = self._default_program_data(overrides={"completed": False}) self._render_listing(expected_program_data=data) def test_empty_programs(self): """ Test that a program with no certs shows as empty """ # Delete all certs for cert in self.course_certs: cert.delete() self.program_cert.delete() data = self._default_program_data(overrides={"completed": False, "empty": True}) self._render_listing(expected_program_data=data)
class ProgramListingViewTests(SiteMixin, TestCase): MOCK_USER_DATA = {'username': '******', 'name': 'Test User', 'email': '*****@*****.**', } def setUp(self): super().setUp() dump_random_state() self.user = UserFactory(username=self.MOCK_USER_DATA['username'], is_superuser=True) self.orgs = [OrganizationFactory.create(name=name, site=self.site) for name in ['TestOrg1', 'TestOrg2']] self.course = CourseFactory.create(site=self.site) self.course_runs = CourseRunFactory.create_batch(2, course=self.course) self.program = ProgramFactory(title="TestProgram1", course_runs=self.course_runs, authoring_organizations=self.orgs, site=self.site) self.course_certs = [CourseCertificateFactory.create( course_id=course_run.key, site=self.site, ) for course_run in self.course_runs] self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) self.course_credential_content_type = ContentType.objects.get( app_label='credentials', model='coursecertificate' ) self.program_credential_content_type = ContentType.objects.get( app_label='credentials', model='programcertificate' ) self.course_user_credentials = [UserCredentialFactory.create( username=self.user.username, credential_content_type=self.course_credential_content_type, credential=course_cert ) for course_cert in self.course_certs] self.program_user_credentials = UserCredentialFactory.create( username=self.user.username, credential_content_type=self.program_credential_content_type, credential=self.program_cert ) self.client.login(username=self.user.username, password=USER_PASSWORD) def _render_listing(self, expected_program_data=None, status_code=200): """ Helper method to mock and render a user certificate.""" response = self.client.get(reverse('program_listing')) self.assertEqual(response.status_code, status_code) if expected_program_data is not None: program_data = json.loads(response.context_data['programs']) self.assertListEqual(program_data, expected_program_data) return response def _default_program_data(self, overrides=None): # if nothing is adjusted, this is the expected listing data = [ { 'name': self.program.title, 'partner': 'TestOrg1, TestOrg2', 'uuid': self.program.uuid.hex, 'type': slugify(self.program.type), 'completed': True, 'empty': False, }, ] if overrides is not None: data[0].update(overrides) return data def assert_matching_template_origin(self, actual, expected_template_name): expected = select_template([expected_template_name]) self.assertEqual(actual.origin, expected.origin) def test_no_anonymous_access(self): """ Verify that the view rejects non-logged-in users. """ self.client.logout() response = self._render_listing(status_code=302) self.assertRegexpMatches(response.url, '^/login/.*') # pylint: disable=deprecated-method def test_only_superuser_access(self): """ Verify that the view rejects non-superusers. """ self.user.is_superuser = False self.user.save() self._render_listing(status_code=404) def test_normal_access(self): """ Verify that the view works in default case. """ response = self._render_listing() response_context_data = response.context_data self.assertContains(response, 'Program Listing View') actual_child_templates = response_context_data['child_templates'] self.assert_matching_template_origin(actual_child_templates['footer'], '_footer.html') self.assert_matching_template_origin(actual_child_templates['header'], '_header.html') self.assertFalse('masquerade' in actual_child_templates) # no masquerading on this view @ddt.data( (Program.ACTIVE, True), (Program.RETIRED, False), # this is different from RecordsView (Program.DELETED, False), (Program.UNPUBLISHED, False), ) @ddt.unpack def test_completed_render_from_db(self, status, visible): """ Verify that a program cert that is completed is returned correctly, with different statuses """ self.program.status = status self.program.save() data = self._default_program_data() if visible else [] self._render_listing(expected_program_data=data) def test_in_progress_from_db(self): """ Verify that no program cert, but course certs results in an In Progress program """ # Delete the program cert self.program_cert.delete() data = self._default_program_data(overrides={'completed': False}) self._render_listing(expected_program_data=data) def test_empty_programs(self): """ Test that a program with no certs shows as empty """ # Delete all certs for cert in self.course_certs: cert.delete() self.program_cert.delete() data = self._default_program_data(overrides={'completed': False, 'empty': True}) self._render_listing(expected_program_data=data)