Пример #1
0
 def _setup_program_and_program_cert(self, program_type):
     self.program = ProgramFactory(site=self.site)
     self.program.type = program_type
     self.program.type_slug = slugify(program_type)
     self.program.save()
     self.program_cert = ProgramCertificateFactory(
         site=self.site, program_uuid=self.program.uuid)
 def test_successful_generation(self):
     ProgramFactory.create(
         title="edX Demonstration Program",
         course_runs=[self.new_course_run],
         authoring_organizations=self.orgs,
         site=self.site,
     )
     call_command(command_name=COMMAND)
     assert len(ProgramCertificate.objects.all()) > 0
Пример #3
0
    def test_list_program_uuid_filtering(self):
        """ Verify the endpoint returns data for all UserCredentials in the given program. """

        # Course run 1 is in a program, course run 2 is not
        course1_run = CourseRunFactory()
        course2_run = CourseRunFactory()
        program = ProgramFactory(course_runs=[course1_run])

        program_certificate = ProgramCertificateFactory(
            site=self.site, program_uuid=program.uuid)
        course1_certificate = CourseCertificateFactory(
            site=self.site, course_id=course1_run.key)
        course2_certificate = CourseCertificateFactory(
            site=self.site, course_id=course2_run.key)

        # Create some credentials related to the program
        course1_cred = UserCredentialFactory(credential=course1_certificate)
        program_creds = UserCredentialFactory.create_batch(
            3, credential=program_certificate)
        expected = [course1_cred] + program_creds

        # Create some more credentials that we don't expect to see returned
        UserCredentialFactory.create_batch(3)
        UserCredentialFactory(credential=course2_certificate)

        self.authenticate_user(self.user)
        self.add_user_permission(self.user, 'view_usercredential')

        response = self.client.get(self.list_path +
                                   '?program_uuid={}'.format(program.uuid))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['results'],
                         self.serialize_user_credential(expected, many=True))
Пример #4
0
    def setUp(self):
        super().setUp()
        dump_random_state()

        user = UserFactory(username=self.USERNAME)
        self.client.login(username=user.username, password=USER_PASSWORD)
        self.program = ProgramFactory(site=self.site)
Пример #5
0
 def setUp(self):
     self.site = SiteFactory()
     self.program = ProgramFactory(site=self.site)
     self.certificate = self.cert_factory.create(program_uuid=self.program.uuid, site=self.site)
     self.username = '******'
     self.user = UserFactory(username=self.username)
     self.user_cred = self.issuer.issue_credential(self.certificate, self.username)
     self.attributes = [{"name": "whitelist_reason", "value": "Reason for whitelisting."}]
Пример #6
0
    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])
Пример #7
0
    def setUp(self):
        super().setUp()
        dump_random_state()

        self.user = UserFactory(username=self.MOCK_USER_DATA["username"])
        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_credential = 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)
Пример #8
0
    def setUp(self):
        super().setUp()
        dump_random_state()
        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.pcr = ProgramCertRecordFactory(program=self.program, user=self.user)
        self.data = {'username': self.USERNAME, 'pathway_id': self.pathway.id}
        self.url = reverse('records:share_program', kwargs={'uuid': self.program.uuid.hex})

        mail.outbox = []
Пример #9
0
    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 = []
Пример #10
0
    def test_get_program_details(self):
        program_uuid = uuid.uuid4()
        unused_program_uuid = uuid.uuid4()
        program = ProgramFactory.create(uuid=program_uuid, site=self.site)

        details = get_program_details_by_uuid(uuid=program_uuid,
                                              site=program.site)
        self.assertIsNotNone(details)
        self.assertIsInstance(details, ProgramDetails)

        details = get_program_details_by_uuid(uuid=unused_program_uuid,
                                              site=program.site)
        self.assertIsNone(details)
