286 lines
12 KiB
Python
286 lines
12 KiB
Python
"""
|
|
Tests for the Badges app models.
|
|
"""
|
|
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.files.images import ImageFile
|
|
from django.core.files.storage import default_storage
|
|
from django.db.utils import IntegrityError
|
|
from django.test import TestCase
|
|
from django.test.utils import override_settings
|
|
from path import Path
|
|
|
|
from common.djangoapps.student.tests.factories import UserFactory
|
|
from lms.djangoapps.badges.models import (
|
|
BadgeAssertion,
|
|
BadgeClass,
|
|
CourseBadgesDisabledError,
|
|
CourseCompleteImageConfiguration,
|
|
validate_badge_image
|
|
)
|
|
from lms.djangoapps.badges.tests.factories import BadgeAssertionFactory, BadgeClassFactory, RandomBadgeClassFactory
|
|
from lms.djangoapps.certificates.tests.test_models import TEST_DATA_ROOT
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
|
|
|
|
def get_image(name):
|
|
"""
|
|
Get one of the test images from the test data directory.
|
|
"""
|
|
return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png', mode='rb')) # lint-amnesty, pylint: disable=bad-option-value, open-builtin
|
|
|
|
|
|
@override_settings(MEDIA_ROOT=TEST_DATA_ROOT)
|
|
class BadgeImageConfigurationTest(TestCase):
|
|
"""
|
|
Test the validation features of BadgeImageConfiguration.
|
|
"""
|
|
|
|
def tearDown(self): # lint-amnesty, pylint: disable=super-method-not-called
|
|
tmp_path = Path(TEST_DATA_ROOT / 'course_complete_badges')
|
|
Path.rmtree_p(tmp_path)
|
|
|
|
def test_no_double_default(self):
|
|
"""
|
|
Verify that creating two configurations as default is not permitted.
|
|
"""
|
|
CourseCompleteImageConfiguration(mode='test', icon=get_image('good'), default=True).save()
|
|
pytest.raises(ValidationError, CourseCompleteImageConfiguration(mode='test2', icon=get_image('good'),
|
|
default=True).full_clean)
|
|
|
|
def test_runs_validator(self):
|
|
"""
|
|
Verify that the image validator is triggered when cleaning the model.
|
|
"""
|
|
pytest.raises(ValidationError, CourseCompleteImageConfiguration(mode='test2', icon=get_image('unbalanced'))
|
|
.full_clean)
|
|
|
|
|
|
class DummyBackend:
|
|
"""
|
|
Dummy badge backend, used for testing.
|
|
"""
|
|
award = Mock()
|
|
|
|
|
|
@override_settings(MEDIA_ROOT=TEST_DATA_ROOT)
|
|
class BadgeClassTest(ModuleStoreTestCase):
|
|
"""
|
|
Test BadgeClass functionality
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.addCleanup(self.cleanup_uploads)
|
|
|
|
def cleanup_uploads(self):
|
|
"""
|
|
Remove all files uploaded as badges.
|
|
"""
|
|
upload_to = BadgeClass._meta.get_field('image').upload_to
|
|
if default_storage.exists(upload_to):
|
|
(_, files) = default_storage.listdir(upload_to)
|
|
for uploaded_file in files:
|
|
default_storage.delete(upload_to + '/' + uploaded_file)
|
|
|
|
# Need full path to make sure class names line up.
|
|
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.tests.test_models.DummyBackend')
|
|
def test_backend(self):
|
|
"""
|
|
Verify the BadgeClass fetches the backend properly.
|
|
"""
|
|
assert isinstance(BadgeClass().backend, DummyBackend)
|
|
|
|
def test_get_badge_class_preexisting(self):
|
|
"""
|
|
Verify fetching a badge first grabs existing badges.
|
|
"""
|
|
premade_badge_class = BadgeClassFactory.create()
|
|
# Ignore additional parameters. This class already exists.
|
|
badge_class = BadgeClass.get_badge_class(
|
|
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
|
criteria='test', display_name='Testola', image_file_handle=get_image('good')
|
|
)
|
|
# These defaults are set on the factory.
|
|
assert badge_class.criteria == 'https://example.com/syllabus'
|
|
assert badge_class.display_name == 'Test Badge'
|
|
assert badge_class.description == "Yay! It's a test badge."
|
|
# File name won't always be the same.
|
|
assert badge_class.image.path == premade_badge_class.image.path
|
|
|
|
def test_unique_for_course(self):
|
|
"""
|
|
Verify that the course_id is used in fetching existing badges or creating new ones.
|
|
"""
|
|
course_key = CourseFactory.create().location.course_key
|
|
premade_badge_class = BadgeClassFactory.create(course_id=course_key)
|
|
badge_class = BadgeClass.get_badge_class(
|
|
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
|
criteria='test', display_name='Testola', image_file_handle=get_image('good')
|
|
)
|
|
course_badge_class = BadgeClass.get_badge_class(
|
|
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
|
criteria='test', display_name='Testola', image_file_handle=get_image('good'),
|
|
course_id=course_key,
|
|
)
|
|
assert badge_class.id != course_badge_class.id
|
|
assert course_badge_class.id == premade_badge_class.id
|
|
|
|
def test_get_badge_class_course_disabled(self):
|
|
"""
|
|
Verify attempting to fetch a badge class for a course which does not issue badges raises an
|
|
exception.
|
|
"""
|
|
course_key = CourseFactory.create(metadata={'issue_badges': False}).location.course_key
|
|
with pytest.raises(CourseBadgesDisabledError):
|
|
BadgeClass.get_badge_class(
|
|
slug='test_slug', issuing_component='test_component', description='Attempted override',
|
|
criteria='test', display_name='Testola', image_file_handle=get_image('good'),
|
|
course_id=course_key,
|
|
)
|
|
|
|
def test_get_badge_class_create(self):
|
|
"""
|
|
Verify fetching a badge creates it if it doesn't yet exist.
|
|
"""
|
|
badge_class = BadgeClass.get_badge_class(
|
|
slug='new_slug', issuing_component='new_component', description='This is a test',
|
|
criteria='https://example.com/test_criteria', display_name='Super Badge',
|
|
image_file_handle=get_image('good')
|
|
)
|
|
# This should have been saved before being passed back.
|
|
assert badge_class.id
|
|
assert badge_class.slug == 'new_slug'
|
|
assert badge_class.issuing_component == 'new_component'
|
|
assert badge_class.description == 'This is a test'
|
|
assert badge_class.criteria == 'https://example.com/test_criteria'
|
|
assert badge_class.display_name == 'Super Badge'
|
|
assert 'good' in badge_class.image.name.rsplit('/', 1)[(- 1)]
|
|
|
|
def test_get_badge_class_nocreate(self):
|
|
"""
|
|
Test returns None if the badge class does not exist.
|
|
"""
|
|
badge_class = BadgeClass.get_badge_class(
|
|
slug='new_slug', issuing_component='new_component', create=False
|
|
)
|
|
assert badge_class is None
|
|
# Run this twice to verify there wasn't a background creation of the badge.
|
|
badge_class = BadgeClass.get_badge_class(
|
|
slug='new_slug', issuing_component='new_component', description=None,
|
|
criteria=None, display_name=None,
|
|
image_file_handle=None, create=False
|
|
)
|
|
assert badge_class is None
|
|
|
|
def test_get_badge_class_image_validate(self):
|
|
"""
|
|
Verify handing a broken image to get_badge_class raises a validation error upon creation.
|
|
"""
|
|
# TODO Test should be updated, this doc doesn't makes sense, the object eventually gets created
|
|
self.assertRaises(
|
|
ValidationError,
|
|
BadgeClass.get_badge_class,
|
|
slug='new_slug', issuing_component='new_component', description='This is a test',
|
|
criteria='https://example.com/test_criteria', display_name='Super Badge',
|
|
image_file_handle=get_image('unbalanced')
|
|
)
|
|
|
|
def test_get_badge_class_data_validate(self):
|
|
"""
|
|
Verify handing incomplete data for required fields when making a badge class raises an Integrity error.
|
|
"""
|
|
image = get_image('good')
|
|
pytest.raises(IntegrityError, BadgeClass.get_badge_class, slug='new_slug', issuing_component='new_component',
|
|
image_file_handle=image)
|
|
|
|
def test_get_for_user(self):
|
|
"""
|
|
Make sure we can get an assertion for a user if there is one.
|
|
"""
|
|
user = UserFactory.create()
|
|
badge_class = BadgeClassFactory.create()
|
|
assert not badge_class.get_for_user(user)
|
|
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=user)
|
|
assert list(badge_class.get_for_user(user)) == [assertion]
|
|
|
|
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.badgr.BadgrBackend', BADGR_API_TOKEN='test')
|
|
@patch('lms.djangoapps.badges.backends.badgr.BadgrBackend.award')
|
|
def test_award(self, mock_award):
|
|
"""
|
|
Verify that the award command calls the award function on the backend with the right parameters.
|
|
"""
|
|
user = UserFactory.create()
|
|
badge_class = BadgeClassFactory.create()
|
|
badge_class.award(user, evidence_url='http://example.com/evidence')
|
|
assert mock_award.called
|
|
mock_award.assert_called_with(badge_class, user, evidence_url='http://example.com/evidence')
|
|
|
|
def test_runs_validator(self):
|
|
"""
|
|
Verify that the image validator is triggered when cleaning the model.
|
|
"""
|
|
pytest.raises(ValidationError, BadgeClass(slug='test', issuing_component='test2', criteria='test3',
|
|
description='test4', image=get_image('unbalanced')).full_clean)
|
|
|
|
|
|
class BadgeAssertionTest(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the BadgeAssertion model
|
|
"""
|
|
def test_assertions_for_user(self):
|
|
"""
|
|
Verify that grabbing all assertions for a user behaves as expected.
|
|
|
|
This function uses object IDs because for some reason Jenkins trips up
|
|
on its assertCountEqual check here despite the items being equal.
|
|
"""
|
|
user = UserFactory()
|
|
assertions = [BadgeAssertionFactory.create(user=user).id for _i in range(3)]
|
|
course = CourseFactory.create()
|
|
course_key = course.location.course_key
|
|
course_badges = [RandomBadgeClassFactory(course_id=course_key) for _i in range(3)]
|
|
course_assertions = [
|
|
BadgeAssertionFactory.create(user=user, badge_class=badge_class).id for badge_class in course_badges
|
|
]
|
|
assertions.extend(course_assertions)
|
|
assertions.sort()
|
|
assertions_for_user = [badge.id for badge in BadgeAssertion.assertions_for_user(user)]
|
|
assertions_for_user.sort()
|
|
assert assertions_for_user == assertions
|
|
course_scoped_assertions = [
|
|
badge.id for badge in BadgeAssertion.assertions_for_user(user, course_id=course_key)
|
|
]
|
|
course_scoped_assertions.sort()
|
|
assert course_scoped_assertions == course_assertions
|
|
|
|
|
|
class ValidBadgeImageTest(TestCase):
|
|
"""
|
|
Tests the badge image field validator.
|
|
"""
|
|
def test_good_image(self):
|
|
"""
|
|
Verify that saving a valid badge image is no problem.
|
|
"""
|
|
validate_badge_image(get_image('good'))
|
|
|
|
def test_unbalanced_image(self):
|
|
"""
|
|
Verify that setting an image with an uneven width and height raises an error.
|
|
"""
|
|
unbalanced = ImageFile(get_image('unbalanced'))
|
|
self.assertRaises(ValidationError, validate_badge_image, unbalanced)
|
|
|
|
def test_large_image(self):
|
|
"""
|
|
Verify that setting an image that is too big raises an error.
|
|
"""
|
|
large = get_image('large')
|
|
self.assertRaises(ValidationError, validate_badge_image, large)
|