Files
edx-platform/lms/djangoapps/badges/tests/test_models.py
2021-02-25 18:15:27 +05:00

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)