Пример #11
0
    def test_multiple_programs(self):
        """ Test that multiple programs can appear, in progress and completed """
        # Create a second program, and delete the first one's certificate
        new_course = CourseFactory.create(site=self.site)
        new_course_run = CourseRunFactory.create(course=new_course)

        new_program = ProgramFactory.create(title='ZTestProgram',
                                            course_runs=[new_course_run],
                                            authoring_organizations=self.orgs,
                                            site=self.site)
        new_course_cert = CourseCertificateFactory.create(course_id=new_course_run.key, site=self.site)
        new_program_cert = ProgramCertificateFactory.create(program_uuid=new_program.uuid, site=self.site)

        # Make a new user credential
        UserCredentialFactory.create(
            username=self.user.username,
            credential_content_type=self.program_credential_content_type,
            credential=new_course_cert
        )
        # Make a new program credential
        UserCredentialFactory.create(
            username=self.user.username,
            credential_content_type=self.program_credential_content_type,
            credential=new_program_cert
        )
        self.program_user_credential.delete()

        response = self.client.get(reverse('records:index'))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data['programs'])
        expected_program_data = [
            {
                'name': self.program.title,
                'partner': 'TestOrg1, TestOrg2',
                'uuid': self.program.uuid.hex,
                'type': slugify(self.program.type),
                'completed': False,
                'empty': False,
            },
            {
                'name': new_program.title,
                'partner': 'TestOrg1, TestOrg2',
                'uuid': new_program.uuid.hex,
                'type': slugify(new_program.type),
                'completed': True,
                'empty': False,
            }
        ]
        self.assertEqual(program_data, expected_program_data)
Пример #12
0
    def setUp(self):
        super().setUp()

        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(course=self.course) for _ in range(3)
        ]

        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.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.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)
Пример #13
0
    def test_create(self):
        program = ProgramFactory(site=self.site)
        program_certificate = ProgramCertificateFactory(
            site=self.site, program_uuid=program.uuid)
        expected_username = self.user.username
        expected_attribute_name = 'fake-name'
        expected_attribute_value = 'fake-value'
        data = {
            'username':
            expected_username,
            'credential': {
                'program_uuid': str(program_certificate.program_uuid)
            },
            'status':
            'awarded',
            'attributes': [{
                'name': expected_attribute_name,
                'value': expected_attribute_value,
            }],
        }

        # Verify users without the add permission are denied access
        self.assert_access_denied(self.user, 'post', self.list_path, data=data)

        self.authenticate_user(self.user)
        self.add_user_permission(self.user, 'add_usercredential')
        response = self.client.post(self.list_path,
                                    data=json.dumps(data),
                                    content_type=JSON_CONTENT_TYPE)
        user_credential = UserCredential.objects.last()

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data,
                         self.serialize_user_credential(user_credential))

        self.assertEqual(user_credential.username, expected_username)
        self.assertEqual(user_credential.credential, program_certificate)
        self.assertEqual(user_credential.attributes.count(), 1)

        attribute = user_credential.attributes.first()
        self.assertEqual(attribute.name, expected_attribute_name)
        self.assertEqual(attribute.value, expected_attribute_value)
Пример #14
0
    def test_create(self):
        program = ProgramFactory(site=self.site)
        program_certificate = ProgramCertificateFactory(
            site=self.site, program_uuid=program.uuid)
        expected_username = self.user.username
        expected_attribute_name = "fake-name"
        expected_attribute_value = "fake-value"
        data = {
            "username":
            expected_username,
            "credential": {
                "program_uuid": str(program_certificate.program_uuid)
            },
            "status":
            "awarded",
            "attributes": [{
                "name": expected_attribute_name,
                "value": expected_attribute_value,
            }],
        }

        # Verify users without the add permission are denied access
        self.assert_access_denied(self.user, "post", self.list_path, data=data)

        self.authenticate_user(self.user)
        self.add_user_permission(self.user, "add_usercredential")
        response = self.client.post(self.list_path,
                                    data=json.dumps(data),
                                    content_type=JSON_CONTENT_TYPE)
        user_credential = UserCredential.objects.last()

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data,
                         self.serialize_user_credential(user_credential))

        self.assertEqual(user_credential.username, expected_username)
        self.assertEqual(user_credential.credential, program_certificate)
        self.assertEqual(user_credential.attributes.count(), 1)

        attribute = user_credential.attributes.first()
        self.assertEqual(attribute.name, expected_attribute_name)
        self.assertEqual(attribute.value, expected_attribute_value)
