Skip to content

Commit

Permalink
fix: update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pkulkark committed Jan 23, 2025
1 parent 6aeffd7 commit 608465f
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 47 deletions.
9 changes: 5 additions & 4 deletions openedx_certificates/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage, default_storage

from pypdf import PdfReader, PdfWriter
from pypdf.constants import UserAccessPermissions
from reportlab.pdfbase import pdfmetrics
Expand Down Expand Up @@ -168,8 +169,8 @@ def _save_certificate(certificate: PdfWriter, certificate_uuid: UUID) -> str:


def generate_pdf_certificate(
resource_id,
resource_type,
resource_id: str,
resource_type: str,
user: User,
certificate_uuid: UUID,
options: dict[str, Any],
Expand Down Expand Up @@ -209,10 +210,10 @@ def generate_pdf_certificate(
try:
LearningPath = apps.get_model('learning_paths', 'LearningPath')
return LearningPath.objects.get(uuid=resource_id).display_name
except Exception:
except Exception: # noqa: BLE001
return ""
else:
raise ValueError(f"Unsupported resource type: {resource_type}")
raise ValueError('Unsupported resource type: {}'.format(resource_type))

# Get template from the ExternalCertificateAsset.
# HACK: We support two-line strings by using a semicolon as a separator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
class Migration(migrations.Migration):

dependencies = [
('django_celery_beat', '0019_alter_periodictasks_options'),
('openedx_certificates', '0001_initial'),
]

Expand Down
4 changes: 2 additions & 2 deletions openedx_certificates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,13 @@ def get_resource_name(self):
"""Retrieve the name of the resource based on its type."""
if self.resource_type == self.ResourceTypes.COURSE:
return get_course_name(self.resource_id)
elif self.resource_type == self.ResourceTypes.LEARNING_PATH:
elif self.resource_type == self.ResourceTypes.LEARNING_PATH: # noqa: RET505
if not apps.is_installed('learning_paths'):
return ""
try:
LearningPath = apps.get_model('learning_paths', 'LearningPath')
return LearningPath.objects.get(uuid=self.resource_id).display_name
except Exception:
except Exception: # noqa: BLE001
return ""
else:
return ""
Expand Down
74 changes: 53 additions & 21 deletions tests/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,33 +63,33 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas


@pytest.mark.parametrize(
("course_name", "options", "expected"),
("resource_name", "options", "expected"),
[
('Programming 101', {}, {}), # No options - use default coordinates and colors.
(
'Programming 101',
{
'name_y': 250,
'course_name_y': 200,
'resource_name_y': 200,
'issue_date_y': 150,
'name_color': '123',
'course_name_color': '#9B192A',
'resource_name_color': '#9B192A',
'issue_date_color': '#f59a8e',
},
{
'name_color': (17 / 255, 34 / 255, 51 / 255),
'course_name_color': (155 / 255, 25 / 255, 42 / 255),
'resource_name_color': (155 / 255, 25 / 255, 42 / 255),
'issue_date_color': (245 / 255, 154 / 255, 142 / 255),
},
), # Custom coordinates and colors.
('Programming\n101\nAdvanced Programming', {}, {}), # Multiline course name.
],
)
@patch('openedx_certificates.generators.canvas.Canvas', return_value=Mock(stringWidth=Mock(return_value=10)))
def test_write_text_on_template(mock_canvas_class: Mock, course_name: str, options: dict[str, int], expected: dict):
def test_write_text_on_template(mock_canvas_class: Mock, resource_name: str, options: dict[str, int], expected: dict):
"""Test the _write_text_on_template function."""
username = 'John Doe'
course_name = 'Programming 101'
resource_name = 'Programming 101'
template_height = 300
template_width = 200
font = 'Helvetica'
Expand All @@ -104,7 +104,7 @@ def test_write_text_on_template(mock_canvas_class: Mock, course_name: str, optio

# Call the function with test parameters and mocks
with patch('openedx_certificates.generators.get_localized_certificate_date', return_value=test_date):
_write_text_on_template(template_mock, font, username, course_name, options)
_write_text_on_template(template_mock, font, username, resource_name, options)

# Verifying that Canvas was the correct pagesize.
# Use `call_args_list` to ignore the first argument, which is an instance of io.BytesIO.
Expand All @@ -116,18 +116,18 @@ def test_write_text_on_template(mock_canvas_class: Mock, course_name: str, optio
# Expected coordinates for drawString method, based on fixed stringWidth
expected_name_x = (template_width - string_width) / 2
expected_name_y = options.get('name_y', 290)
expected_course_name_x = (template_width - string_width) / 2
expected_course_name_y = options.get('course_name_y', 220)
expected_resource_name_x = (template_width - string_width) / 2
expected_resource_name_y = options.get('resource_name_y', 220)
expected_issue_date_x = (template_width - string_width) / 2
expected_issue_date_y = options.get('issue_date_y', 120)

# Expected colors for setFillColorRGB method
expected_name_color = expected.get('name_color', (0, 0, 0))
expected_course_name_color = expected.get('course_name_color', (0, 0, 0))
expected_resource_name_color = expected.get('resource_name_color', (0, 0, 0))
expected_issue_date_color = expected.get('issue_date_color', (0, 0, 0))

# The number of calls to drawString should be 2 (name and issue date) + number of lines in course name.
assert canvas_object.drawString.call_count == 3 + course_name.count('\n')
assert canvas_object.drawString.call_count == 3 + resource_name.count('\n')

# Check the calls to setFont, setFillColorRGB and drawString methods on Canvas object
assert canvas_object.setFont.call_args_list[0] == call(font, 32)
Expand All @@ -136,16 +136,16 @@ def test_write_text_on_template(mock_canvas_class: Mock, course_name: str, optio
assert mock_canvas_class.return_value.stringWidth.mock_calls[0][1] == (username,)

assert canvas_object.setFont.call_args_list[1] == call(font, 28)
assert canvas_object.setFillColorRGB.call_args_list[1] == call(*expected_course_name_color)
assert canvas_object.setFillColorRGB.call_args_list[1] == call(*expected_resource_name_color)

assert canvas_object.setFont.call_args_list[2] == call(font, 12)
assert canvas_object.setFillColorRGB.call_args_list[2] == call(*expected_issue_date_color)

for line_number, line in enumerate(course_name.split('\n')):
for line_number, line in enumerate(resource_name.split('\n')):
assert mock_canvas_class.return_value.stringWidth.mock_calls[line_number + 1][1] == (line,)
assert canvas_object.drawString.mock_calls[1 + line_number][1] == (
expected_course_name_x,
expected_course_name_y - (line_number * 28 * 1.1),
expected_resource_name_x,
expected_resource_name_y - (line_number * 28 * 1.1),
line,
)

Expand Down Expand Up @@ -216,8 +216,40 @@ def test_save_certificate(mock_contentfile: Mock, mock_token_hex: Mock, storage:
assert url == f'https://example2.com/{certificate_uuid}.pdf'


@override_settings(INSTALLED_APPS=['learning_paths'])
@patch('openedx_certificates.generators.apps.get_model')
@patch('openedx_certificates.generators.ExternalCertificateAsset.get_asset_by_slug')
@patch('openedx_certificates.generators._write_text_on_template')
@patch('openedx_certificates.generators._save_certificate')
def test_generate_pdf_certificate_for_learning_path(
mock_save_certificate: Mock,
mock_write_text_on_template: Mock,
mock_get_asset_by_slug: Mock,
mock_get_model: Mock,
):
"""Test generate_pdf_certificate function for Learning Paths."""
resource_id = "learning-path-uuid"
resource_type = "learning_path"
user = Mock()
certificate_uuid = uuid4()
options = {"template": "template_slug", "resource_name": "Learning Path Name"}
mock_get_asset_by_slug.return_value = Mock(open=Mock(return_value=io.BytesIO()))

learning_path_mock = Mock(display_name="Learning Path Name")
mock_get_model.return_value.objects.get.return_value = learning_path_mock

result = generate_pdf_certificate(resource_id, resource_type, user, certificate_uuid, options)

assert result == mock_save_certificate.return_value
mock_get_model.assert_called_once_with('learning_paths', 'LearningPath')
mock_get_model.return_value.objects.get.assert_called_once_with(uuid=resource_id)
mock_get_asset_by_slug.assert_called_once_with(options["template"])
mock_write_text_on_template.assert_called_once()
mock_save_certificate.assert_called_once()


@pytest.mark.parametrize(
("course_name", "options", "expected_template_slug", "expected_course_name"),
("resource_name", "options", "expected_template_slug", "expected_resource_name"),
[
# Default.
('Test Course', {'template': 'template_slug'}, 'template_slug', 'Test Course'),
Expand Down Expand Up @@ -266,22 +298,22 @@ def test_generate_pdf_certificate( # noqa: PLR0913
mock_get_course_name: Mock,
mock_get_user_name: Mock,
mock_get_asset_by_slug: Mock,
course_name: str,
resource_name: str,
options: dict[str, str],
expected_template_slug: str,
expected_course_name: str,
expected_resource_name: str,
):
"""Test the generate_pdf_certificate function."""
course_id = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
user = Mock()
mock_get_course_name.return_value = course_name
mock_get_course_name.return_value = resource_name

result = generate_pdf_certificate(course_id, user, Mock(), options)

assert result == 'certificate_url'
mock_get_asset_by_slug.assert_called_with(expected_template_slug)
mock_get_user_name.assert_called_once_with(user)
if options.get('course_name'):
if options.get('resource_name'):
mock_get_course_name.assert_not_called()
else:
mock_get_course_name.assert_called_once_with(course_id)
Expand All @@ -291,7 +323,7 @@ def test_generate_pdf_certificate( # noqa: PLR0913

mock_write_text_on_template.assert_called_once()
_, args, _kwargs = mock_write_text_on_template.mock_calls[0]
assert args[-2] == expected_course_name
assert args[-2] == expected_resource_name
assert args[-1] == options

mock_save_certificate.assert_called_once()
38 changes: 19 additions & 19 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from openedx_certificates.exceptions import CertificateGenerationError
from openedx_certificates.models import (
ExternalCertificate,
ExternalCertificateCourseConfiguration,
ExternalCertificateConfiguration,
ExternalCertificateType,
)
from test_utils.factories import UserFactory
Expand Down Expand Up @@ -77,8 +77,8 @@ def test_clean_with_invalid_function(self):
)


class TestExternalCertificateCourseConfiguration:
"""Tests for the ExternalCertificateCourseConfiguration model."""
class TestExternalCertificateConfiguration:
"""Tests for the ExternalCertificateConfiguration model."""

def setup_method(self):
"""Prepare the test data."""
Expand All @@ -87,8 +87,8 @@ def setup_method(self):
retrieval_func="test_models._mock_retrieval_func",
generation_func="test_models._mock_generation_func",
)
self.course_config = ExternalCertificateCourseConfiguration(
course_id="course-v1:TestX+T101+2023",
self.course_config = ExternalCertificateConfiguration(
resource_id="course-v1:TestX+T101+2023",
certificate_type=self.certificate_type,
)

Expand Down Expand Up @@ -123,23 +123,23 @@ def test_periodic_task_deletion_removes_the_configuration(self):
assert PeriodicTask.objects.count() == 1

self.course_config.periodic_task.delete()
assert not ExternalCertificateCourseConfiguration.objects.exists()
assert not ExternalCertificateConfiguration.objects.exists()

@pytest.mark.django_db()
@pytest.mark.parametrize(
("deleted_model", "verified_model"),
[
(ExternalCertificateCourseConfiguration, PeriodicTask), # `post_delete` signal.
(PeriodicTask, ExternalCertificateCourseConfiguration), # Cascade deletion of the `OneToOneField`.
(ExternalCertificateConfiguration, PeriodicTask), # `post_delete` signal.
(PeriodicTask, ExternalCertificateConfiguration), # Cascade deletion of the `OneToOneField`.
],
)
def test_bulk_delete(self, deleted_model: type[Model], verified_model: type[Model]):
"""Test that the bulk deletion of configurations removes the periodic tasks (and vice versa)."""
self.certificate_type.save()
self.course_config.save()

ExternalCertificateCourseConfiguration(
course_id="course-v1:TestX+T101+2024",
ExternalCertificateConfiguration(
resource_id="course-v1:TestX+T101+2024",
certificate_type=self.certificate_type,
).save()
assert PeriodicTask.objects.count() == 2
Expand All @@ -163,7 +163,7 @@ def test_filter_out_user_ids_with_certificates(self):
self.course_config.save()

cert_data = {
"course_id": self.course_config.course_id,
"resource_id": self.course_config.course_id,
"certificate_type": self.certificate_type.name,
}

Expand Down Expand Up @@ -211,7 +211,7 @@ def test_generate_certificate_for_user(self, mock_send_email: Mock):
self.course_config.generate_certificate_for_user(user.id, task_id)
assert ExternalCertificate.objects.filter(
user_id=user.id,
course_id=self.course_config.course_id,
resource_id=self.course_config.course_id,
certificate_type=self.certificate_type,
user_full_name=f"{user.first_name} {user.last_name}",
status=ExternalCertificate.Status.AVAILABLE,
Expand All @@ -226,15 +226,15 @@ def test_generate_certificate_for_user(self, mock_send_email: Mock):
user = UserFactory.create(is_active=False)

self.course_config.generate_certificate_for_user(user.id, task_id)
assert ExternalCertificate.objects.filter(course_id=self.course_config.course_id).count() == 2
assert ExternalCertificate.objects.filter(resource_id=self.course_config.course_id).count() == 2
mock_send_email.assert_called_once()

user = UserFactory.create()
user.set_unusable_password()
user.save()

self.course_config.generate_certificate_for_user(user.id, task_id)
assert ExternalCertificate.objects.filter(course_id=self.course_config.course_id).count() == 3
assert ExternalCertificate.objects.filter(resource_id=self.course_config.course_id).count() == 3
mock_send_email.assert_called_once()

@pytest.mark.django_db()
Expand All @@ -245,7 +245,7 @@ def test_generate_certificate_for_user_update_existing(self, mock_send_email: Mo

ExternalCertificate.objects.create(
user_id=user.id,
course_id=self.course_config.course_id,
resource_id=self.course_config.course_id,
certificate_type=self.certificate_type,
user_full_name="Random Name",
status=ExternalCertificate.Status.ERROR,
Expand All @@ -256,7 +256,7 @@ def test_generate_certificate_for_user_update_existing(self, mock_send_email: Mo
self.course_config.generate_certificate_for_user(user.id)
assert ExternalCertificate.objects.filter(
user_id=user.id,
course_id=self.course_config.course_id,
resource_id=self.course_config.course_id,
certificate_type=self.certificate_type,
user_full_name=f"{user.first_name} {user.last_name}",
status=ExternalCertificate.Status.AVAILABLE,
Expand Down Expand Up @@ -285,7 +285,7 @@ def mock_func_raise_exception(*_args, **_kwargs):
assert 'Failed to generate the' in str(exc.value)
assert ExternalCertificate.objects.filter(
user_id=user.id,
course_id=self.course_config.course_id,
resource_id=self.course_config.course_id,
certificate_type=self.certificate_type,
user_full_name=f"{user.first_name} {user.last_name}",
status=ExternalCertificate.Status.ERROR,
Expand All @@ -303,7 +303,7 @@ def setup_method(self):
uuid=uuid4(),
user_id=1,
user_full_name='Test User',
course_id='course-v1:TestX+T101+2023',
resource_id='course-v1:TestX+T101+2023',
certificate_type='Test Type',
status=ExternalCertificate.Status.GENERATING,
download_url='http://www.test.com',
Expand All @@ -322,7 +322,7 @@ def test_unique_together_constraint(self):
"uuid": uuid4(),
"user_id": 1,
"user_full_name": 'Test User 2',
"course_id": 'course-v1:TestX+T101+2023',
"resource_id": 'course-v1:TestX+T101+2023',
"certificate_type": 'Test Type',
"status": ExternalCertificate.Status.GENERATING,
"download_url": 'http://www.test2.com',
Expand Down

0 comments on commit 608465f

Please sign in to comment.