diff --git a/credentials/apps/badges/tests/test_admin_forms.py b/credentials/apps/badges/tests/test_admin_forms.py index db5152331..a528febbc 100644 --- a/credentials/apps/badges/tests/test_admin_forms.py +++ b/credentials/apps/badges/tests/test_admin_forms.py @@ -2,10 +2,11 @@ from django import forms from django.contrib.sites.models import Site -from django.test import TestCase -from django.utils.translation import gettext as _ +from django.test import TestCase, override_settings +from unittest.mock import MagicMock, patch -from credentials.apps.badges.admin_forms import BadgePenaltyForm +from credentials.apps.badges.admin_forms import BadgePenaltyForm, CredlyOrganizationAdminForm +from credentials.apps.badges.credly.exceptions import CredlyAPIError from credentials.apps.badges.models import BadgeRequirement, BadgeTemplate @@ -70,4 +71,110 @@ def test_clean_requirements_different_template(self): with self.assertRaises(forms.ValidationError) as cm: form.clean() - self.assertEqual(str(cm.exception), "['All requirements must belong to the same template.']") + self.assertEqual( + str(cm.exception), "['All requirements must belong to the same template.']" + ) + + @override_settings(BADGES_CONFIG={"credly": {"ORGANIZATIONS": {}}}) + def test_clean(self): + form = CredlyOrganizationAdminForm() + form.cleaned_data = { + "uuid": "test_uuid", + "api_key": "test_api_key", + } + + with patch( + "credentials.apps.badges.models.CredlyOrganization.get_preconfigured_organizations" + ) as mock_get_orgs: + mock_get_orgs.return_value = {} + + with patch( + "credentials.apps.badges.admin_forms.CredlyAPIClient" + ) as mock_client: + mock_client.return_value = MagicMock() + + form.clean() + + mock_get_orgs.assert_called_once() + mock_client.assert_called_once_with("test_uuid", "test_api_key") + + @override_settings(BADGES_CONFIG={"credly": {"ORGANIZATIONS": {"test_uuid": "test_api_key"}}}) + def test_clean_with_configured_organization(self): + form = CredlyOrganizationAdminForm() + form.cleaned_data = { + "uuid": "test_uuid", + "api_key": None, + } + + with patch( + "credentials.apps.badges.models.CredlyOrganization.get_preconfigured_organizations" + ) as mock_get_orgs: + mock_get_orgs.return_value = {"test_uuid": "test_org"} + + with patch( + "credentials.apps.badges.admin_forms.CredlyAPIClient" + ) as mock_client: + mock_client.return_value = MagicMock() + + form.clean() + + mock_get_orgs.assert_called_once() + mock_client.assert_called_once_with("test_uuid", "test_api_key") + + def test_clean_with_invalid_organization(self): + form = CredlyOrganizationAdminForm() + form.cleaned_data = { + "uuid": "invalid_uuid", + "api_key": "test_api_key", + } + + with patch( + "credentials.apps.badges.models.CredlyOrganization.get_preconfigured_organizations" + ) as mock_get_orgs: + mock_get_orgs.return_value = {"test_uuid": "test_org"} + + with self.assertRaises(forms.ValidationError) as cm: + form.clean() + + self.assertIn("You specified an invalid authorization token.", str(cm.exception)) + + def test_clean_cannot_provide_api_key_for_configured_organization(self): + form = CredlyOrganizationAdminForm() + form.cleaned_data = { + "uuid": "test_uuid", + "api_key": "test_api_key", + } + + with patch( + "credentials.apps.badges.models.CredlyOrganization.get_preconfigured_organizations" + ) as mock_get_orgs: + mock_get_orgs.return_value = {"test_uuid": "test_org"} + + with self.assertRaises(forms.ValidationError) as cm: + form.clean() + + self.assertEqual( + str(cm.exception), + '["You can\'t provide an API key for a configured organization."]', + ) + + def test_ensure_organization_exists(self): + form = CredlyOrganizationAdminForm() + api_client = MagicMock() + api_client.fetch_organization.return_value = {"data": {"org_id": "test_org_id"}} + + form._ensure_organization_exists(api_client) + + api_client.fetch_organization.assert_called_once() + self.assertEqual(form.api_data, {"org_id": "test_org_id"}) + + def test_ensure_organization_exists_with_error(self): + form = CredlyOrganizationAdminForm() + api_client = MagicMock() + api_client.fetch_organization.side_effect = CredlyAPIError("API Error") + + with self.assertRaises(forms.ValidationError) as cm: + form._ensure_organization_exists(api_client) + + api_client.fetch_organization.assert_called_once() + self.assertEqual(str(cm.exception), "['API Error']") diff --git a/credentials/apps/badges/tests/test_api_client.py b/credentials/apps/badges/tests/test_api_client.py new file mode 100644 index 000000000..e565729e2 --- /dev/null +++ b/credentials/apps/badges/tests/test_api_client.py @@ -0,0 +1,42 @@ +from unittest import mock +from django.test import TestCase +from requests.models import Response +from credentials.apps.badges.credly.api_client import CredlyAPIClient +from credentials.apps.badges.credly.exceptions import CredlyAPIError +from credentials.apps.badges.models import CredlyOrganization +from credentials.apps.badges.credly.exceptions import CredlyError + +class CredlyApiClientTestCase(TestCase): + def setUp(self): + self.api_client = CredlyAPIClient("test_organization_id", "test_api_key") + + def test_get_organization_nonexistent(self): + with mock.patch("credentials.apps.badges.credly.api_client.CredlyOrganization.objects.get") as mock_get: + mock_get.side_effect = CredlyOrganization.DoesNotExist + with self.assertRaises(CredlyError) as cm: + self.api_client._get_organization("nonexistent_organization_id") + self.assertEqual(str(cm.exception), "CredlyOrganization with the uuid nonexistent_organization_id does not exist!") + + def test_perform_request(self): + with mock.patch("credentials.apps.badges.credly.api_client.requests.request") as mock_request: + mock_response = mock.Mock() + mock_response.json.return_value = {"key": "value"} + mock_request.return_value = mock_response + result = self.api_client.perform_request("GET", "/api/endpoint") + mock_request.assert_called_once_with("GET", "https://api.credly.com/api/endpoint", headers=self.api_client._get_headers(), json=None) + self.assertEqual(result, {"key": "value"}) + + def test_raise_for_error_success(self): + response = mock.Mock(spec=Response) + response.status_code = 200 + self.api_client._raise_for_error(response) + + def test_raise_for_error_error(self): + response = mock.Mock(spec=Response) + response.status_code = 404 + response.text = "Not Found" + response.raise_for_status.side_effect = CredlyAPIError(f"Credly API: {response.text} ({response.status_code})") + + with self.assertRaises(CredlyAPIError) as cm: + self.api_client._raise_for_error(response) + self.assertEqual(str(cm.exception), "Credly API: Not Found (404)") diff --git a/credentials/apps/badges/tests/test_services.py b/credentials/apps/badges/tests/test_services.py index ae5e9dfbc..3caa8cf60 100644 --- a/credentials/apps/badges/tests/test_services.py +++ b/credentials/apps/badges/tests/test_services.py @@ -6,6 +6,7 @@ from opaque_keys.edx.keys import CourseKey from openedx_events.learning.data import CourseData, CoursePassingStatusData, UserData, UserPersonalData +from credentials.apps.badges.exceptions import BadgesProcessingError from credentials.apps.badges.models import ( BadgePenalty, BadgeProgress, @@ -631,3 +632,15 @@ class TestIdentifyUser(TestCase): def test_identify_user(self): username = identify_user(event_type=COURSE_PASSING_EVENT, event_payload=COURSE_PASSING_DATA) self.assertEqual(username, "test_username") + + def test_identify_user_not_found(self): + event_type = "unknown_event_type" + event_payload = None + + with self.assertRaises(BadgesProcessingError) as cm: + identify_user(event_type="unknown_event_type", event_payload=event_payload) + + self.assertEqual( + str(cm.exception), + f"User data cannot be found (got: None): {event_payload}. Does event {event_type} include user data at all?" + )