diff --git a/api/outdated/conftest.py b/api/outdated/conftest.py index 3d3a1831..91973d6d 100644 --- a/api/outdated/conftest.py +++ b/api/outdated/conftest.py @@ -1,6 +1,7 @@ from functools import partial import pytest +from django.core.management import call_command from pytest_factoryboy import register from rest_framework.test import APIClient @@ -59,6 +60,18 @@ def client(db, settings, get_claims): return client +@pytest.fixture +def setup_notifications(transactional_db, settings): + settings.NOTIFICATIONS = [ + ("test-foo", 60), + ("test-bar", 10), + ("test-baz", -20), + ] + call_command("update-notifications") + settings.TEMPLATES[0]["DIRS"] = ["outdated/notifications/tests/templates"] + settings.TEMPLATES[0]["APP_DIRS"] = False + + @pytest.fixture(scope="module") def vcr_config(): return { diff --git a/api/outdated/notifications/tests/__init__.py b/api/outdated/notifications/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/outdated/notifications/tests/templates/base.txt b/api/outdated/notifications/tests/templates/base.txt new file mode 100644 index 00000000..55f1e8b5 --- /dev/null +++ b/api/outdated/notifications/tests/templates/base.txt @@ -0,0 +1,8 @@ +{% block subject -%} +{% endblock -%} + +Project: {{project.name}} +Repo: {{project.repo}} + +{% block content -%} +{% endblock %} diff --git a/api/outdated/notifications/tests/templates/test-bar.txt b/api/outdated/notifications/tests/templates/test-bar.txt new file mode 100644 index 00000000..07eba94d --- /dev/null +++ b/api/outdated/notifications/tests/templates/test-bar.txt @@ -0,0 +1,9 @@ +{% extends 'base.txt' %} + +{% block subject %} +bar +{% endblock %} + +{% block content %} +test-bar.txt contents +{% endblock %} diff --git a/api/outdated/notifications/tests/templates/test-baz.txt b/api/outdated/notifications/tests/templates/test-baz.txt new file mode 100644 index 00000000..182a49bc --- /dev/null +++ b/api/outdated/notifications/tests/templates/test-baz.txt @@ -0,0 +1,9 @@ +{% extends 'base.txt' %} + +{% block subject %} +baz +{% endblock %} + +{% block content %} +test-baz.txt contents +{% endblock %} diff --git a/api/outdated/notifications/tests/templates/test-foo.txt b/api/outdated/notifications/tests/templates/test-foo.txt new file mode 100644 index 00000000..85c1ecc9 --- /dev/null +++ b/api/outdated/notifications/tests/templates/test-foo.txt @@ -0,0 +1,9 @@ +{% extends 'base.txt' %} + +{% block subject %} +foo +{% endblock %} + +{% block content %} +test-foo.txt contents +{% endblock %} diff --git a/api/outdated/notifications/tests/test_build_notification_queue.py b/api/outdated/notifications/tests/test_build_notification_queue.py new file mode 100644 index 00000000..0c639c30 --- /dev/null +++ b/api/outdated/notifications/tests/test_build_notification_queue.py @@ -0,0 +1,47 @@ +from datetime import date, timedelta +from unittest.mock import MagicMock + +import pytest +from pytest_mock import MockerFixture + +from .. import models + + +@pytest.mark.parametrize( + "status,called", + (["UNDEFINED", False], ["OUTDATED", True], ["WARNING", True], ["UP-TO-DATE", True]), +) +def test_build_notification_queue_called( + transactional_db, + mocker: MockerFixture, + status, + called, + project_factory, + release_version_factory, + version_factory, +): + build_notification_queue_mock: MagicMock = mocker.patch.object( + models, "build_notification_queue" + ) + release_version = release_version_factory( + undefined=status == "UNDEFINED", + outdated=status == "OUTDATED", + warning=status == "WARNING", + up_to_date=status == "UP-TO-DATE", + ) + version = version_factory(release_version=release_version) + project = project_factory() + assert build_notification_queue_mock.call_count == 0 + project.versioned_dependencies.add(version) + + if called: + build_notification_queue_mock.assert_called_with(project) + other_project = project_factory(versioned_dependencies=[version]) + build_notification_queue_mock.assert_called_with(other_project) + + release_version.end_of_life = date.today() + timedelta(days=2000) + release_version.save() + + assert build_notification_queue_mock.call_count == 4 + else: + assert build_notification_queue_mock.call_count == 0 diff --git a/api/outdated/notifications/tests/test_management_commands.py b/api/outdated/notifications/tests/test_management_commands.py new file mode 100644 index 00000000..e229e39f --- /dev/null +++ b/api/outdated/notifications/tests/test_management_commands.py @@ -0,0 +1,29 @@ +from django.core.management import call_command + +from outdated.notifications.notifier import Notifier + + +def test_notify( + db, + project, + maintainer_factory, + version_factory, + release_version_factory, + capsys, + mocker, +): + call_command("notify", project.name) + stdout, _ = capsys.readouterr() + assert stdout == f"Skipped {project.name} (no-maintainers)\n" + maintainer_factory(project=project) + call_command("notify", project.name) + notify_mocker = mocker.spy(Notifier, "__init__") + stdout, _ = capsys.readouterr() + assert stdout == "" + notify_mocker.assert_not_called() + project.versioned_dependencies.set( + [version_factory(release_version=release_version_factory(warning=True))] + ) + project.save() + call_command("notify", project.name) + notify_mocker.assert_called_once() diff --git a/api/outdated/notifications/tests/test_notifier.py b/api/outdated/notifications/tests/test_notifier.py new file mode 100644 index 00000000..7181fb20 --- /dev/null +++ b/api/outdated/notifications/tests/test_notifier.py @@ -0,0 +1,58 @@ +from datetime import date, timedelta +from typing import Optional + +import pytest + +from outdated.notifications.notifier import Notifier +from outdated.outdated.models import Maintainer + + +@pytest.mark.parametrize("nonprimary_maintainers", [False, True]) +@pytest.mark.parametrize( + "days_until_outdated,template,sent", + [ + (200, None, False), + (60, "test-foo", True), + (50, "test-foo", True), + (10, "test-bar", True), + (-20, "test-baz", True), + ], +) +def test_send_notification( + setup_notifications, + days_until_outdated: int, + template: Optional[str], + sent: bool, + nonprimary_maintainers: bool, + maintainer, + maintainer_factory, + mailoutbox, + version_factory, + release_version_factory, +): + project = maintainer.project + release_version = release_version_factory( + end_of_life=date.today() + timedelta(days=days_until_outdated) + ) + version = version_factory(release_version=release_version) + project.versioned_dependencies.add(version) + project.save() + if nonprimary_maintainers: + maintainer_factory(project=project) + maintainer_factory(project=project) + maintainer_factory(project=project) + nonprimary_maintainers = list(Maintainer.objects.filter(is_primary=False)) + notification_queue = list(project.notification_queue.all()) + Notifier(project).notify() + if sent: + mail = mailoutbox[0] + assert mail.subject == template.replace("test-", "") + assert ( + mail.body + == f"Project: {project.name}\nRepo: {project.repo}\n\n{template}.txt contents\n" + ) + assert mail.to[0] == maintainer.user.email + assert mail.cc == [m.user.email for m in nonprimary_maintainers] + assert notification_queue[1:] == list(project.notification_queue.all()) + else: + assert not mailoutbox