Пример #15
0
    def test_keep_old_data(self):
        """ Verify that we don't destroy existing but obsolete data. """
        # org with a uuid that won't be in data we get back
        org = OrganizationFactory(site=self.site,
                                  uuid='44f0dded-fee9-4dec-a333-b9d8c2c82bd2',
                                  key='OldX')
        program = ProgramFactory(site=self.site,
                                 uuid='33f0dded-fee9-4dec-a333-c9d8c2c82bd5',
                                 title='Old Program')
        program.authoring_organizations.add(org)  # pylint: disable=no-member

        self.mock_access_token_response()
        self.mock_programs_response(self.wrap_programs(self.PROGRAMS))
        self.mock_pathways_response(self.wrap_pathways(self.PATHWAYS))
        self.call_command()

        org = Organization.objects.get(uuid='44f0dded-fee9-4dec-a333-b9d8c2c82bd2',
                                       key='OldX')

        # But note that it will no longer be connected to our program
        program = Program.objects.get(uuid='33f0dded-fee9-4dec-a333-c9d8c2c82bd5')
        self.assertNotIn(org, program.authoring_organizations.all())
Пример #16
0
    def test_update(self):
        """ Verify the command updates existing data. """
        OrganizationFactory(site=self.site,
                            uuid='33f0dded-fee9-4dec-a333-b9d8c2c82bd2',
                            key='OldX')
        ProgramFactory(site=self.site,
                       uuid='33f0dded-fee9-4dec-a333-c9d8c2c82bd5',
                       title='Old Program')

        self.mock_access_token_response()
        self.mock_programs_response(self.wrap_programs(self.PROGRAMS))
        self.mock_pathways_response(self.wrap_pathways(self.PATHWAYS))
        self.call_command()

        self.assertProgramsSaved()
        self.assertPathwaysSaved()

        org = Organization.objects.get(uuid='33f0dded-fee9-4dec-a333-b9d8c2c82bd2')
        self.assertNotEqual(org.key, 'OldX')

        program = Program.objects.get(uuid='33f0dded-fee9-4dec-a333-c9d8c2c82bd5')
        self.assertNotEqual(program.title, 'Old Program')
Пример #17
0
    def setUp(self):
        super().setUp()
        self.user = UserFactory(username=self.MOCK_USER_DATA['username'])
        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(course=self.course) for _ in range(2)
        ]
        self.program = ProgramFactory(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)
Пример #18
0
 def test_protected_deletion(self):
     program = ProgramFactory()
     ProgramCertRecordFactory(program=program)
     with self.assertRaises(ProtectedError):
         program.delete()
