diff --git a/api/tests/test_admin.py b/api/tests/test_admin.py new file mode 100644 index 00000000..134484ee --- /dev/null +++ b/api/tests/test_admin.py @@ -0,0 +1,24 @@ +import pytest +from django.contrib.admin import AdminSite +from django.http import HttpRequest +from model_bakery import baker +from pytest_django.asserts import assertNumQueries + +from api.admin import ApiClientAdmin +from api.models import ApiClient + + +@pytest.mark.django_db +def test_reset_hmac_action(): + client_admin = ApiClientAdmin(ApiClient, AdminSite()) + api_clients = baker.make(ApiClient, _quantity=4, _bulk_create=True) + old_hmac_keys = [c.hmac_key for c in api_clients] + with assertNumQueries(2): + qs = ApiClient.objects.filter(id__in=[c.id for c in api_clients[2:4]]) + client_admin.reset_hmac_key(HttpRequest(), qs) + for c in api_clients: + c.refresh_from_db() + assert api_clients[0].hmac_key == old_hmac_keys[0] + assert api_clients[1].hmac_key == old_hmac_keys[1] + assert api_clients[2].hmac_key != old_hmac_keys[2] + assert api_clients[3].hmac_key != old_hmac_keys[3] diff --git a/api/tests/test_api_client_controller.py b/api/tests/test_api_client_controller.py new file mode 100644 index 00000000..6e25910d --- /dev/null +++ b/api/tests/test_api_client_controller.py @@ -0,0 +1,18 @@ +import pytest +from django.test import Client +from django.urls import reverse +from model_bakery import baker + +from api.hashers import generate_key +from api.models import ApiClient, ApiKey +from api.schemas import ApiClientSchema + + +@pytest.mark.django_db +def test_api_client_controller(client: Client): + key, hashed = generate_key() + api_client = baker.make(ApiClient) + baker.make(ApiKey, client=api_client, hashed_key=hashed) + res = client.get(reverse("api:api-client-infos"), headers={"X-APIKey": key}) + assert res.status_code == 200 + assert res.json() == ApiClientSchema.from_orm(api_client).model_dump() diff --git a/api/tests/test_client.py b/api/tests/test_client.py new file mode 100644 index 00000000..b813b06b --- /dev/null +++ b/api/tests/test_client.py @@ -0,0 +1,59 @@ +import pytest +from django.contrib.auth.models import Permission +from django.test import TestCase +from model_bakery import baker + +from api.models import ApiClient +from core.models import Group + + +class TestClientPermissions(TestCase): + @classmethod + def setUpTestData(cls): + cls.api_client = baker.make(ApiClient) + cls.perms = baker.make(Permission, _quantity=10, _bulk_create=True) + cls.api_client.groups.set( + [ + baker.make(Group, permissions=cls.perms[0:3]), + baker.make(Group, permissions=cls.perms[3:5]), + ] + ) + cls.api_client.client_permissions.set( + [cls.perms[3], cls.perms[5], cls.perms[6], cls.perms[7]] + ) + + def test_all_permissions(self): + assert self.api_client.all_permissions == { + f"{p.content_type.app_label}.{p.codename}" for p in self.perms[0:8] + } + + def test_has_perm(self): + assert self.api_client.has_perm( + f"{self.perms[1].content_type.app_label}.{self.perms[1].codename}" + ) + assert not self.api_client.has_perm( + f"{self.perms[9].content_type.app_label}.{self.perms[9].codename}" + ) + + def test_has_perms(self): + assert self.api_client.has_perms( + [ + f"{self.perms[1].content_type.app_label}.{self.perms[1].codename}", + f"{self.perms[2].content_type.app_label}.{self.perms[2].codename}", + ] + ) + assert not self.api_client.has_perms( + [ + f"{self.perms[1].content_type.app_label}.{self.perms[1].codename}", + f"{self.perms[9].content_type.app_label}.{self.perms[9].codename}", + ], + ) + + +@pytest.mark.django_db +def test_reset_hmac_key(): + client = baker.make(ApiClient) + original_key = client.hmac_key + client.reset_hmac(commit=True) + assert len(client.hmac_key) == len(original_key) + assert client.hmac_key != original_key diff --git a/api/tests/test_third_party_auth.py b/api/tests/test_third_party_auth.py new file mode 100644 index 00000000..1d69ef0f --- /dev/null +++ b/api/tests/test_third_party_auth.py @@ -0,0 +1,111 @@ +from unittest import mock +from unittest.mock import Mock + +from django.db.models import Max +from django.test import TestCase +from django.urls import reverse +from model_bakery import baker +from pytest_django.asserts import assertRedirects + +from api.models import ApiClient, get_hmac_key +from core.baker_recipes import subscriber_user +from core.utils import hmac_hexdigest + + +def mocked_post(*, ok: bool): + class MockedResponse(Mock): + @property + def ok(self): + return ok + + def mocked(): + return MockedResponse() + + return mocked + + +class TestThirdPartyAuth(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = subscriber_user.make() + cls.api_client = baker.make(ApiClient) + + def setUp(self): + self.query = { + "client_id": self.api_client.id, + "third_party_app": "app", + "cgu_link": "https://foobar.fr/", + "username": "bibou", + "callback_url": "https://callback.fr/", + } + self.query["signature"] = hmac_hexdigest(self.api_client.hmac_key, self.query) + self.callback_data = {"user_id": self.user.id} + self.callback_data["signature"] = hmac_hexdigest( + self.api_client.hmac_key, self.callback_data + ) + + def test_auth_ok(self): + self.client.force_login(self.user) + res = self.client.get(reverse("api-link:third-party-auth", query=self.query)) + assert res.status_code == 200 + with mock.patch("requests.post", new_callable=mocked_post(ok=True)) as mocked: + res = self.client.post( + reverse("api-link:third-party-auth"), + data={"cgu_accepted": True, "is_username_valid": True, **self.query}, + ) + mocked.assert_called_once_with( + self.query["callback_url"], data=self.callback_data + ) + assertRedirects( + res, + reverse("api-link:third-party-auth-result", kwargs={"result": "success"}), + ) + + def test_callback_error(self): + """Test that the user see the failure page if the callback request failed.""" + self.client.force_login(self.user) + with mock.patch("requests.post", new_callable=mocked_post(ok=False)) as mocked: + res = self.client.post( + reverse("api-link:third-party-auth"), + data={"cgu_accepted": True, "is_username_valid": True, **self.query}, + ) + mocked.assert_called_once_with( + self.query["callback_url"], data=self.callback_data + ) + assertRedirects( + res, + reverse("api-link:third-party-auth-result", kwargs={"result": "failure"}), + ) + + def test_wrong_signature(self): + """Test that a 403 is raised if the signature of the query is wrong.""" + self.client.force_login(subscriber_user.make()) + new_key = get_hmac_key() + del self.query["signature"] + self.query["signature"] = hmac_hexdigest(new_key, self.query) + res = self.client.get(reverse("api-link:third-party-auth", query=self.query)) + assert res.status_code == 403 + + def test_cgu_not_accepted(self): + self.client.force_login(self.user) + res = self.client.get(reverse("api-link:third-party-auth", query=self.query)) + assert res.status_code == 200 + res = self.client.post(reverse("api-link:third-party-auth"), data=self.query) + assert res.status_code == 200 # no redirect means invalid form + res = self.client.post( + reverse("api-link:third-party-auth"), + data={"cgu_accepted": False, "is_username_valid": False, **self.query}, + ) + assert res.status_code == 200 + + def test_invalid_client(self): + self.query["client_id"] = ApiClient.objects.aggregate(res=Max("id"))["res"] + 1 + res = self.client.get(reverse("api-link:third-party-auth", query=self.query)) + assert res.status_code == 403 + + def test_missing_parameter(self): + """Test that a 403 is raised if there is a missing parameter.""" + del self.query["username"] + self.query["signature"] = hmac_hexdigest(self.api_client.hmac_key, self.query) + res = self.client.get(reverse("api-link:third-party-auth", query=self.query)) + assert res.status_code == 403