""" Tests for BadgrBackend """ from datetime import datetime from unittest.mock import Mock, call, patch import ddt from django.db.models.fields.files import ImageFieldFile from django.test.utils import override_settings from lazy.lazy import lazy # lint-amnesty, pylint: disable=no-name-in-module from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from common.djangoapps.track.tests import EventTrackingTestCase from lms.djangoapps.badges.backends.badgr import BadgrBackend from lms.djangoapps.badges.models import BadgeAssertion from lms.djangoapps.badges.tests.factories import BadgeClassFactory from openedx.core.lib.tests.assertions.events import assert_event_matches from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory BADGR_SETTINGS = { 'BADGR_API_TOKEN': '12345', 'BADGR_BASE_URL': 'https://example.com', 'BADGR_ISSUER_SLUG': 'test-issuer', } # Should be the hashed result of test_slug as the slug, and test_component as the component EXAMPLE_SLUG = '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a' # pylint: disable=protected-access @ddt.ddt @override_settings(**BADGR_SETTINGS) class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): """ Tests the BadgeHandler object """ def setUp(self): """ Create a course and user to test with. """ super().setUp() # Need key to be deterministic to test slugs. self.course = CourseFactory.create( org='edX', course='course_test', run='test_run', display_name='Badged', start=datetime(year=2015, month=5, day=19), end=datetime(year=2015, month=5, day=20) ) self.user = UserFactory.create(email='example@example.com') CourseEnrollmentFactory.create(user=self.user, course_id=self.course.location.course_key, mode='honor') # Need to empty this on each run. BadgrBackend.badges = [] self.badge_class = BadgeClassFactory.create(course_id=self.course.location.course_key) self.legacy_badge_class = BadgeClassFactory.create( course_id=self.course.location.course_key, issuing_component='' ) self.no_course_badge_class = BadgeClassFactory.create() @lazy def handler(self): """ Lazily loads a BadgeHandler object for the current course. Can't do this on setUp because the settings overrides aren't in place. """ return BadgrBackend() def test_urls(self): """ Make sure the handler generates the correct URLs for different API tasks. """ assert self.handler._base_url == 'https://example.com/v1/issuer/issuers/test-issuer' # lint-amnesty, pylint: disable=no-member assert self.handler._badge_create_url == 'https://example.com/v1/issuer/issuers/test-issuer/badges' # lint-amnesty, pylint: disable=no-member assert self.handler._badge_url('test_slug_here') ==\ 'https://example.com/v1/issuer/issuers/test-issuer/badges/test_slug_here' assert self.handler._assertion_url('another_test_slug') ==\ 'https://example.com/v1/issuer/issuers/test-issuer/badges/another_test_slug/assertions' def check_headers(self, headers): """ Verify the a headers dict from a requests call matches the proper auth info. """ assert headers == {'Authorization': 'Token 12345'} def test_get_headers(self): """ Check to make sure the handler generates appropriate HTTP headers. """ self.check_headers(self.handler._get_headers()) # lint-amnesty, pylint: disable=no-member @patch('requests.post') def test_create_badge(self, post): """ Verify badge spec creation works. """ self.handler._create_badge(self.badge_class) args, kwargs = post.call_args assert args[0] == 'https://example.com/v1/issuer/issuers/test-issuer/badges' assert kwargs['files']['image'][0] == self.badge_class.image.name assert isinstance(kwargs['files']['image'][1], ImageFieldFile) assert kwargs['files']['image'][2] == 'image/png' self.check_headers(kwargs['headers']) assert kwargs['data'] ==\ {'name': 'Test Badge', 'slug': EXAMPLE_SLUG, 'criteria': 'https://example.com/syllabus', 'description': "Yay! It's a test badge."} def test_ensure_badge_created_cache(self): """ Make sure ensure_badge_created doesn't call create_badge if we know the badge is already there. """ BadgrBackend.badges.append(EXAMPLE_SLUG) self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member assert not self.handler._create_badge.called @ddt.unpack @ddt.data( ('badge_class', EXAMPLE_SLUG), ('legacy_badge_class', 'test_slug'), ('no_course_badge_class', 'test_componenttest_slug') ) def test_slugs(self, badge_class_type, slug): assert self.handler._slugify(getattr(self, badge_class_type)) == slug # lint-amnesty, pylint: disable=no-member @patch('requests.get') def test_ensure_badge_created_checks(self, get): response = Mock() response.status_code = 200 get.return_value = response assert 'test_componenttest_slug' not in BadgrBackend.badges self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member assert get.called args, kwargs = get.call_args assert args[0] == ('https://example.com/v1/issuer/issuers/test-issuer/badges/' + EXAMPLE_SLUG) self.check_headers(kwargs['headers']) assert EXAMPLE_SLUG in BadgrBackend.badges assert not self.handler._create_badge.called @patch('requests.get') def test_ensure_badge_created_creates(self, get): response = Mock() response.status_code = 404 get.return_value = response assert EXAMPLE_SLUG not in BadgrBackend.badges self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member assert self.handler._create_badge.called assert self.handler._create_badge.call_args == call(self.badge_class) assert EXAMPLE_SLUG in BadgrBackend.badges @patch('requests.post') def test_badge_creation_event(self, post): result = { 'json': {'id': 'http://www.example.com/example'}, 'image': 'http://www.example.com/example.png', 'badge': 'test_assertion_slug', 'issuer': 'https://example.com/v1/issuer/issuers/test-issuer', } response = Mock() response.json.return_value = result post.return_value = response self.recreate_tracker() self.handler._create_assertion(self.badge_class, self.user, 'https://example.com/irrefutable_proof') # lint-amnesty, pylint: disable=no-member args, kwargs = post.call_args assert args[0] == (('https://example.com/v1/issuer/issuers/test-issuer/badges/' + EXAMPLE_SLUG) + '/assertions') self.check_headers(kwargs['headers']) assertion = BadgeAssertion.objects.get(user=self.user, badge_class__course_id=self.course.location.course_key) assert assertion.data == result assert assertion.image_url == 'http://www.example.com/example.png' assert assertion.assertion_url == 'http://www.example.com/example' assert kwargs['data'] == {'email': 'example@example.com', 'evidence': 'https://example.com/irrefutable_proof'} assert_event_matches({ 'name': 'edx.badge.assertion.created', 'data': { 'user_id': self.user.id, 'course_id': str(self.course.location.course_key), 'enrollment_mode': 'honor', 'assertion_id': assertion.id, 'badge_name': 'Test Badge', 'badge_slug': 'test_slug', 'issuing_component': 'test_component', 'assertion_image_url': 'http://www.example.com/example.png', 'assertion_json_url': 'http://www.example.com/example', 'issuer': 'https://example.com/v1/issuer/issuers/test-issuer', } }, self.get_event())