Пример #19
0
class RecordsViewTests(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.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_credential = 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_records(self, program_data=None, status_code=200):
        """ Helper method to mock and render a user certificate."""
        if program_data is None:
            program_data = []

        with patch("credentials.apps.records.views.RecordsView._get_programs") as get_programs:
            get_programs.return_value = program_data
            response = self.client.get(reverse("records:index"))
            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(self):
        """ Verify that the view rejects non-logged-in users. """
        self.client.logout()
        response = self._render_records(status_code=302)
        self.assertRegex(response.url, "^/login/.*")

    def test_normal_access(self):
        """ Verify that the view works in default case. """
        response = self._render_records()
        response_context_data = response.context_data

        self.assertContains(response, "My Learner Records")

        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_xss(self):
        """ Verify that the view protects against xss in translations. """
        response = self._render_records(
            [
                {
                    "name": "<xss>",
                    "partner": "XSS",
                    "uuid": "uuid",
                },
            ]
        )

        # Test that the data is parsed from an escaped string
        self.assertContains(
            response,
            "JSON.parse('[{"
            + "\\u0022name\\u0022: \\u0022\\u003Cxss\\u003E\\u0022, "
            + "\\u0022partner\\u0022: \\u0022XSS\\u0022, "
            + "\\u0022uuid\\u0022: \\u0022uuid\\u0022"
            + "}]')",
        )
        self.assertNotContains(response, "<xss>")

    def test_help_url(self):
        """ Verify that the records help url gets loaded into the context """
        response = self._render_records()
        response_context_data = response.context_data
        self.assertIn("records_help_url", response_context_data)
        self.assertNotEqual(response_context_data["records_help_url"], "")

    @ddt.data(
        (Program.ACTIVE, True),
        (Program.RETIRED, True),
        (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()

        response = self.client.get(reverse("records:index"))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data["programs"])

        expected_program_data = [
            {
                "name": self.program.title,
                "partner": "TestOrg1, TestOrg2",
                "uuid": self.program.uuid.hex,
                "type": slugify(self.program.type),
                "completed": True,
                "empty": False,
            }
        ]
        self.assertEqual(program_data, expected_program_data if visible else [])

    def test_in_progress_from_db(self):
        """ Verify that no program cert, but course certs results in an In Progress program """
        # Delete the program
        self.program_cert.delete()
        response = self.client.get(reverse("records:index"))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data["programs"])
        expected_program_data = [
            {
                "name": self.program.title,
                "partner": "TestOrg1, TestOrg2",
                "uuid": self.program.uuid.hex,
                "type": slugify(self.program.type),
                "completed": False,
                "empty": False,
            }
        ]
        self.assertEqual(program_data, expected_program_data)

    def test_not_visible_from_db(self):
        """ Test that the program's visible_date is considered """
        UserCredentialAttributeFactory(
            user_credential=self.program_user_credential,
            name="visible_date",
            value="9999-01-01T01:01:01Z",
        )
        response = self.client.get(reverse("records:index"))
        self.assertFalse(json.loads(response.context_data["programs"])[0]["completed"])

    def test_multiple_programs(self):
        """ Test that multiple programs can appear, in progress and completed """
        # Create a second program, and delete the first one's certificate
        new_course = CourseFactory.create(site=self.site)
        new_course_run = CourseRunFactory.create(course=new_course)

        new_program = ProgramFactory.create(
            title="ZTestProgram", course_runs=[new_course_run], authoring_organizations=self.orgs, site=self.site
        )
        new_course_cert = CourseCertificateFactory.create(course_id=new_course_run.key, site=self.site)
        new_program_cert = ProgramCertificateFactory.create(program_uuid=new_program.uuid, site=self.site)

        # Make a new user credential
        UserCredentialFactory.create(
            username=self.user.username,
            credential_content_type=self.program_credential_content_type,
            credential=new_course_cert,
        )
        # Make a new program credential
        UserCredentialFactory.create(
            username=self.user.username,
            credential_content_type=self.program_credential_content_type,
            credential=new_program_cert,
        )
        self.program_user_credential.delete()

        response = self.client.get(reverse("records:index"))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data["programs"])
        expected_program_data = [
            {
                "name": self.program.title,
                "partner": "TestOrg1, TestOrg2",
                "uuid": self.program.uuid.hex,
                "type": slugify(self.program.type),
                "completed": False,
                "empty": False,
            },
            {
                "name": new_program.title,
                "partner": "TestOrg1, TestOrg2",
                "uuid": new_program.uuid.hex,
                "type": slugify(new_program.type),
                "completed": True,
                "empty": False,
            },
        ]
        self.assertEqual(program_data, expected_program_data)
