From 8e6b6318c9bfc1c84a33e134416138683f2c2b3c Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Fri, 15 Nov 2024 15:25:45 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(service=5Fproviders)=20add=20API=20en?= =?UTF-8?q?dpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allow to display service providers in the frontend. Not used yet, but will allow to manage organization and teams related service providers. --- src/backend/core/api/serializers.py | 9 ++ src/backend/core/api/viewsets.py | 47 ++++++++++ src/backend/core/factories.py | 15 +++ .../{ => resource_server_api}/conftest.py | 0 .../core/tests/service_providers/__init__.py | 3 + .../test_core_api_service_providers_list.py | 91 +++++++++++++++++++ ...est_core_api_service_providers_retrieve.py | 85 +++++++++++++++++ src/backend/people/api_urls.py | 3 + 8 files changed, 253 insertions(+) rename src/backend/core/tests/{ => resource_server_api}/conftest.py (100%) create mode 100644 src/backend/core/tests/service_providers/__init__.py create mode 100644 src/backend/core/tests/service_providers/test_core_api_service_providers_list.py create mode 100644 src/backend/core/tests/service_providers/test_core_api_service_providers_retrieve.py diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index f847ddf..47b5808 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -285,3 +285,12 @@ class InvitationSerializer(serializers.ModelSerializer): attrs["team_id"] = team_id attrs["issuer"] = user return attrs + + +class ServiceProviderSerializer(serializers.ModelSerializer): + """Serialize service providers.""" + + class Meta: + model = models.ServiceProvider + fields = ["id", "audience_id", "name"] + read_only_fields = ["id", "audience_id"] diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index f055892..bb412b2 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -494,3 +494,50 @@ class ConfigView(views.APIView): dict_settings[setting] = getattr(settings, setting) return response.Response(dict_settings) + + +class ServiceProviderFilter(filters.BaseFilterBackend): + """ + Filter service providers. + """ + + def filter_queryset(self, request, queryset, view): + """ + Filter service providers by audience or name. + """ + if name := request.GET.get("name"): + queryset = queryset.filter(name__icontains=name) + if audience_id := request.GET.get("audience_id"): + queryset = queryset.filter(audience_id=audience_id) + return queryset + + +class ServiceProviderViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): + """ + API ViewSet for all interactions with service providers. + + GET /api/v1.0/service-providers/ + Return a list of service providers. + + GET /api/v1.0/service-providers// + Return a service provider. + """ + + permission_classes = [permissions.IsAuthenticated] + queryset = models.ServiceProvider.objects.all() + serializer_class = serializers.ServiceProviderSerializer + throttle_classes = [BurstRateThrottle, SustainedRateThrottle] + pagination_class = Pagination + filter_backends = [filters.OrderingFilter, ServiceProviderFilter] + ordering = ["name"] + ordering_fields = ["name", "created_at"] + + def get_queryset(self): + """Filter the queryset to limit results to user's organization.""" + queryset = super().get_queryset() + queryset = queryset.filter(organizations__id=self.request.user.organization_id) + return queryset diff --git a/src/backend/core/factories.py b/src/backend/core/factories.py index 8fea20e..ca715bd 100644 --- a/src/backend/core/factories.py +++ b/src/backend/core/factories.py @@ -226,5 +226,20 @@ class ServiceProviderFactory(factory.django.DjangoModelFactory): class Meta: model = models.ServiceProvider + skip_postgeneration_save = True audience_id = factory.Faker("uuid4") + + @factory.post_generation + def teams(self, create, extracted, **kwargs): + """Add teams to service provider from a given list.""" + if not create or not extracted: + return + self.teams.set(extracted) + + @factory.post_generation + def organizations(self, create, extracted, **kwargs): + """Add organization to service provider from a given list.""" + if not create or not extracted: + return + self.organizations.set(extracted) diff --git a/src/backend/core/tests/conftest.py b/src/backend/core/tests/resource_server_api/conftest.py similarity index 100% rename from src/backend/core/tests/conftest.py rename to src/backend/core/tests/resource_server_api/conftest.py diff --git a/src/backend/core/tests/service_providers/__init__.py b/src/backend/core/tests/service_providers/__init__.py new file mode 100644 index 0000000..a1d1f69 --- /dev/null +++ b/src/backend/core/tests/service_providers/__init__.py @@ -0,0 +1,3 @@ +""" +Test for the service providers viewset. +""" diff --git a/src/backend/core/tests/service_providers/test_core_api_service_providers_list.py b/src/backend/core/tests/service_providers/test_core_api_service_providers_list.py new file mode 100644 index 0000000..9eb179a --- /dev/null +++ b/src/backend/core/tests/service_providers/test_core_api_service_providers_list.py @@ -0,0 +1,91 @@ +""" +Tests for Service Provider API endpoint in People's core app: list +""" + +import pytest +from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED + +from core import factories + +pytestmark = pytest.mark.django_db + + +def test_api_service_providers_list_anonymous(client): + """Anonymous users should not be allowed to list service providers.""" + factories.ServiceProviderFactory.create_batch(2) + + response = client.get("/api/v1.0/service-providers/") + + assert response.status_code == HTTP_401_UNAUTHORIZED + assert response.json() == { + "detail": "Authentication credentials were not provided." + } + + +def test_api_service_providers_list_authenticated(client): + """ + Authenticated users should be able to list service providers + of their organization. + """ + user = factories.UserFactory(with_organization=True) + client.force_login(user) + + service_provider_1 = factories.ServiceProviderFactory( + name="A", organizations=[user.organization] + ) + service_provider_2 = factories.ServiceProviderFactory( + name="B", organizations=[user.organization] + ) + + # Generate some not fetched data + factories.ServiceProviderFactory.create_batch( + 2, organizations=[factories.OrganizationFactory(with_registration_id=True)] + ) # Other service providers + factories.TeamFactory( + users=[user], service_providers=[factories.ServiceProviderFactory()] + ) + + response = client.get( + "/api/v1.0/service-providers/", + ) + + assert response.status_code == HTTP_200_OK + assert response.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "audience_id": str(service_provider_1.audience_id), + "id": str(service_provider_1.pk), + "name": "A", + }, + { + "audience_id": str(service_provider_2.audience_id), + "id": str(service_provider_2.pk), + "name": "B", + }, + ], + } + + +def test_api_service_providers_order(client): + """Test that the service providers are sorted as requested.""" + user = factories.UserFactory(with_organization=True) + factories.ServiceProviderFactory(name="A", organizations=[user.organization]) + factories.ServiceProviderFactory(name="B", organizations=[user.organization]) + + client.force_login(user) + + # Test ordering by name descending + response = client.get("/api/v1.0/service-providers/?ordering=-name") + assert response.status_code == 200 + response_data = response.json()["results"] + assert response_data[0]["name"] == "B" + assert response_data[1]["name"] == "A" + + # Test ordering by creation date ascending + response = client.get("/api/v1.0/service-providers/?ordering=created_at") + response_data = response.json()["results"] + assert response_data[0]["name"] == "A" + assert response_data[1]["name"] == "B" diff --git a/src/backend/core/tests/service_providers/test_core_api_service_providers_retrieve.py b/src/backend/core/tests/service_providers/test_core_api_service_providers_retrieve.py new file mode 100644 index 0000000..8401d98 --- /dev/null +++ b/src/backend/core/tests/service_providers/test_core_api_service_providers_retrieve.py @@ -0,0 +1,85 @@ +""" +Tests for Service Provider API endpoint in People's core app: retrieve +""" + +import pytest +from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND + +from core import factories + +pytestmark = pytest.mark.django_db + + +def test_api_service_providers_retrieve_anonymous(client): + """Anonymous users should not be allowed to retrieve service providers.""" + service_provider = factories.ServiceProviderFactory() + + response = client.get(f"/api/v1.0/service-providers/{service_provider.pk}/") + + assert response.status_code == HTTP_401_UNAUTHORIZED + assert response.json() == { + "detail": "Authentication credentials were not provided." + } + + +def test_api_service_providers_retrieve_authenticated_allowed(client): + """ + Authenticated users should be able to retrieve service providers + of their organization. + """ + user = factories.UserFactory(with_organization=True) + client.force_login(user) + + service_provider = factories.ServiceProviderFactory( + organizations=[user.organization] + ) + + response = client.get(f"/api/v1.0/service-providers/{service_provider.pk}/") + + assert response.status_code == HTTP_200_OK + assert response.json() == { + "audience_id": str(service_provider.audience_id), + "id": str(service_provider.pk), + "name": service_provider.name, + } + + +def test_api_service_providers_retrieve_authenticated_other_organization(client): + """ + Authenticated users should not be able to retrieve service providers + of other organization. + """ + user = factories.UserFactory(with_organization=True) + client.force_login(user) + + service_provider = factories.ServiceProviderFactory( + organizations=[factories.OrganizationFactory(with_registration_id=True)] + ) + + response = client.get(f"/api/v1.0/service-providers/{service_provider.pk}/") + + assert response.status_code == HTTP_404_NOT_FOUND + assert response.json() == {"detail": "No ServiceProvider matches the given query."} + + +def test_api_service_providers_retrieve_authenticated_on_teams(client): + """ + Authenticated users should not be able to retrieve service providers + just because it is related to one of their teams if it is not related + to their organization (might change later if needed). + """ + user = factories.UserFactory(with_organization=True) + client.force_login(user) + + other_organization = factories.OrganizationFactory(with_registration_id=True) + service_provider = factories.ServiceProviderFactory() + factories.TeamFactory( + users=[user], + organization=other_organization, + service_providers=[service_provider], + ) + + response = client.get(f"/api/v1.0/service-providers/{service_provider.pk}/") + + assert response.status_code == HTTP_404_NOT_FOUND + assert response.json() == {"detail": "No ServiceProvider matches the given query."} diff --git a/src/backend/people/api_urls.py b/src/backend/people/api_urls.py index 059b662..451e3c0 100644 --- a/src/backend/people/api_urls.py +++ b/src/backend/people/api_urls.py @@ -14,6 +14,9 @@ router = DefaultRouter() router.register("contacts", viewsets.ContactViewSet, basename="contacts") router.register("teams", viewsets.TeamViewSet, basename="teams") router.register("users", viewsets.UserViewSet, basename="users") +router.register( + "service-providers", viewsets.ServiceProviderViewSet, basename="service-providers" +) # - Routes nested under a team team_related_router = DefaultRouter()