Пример #20
0
class ProgramCertificateIssuedEmailTests(SiteMixin, TestCase):
    """
    Tests for the automated email sent to learners after completing an edX Program.
    """
    USERNAME = "******"
    FAKE_PROGRAM_UUID = 'f6551af4-aa5a-4089-801b-53485d0d1726'

    def setUp(self):
        super().setUp()
        self.user = UserFactory(username=self.USERNAME)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.program = None
        self.program_cert = None

        mail.outbox = []

    def _get_custom_completion_email_template_settings(self):
        return {
            'f6551af4-aa5a-4089-801b-53485d0d1726': {
                'plaintext':
                '''
                I am email one
                I have the best content
                ''',
                'html':
                '''
                <p>I am email one</p>
                <p>I have the best content</p>
                ''',
            },
            'excellent-program': {
                'plaintext':
                '''
                I am email two
                I have better content
                ''',
                'html':
                '''
                <p>I am email two</p>
                <p>I have better content</p>
                ''',
            },
            'tubular-program': {
                'plaintext':
                '''
                I am email three
                I have great content too
                ''',
                'html':
                '''
                <p>I am email three</p>
                <p>I have great content too</p>
                ''',
            }
        }

    def _setup_program_and_program_cert(self, program_type, uuid=None):
        self.program = ProgramFactory(site=self.site)
        self.program.type = program_type
        self.program.type_slug = slugify(program_type)

        if uuid:
            self.program.uuid = uuid

        self.program.save()

        self.program_cert = ProgramCertificateFactory(
            site=self.site, program_uuid=self.program.uuid)

    def _build_expected_plaintext_email_body(self):
        custom_completion_email_template_settings = self._get_custom_completion_email_template_settings(
        )

        email_fragments = [
            'Congratulations on completing the {} {} Program!'.format(
                self.program.title,
                self.program.type,
            ),
            'Sincerely,',
            'The {} Team'.format(self.site.siteconfiguration.platform_name),
        ]

        if custom_completion_email_template_settings.get(self.program.uuid):
            email_fragments.append(
                textwrap.dedent(
                    custom_completion_email_template_settings.get(
                        self.program.uuid).get('plaintext')))
        elif custom_completion_email_template_settings.get(
                self.program.type_slug):
            email_fragments.append(
                textwrap.dedent(
                    custom_completion_email_template_settings.get(
                        self.program.type_slug).get('plaintext')))

        return email_fragments

    def _build_expected_html_email_body(self):
        custom_completion_email_template_settings = self._get_custom_completion_email_template_settings(
        )

        email_fragments = [
            "Congratulations on completing the {} {} Program!".format(
                self.program.title, self.program.type),
            'Sincerely,<br/>The {} Team'.format(
                self.site.siteconfiguration.platform_name),
        ]

        if custom_completion_email_template_settings.get(self.program.uuid):
            email_fragments.append(
                custom_completion_email_template_settings.get(
                    self.program.uuid).get('html'))
        elif custom_completion_email_template_settings.get(
                self.program.type_slug):
            email_fragments.append(
                custom_completion_email_template_settings.get(
                    self.program.type_slug).get('html'))

        return email_fragments

    def _assert_email_contents(self):
        """
        Utility function that verifies the contents of the generated email
        """
        expected_plaintext_email_contents = self._build_expected_plaintext_email_body(
        )
        expected_html_email_contents = self._build_expected_html_email_body()

        self.assertEqual(1, len(mail.outbox))

        email = mail.outbox[0]
        plaintext_body = email.body
        html_body = email.alternatives[0][0]

        self.assertEqual(email.to[0], self.user.email)
        self._assert_subject(email.subject)
        self._assert_email_body_contents(plaintext_body,
                                         expected_plaintext_email_contents)
        self._assert_email_body_contents(html_body,
                                         expected_html_email_contents)

    def _assert_subject(self, email_subject):
        """
        Utility method that verifies the subject text of the automated emails being sent to
        learners.
        """
        expected_subject = 'Congratulations for finishing your {} {} Program!'.format(
            self.program.title, self.program.type)
        self.assertEqual(email_subject, expected_subject)

    def _assert_email_body_contents(self, email_body, fragments):
        """
        Utility method that verifies the content in the generated email is as expected.
        """
        for fragment in fragments:
            self.assertIn(fragment, email_body)

    def test_base_template(self):
        self._setup_program_and_program_cert('Radical Program')

        send_program_certificate_created_message(self.user.username,
                                                 self.program_cert)

        self._assert_email_contents()

    def test_custom_email_template_program_uuid(self):
        """
        Test for the contents of a custom email template.
        """
        self._setup_program_and_program_cert('Excellent Program',
                                             self.FAKE_PROGRAM_UUID)

        with self.settings(CUSTOM_COMPLETION_EMAIL_TEMPLATE_EXTRA=self.
                           _get_custom_completion_email_template_settings()):
            send_program_certificate_created_message(self.user.username,
                                                     self.program_cert)

        self._assert_email_contents()

    def test_custom_email_template_program_type(self):
        self._setup_program_and_program_cert('Excellent Program')

        with self.settings(CUSTOM_COMPLETION_EMAIL_TEMPLATE_EXTRA=self.
                           _get_custom_completion_email_template_settings()):
            send_program_certificate_created_message(self.user.username,
                                                     self.program_cert)

        self._assert_email_contents()

    def test_custom_email_template_alternative_program_type(self):
        self._setup_program_and_program_cert('Tubular Program')

        with self.settings(CUSTOM_COMPLETION_EMAIL_TEMPLATE_EXTRA=self.
                           _get_custom_completion_email_template_settings()):
            send_program_certificate_created_message(self.user.username,
                                                     self.program_cert)

        self._assert_email_contents()

    def test_send_email_exception_occurs(self):
        self._setup_program_and_program_cert("Radical Program")

        expected_messages = [
            'Sending Program completion email to learner with id [{}] in Program [{}]'
            .format(self.user.id, self.program.uuid),
            'Unable to send email to learner with id: [{}] for Program [{}]. Error occurred while attempting to '
            'format or send message: Error!'.format(self.user.id,
                                                    self.program.uuid)
        ]

        with LogCapture() as log:
            with mock.patch('edx_ace.ace.send',
                            side_effect=Exception("Error!")):
                send_program_certificate_created_message(
                    self.user.username, self.program_cert)

        for index, message in enumerate(expected_messages):
            assert message in log.records[index].getMessage()
Пример #21
0
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)
Пример #22
0
class RecordsViewTests(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.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_credential = 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_records(self, program_data=None, status_code=200):
        """ Helper method to mock and render a user certificate."""
        if program_data is None:
            program_data = []

        with patch('credentials.apps.records.views.RecordsView._get_programs') as get_programs:
            get_programs.return_value = program_data
            response = self.client.get(reverse('records:index'))
            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(self):
        """ Verify that the view rejects non-logged-in users. """
        self.client.logout()
        response = self._render_records(status_code=302)
        self.assertRegex(response.url, '^/login/.*')  # pylint: disable=deprecated-method

    def test_normal_access(self):
        """ Verify that the view works in default case. """
        response = self._render_records()
        response_context_data = response.context_data

        self.assertContains(response, 'My Learner Records')

        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_xss(self):
        """ Verify that the view protects against xss in translations. """
        response = self._render_records([
            {
                "name": "<xss>",
                'partner': 'XSS',
                'uuid': 'uuid',
            },
        ])

        # Test that the data is parsed from an escaped string
        self.assertContains(response, 'JSON.parse(\'[{' +
                                      '\\u0022name\\u0022: \\u0022\\u003Cxss\\u003E\\u0022, ' +
                                      '\\u0022partner\\u0022: \\u0022XSS\\u0022, ' +
                                      '\\u0022uuid\\u0022: \\u0022uuid\\u0022' +
                                      '}]\')')
        self.assertNotContains(response, '<xss>')

    def test_help_url(self):
        """ Verify that the records help url gets loaded into the context """
        response = self._render_records()
        response_context_data = response.context_data
        self.assertIn('records_help_url', response_context_data)
        self.assertNotEqual(response_context_data['records_help_url'], '')

    @ddt.data(
        (Program.ACTIVE, True),
        (Program.RETIRED, True),
        (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()

        response = self.client.get(reverse('records:index'))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data['programs'])

        expected_program_data = [
            {
                'name': self.program.title,
                'partner': 'TestOrg1, TestOrg2',
                'uuid': self.program.uuid.hex,
                'type': slugify(self.program.type),
                'completed': True,
                'empty': False,
            }
        ]
        self.assertEqual(program_data, expected_program_data if visible else [])

    def test_in_progress_from_db(self):
        """ Verify that no program cert, but course certs reuslts in an In Progress program """
        # Delete the program
        self.program_cert.delete()
        response = self.client.get(reverse('records:index'))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data['programs'])
        expected_program_data = [
            {
                'name': self.program.title,
                'partner': 'TestOrg1, TestOrg2',
                'uuid': self.program.uuid.hex,
                'type': slugify(self.program.type),
                'completed': False,
                'empty': False,
            }
        ]
        self.assertEqual(program_data, expected_program_data)

    def test_not_visible_from_db(self):
        """ Test that the program's visible_date is considered """
        UserCredentialAttributeFactory(
            user_credential=self.program_user_credential,
            name='visible_date',
            value='9999-01-01T01:01:01Z',
        )
        response = self.client.get(reverse('records:index'))
        self.assertFalse(json.loads(response.context_data['programs'])[0]['completed'])

    def test_multiple_programs(self):
        """ Test that multiple programs can appear, in progress and completed """
        # Create a second program, and delete the first one's certificate
        new_course = CourseFactory.create(site=self.site)
        new_course_run = CourseRunFactory.create(course=new_course)

        new_program = ProgramFactory.create(title='ZTestProgram',
                                            course_runs=[new_course_run],
                                            authoring_organizations=self.orgs,
                                            site=self.site)
        new_course_cert = CourseCertificateFactory.create(course_id=new_course_run.key, site=self.site)
        new_program_cert = ProgramCertificateFactory.create(program_uuid=new_program.uuid, site=self.site)

        # Make a new user credential
        UserCredentialFactory.create(
            username=self.user.username,
            credential_content_type=self.program_credential_content_type,
            credential=new_course_cert
        )
        # Make a new program credential
        UserCredentialFactory.create(
            username=self.user.username,
            credential_content_type=self.program_credential_content_type,
            credential=new_program_cert
        )
        self.program_user_credential.delete()

        response = self.client.get(reverse('records:index'))
        self.assertEqual(response.status_code, 200)
        program_data = json.loads(response.context_data['programs'])
        expected_program_data = [
            {
                'name': self.program.title,
                'partner': 'TestOrg1, TestOrg2',
                'uuid': self.program.uuid.hex,
                'type': slugify(self.program.type),
                'completed': False,
                'empty': False,
            },
            {
                'name': new_program.title,
                'partner': 'TestOrg1, TestOrg2',
                'uuid': new_program.uuid.hex,
                'type': slugify(new_program.type),
                'completed': True,
                'empty': False,
            }
        ]
        self.assertEqual(program_data, expected_program_data)
Пример #23
0
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)
Пример #24
0
class ProgramCertificateIssuedEmailTests(SiteMixin, TestCase):
    """
    Tests for the automated email sent to learners after completing an edX Program.

    Testing that the right configuration is used is done in `test_models.py` so this code
    will only verify that an email matching the configuration is sent.
    """

    USERNAME = "******"
    FAKE_PROGRAM_UUID = "f6551af4-aa5a-4089-801b-53485d0d1726"

    def setUp(self):
        super().setUp()
        self.user = UserFactory(username=self.USERNAME)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.program = None
        self.program_cert = None

        mail.outbox = []

        # Setup program and default email config
        self._setup_program_and_program_cert("Example Program")
        self.default_config = ProgramCompletionEmailConfiguration.objects.create(
            identifier="default",
            html_template="<h1>Default Template</h1>",
            plaintext_template="Default Template",
            enabled=True,
        )

    def _setup_program_and_program_cert(self, program_type):
        self.program = ProgramFactory(site=self.site)
        self.program.type = program_type
        self.program.type_slug = slugify(program_type)
        self.program.save()
        self.program_cert = ProgramCertificateFactory(
            site=self.site, program_uuid=self.program.uuid)

    def _build_expected_plaintext_email_body(self):
        return [
            "Congratulations on completing the {} {} Program!".format(
                self.program.title,
                self.program.type,
            ),
            "Sincerely,",
            "The {} Team".format(self.site.siteconfiguration.platform_name),
            textwrap.dedent(self.default_config.plaintext_template),
        ]

    def _build_expected_html_email_body(self):
        return [
            "Congratulations on completing the {} {} Program!".format(
                self.program.title, self.program.type),
            "Sincerely,<br/>The {} Team".format(
                self.site.siteconfiguration.platform_name),
            self.default_config.html_template,
        ]

    def _assert_email_contents(self):
        """
        Utility function that verifies the contents of the generated email
        """
        expected_plaintext_email_contents = self._build_expected_plaintext_email_body(
        )
        expected_html_email_contents = self._build_expected_html_email_body()

        self.assertEqual(1, len(mail.outbox))

        email = mail.outbox[0]
        plaintext_body = email.body
        html_body = email.alternatives[0][0]

        self.assertEqual(email.to[0], self.user.email)
        self._assert_subject(email.subject)
        self._assert_email_body_contents(plaintext_body,
                                         expected_plaintext_email_contents)
        self._assert_email_body_contents(html_body,
                                         expected_html_email_contents)

    def _assert_subject(self, email_subject):
        """
        Utility method that verifies the subject text of the automated emails being sent to
        learners.
        """
        expected_subject = "Congratulations for finishing your {} {} Program!".format(
            self.program.title, self.program.type)
        self.assertEqual(email_subject, expected_subject)

    def _assert_email_body_contents(self, email_body, fragments):
        """
        Utility method that verifies the content in the generated email is as expected.
        """
        for fragment in fragments:
            self.assertIn(fragment, email_body)

    def test_base_template(self):
        send_program_certificate_created_message(self.user.username,
                                                 self.program_cert)
        self._assert_email_contents()

    def test_no_config(self):
        """With the config deleted, it shouldn't send an email"""
        self.default_config.delete()
        send_program_certificate_created_message(self.user.username,
                                                 self.program_cert)
        self.assertEqual(0, len(mail.outbox))

    def test_disabled_config(self):
        """With the config disabled, it shouldn't send an email"""
        self.default_config.enabled = False
        self.default_config.save()
        send_program_certificate_created_message(self.user.username,
                                                 self.program_cert)
        self.assertEqual(0, len(mail.outbox))

    def test_retired_program(self):
        self.program.status = ProgramStatus.RETIRED.value
        self.program.save()
        send_program_certificate_created_message(self.user.username,
                                                 self.program_cert)
        self.assertEqual(0, len(mail.outbox))

    def test_send_email_exception_occurs(self):
        self._setup_program_and_program_cert("Radical Program")

        expected_messages = [
            "Sending Program completion email to learner with id [{}] in Program [{}]"
            .format(self.user.id, self.program.uuid),
            "Unable to send email to learner with id: [{}] for Program [{}]. Error occurred while attempting to "
            "format or send message: Error!".format(self.user.id,
                                                    self.program.uuid),
        ]

        with LogCapture() as log:
            with mock.patch("edx_ace.ace.send",
                            side_effect=Exception("Error!")):
                send_program_certificate_created_message(
                    self.user.username, self.program_cert)

        for index, message in enumerate(expected_messages):
            assert message in log.records[index].getMessage()