Skip to content

Commit

Permalink
Add party document streaming endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
currycoder committed Feb 12, 2024
1 parent b874f40 commit 1b8e582
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 1 deletion.
8 changes: 8 additions & 0 deletions api/applications/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from rest_framework import permissions

from api.organisations.libraries.get_organisation import get_request_user_organisation_id


class IsPartyDocumentInOrganisation(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.party.organisation_id == get_request_user_organisation_id(request)
38 changes: 38 additions & 0 deletions api/applications/tests/test_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase
from rest_framework.permissions import IsAdminUser

from test_helpers.clients import DataTestClient
from api.core.authentication import ORGANISATION_ID
from api.parties.tests.factories import PartyDocumentFactory, PartyFactory
from api.organisations.tests.factories import OrganisationFactory
from api.applications.permissions import IsPartyDocumentInOrganisation


class IsPartyDocumentInOrganisationTest(DataTestClient):
def test_permissions_success(self):
organisation = OrganisationFactory()
party = PartyFactory(organisation=organisation)
party_document = PartyDocumentFactory(party=party, s3_key="somekey")
factory = RequestFactory()

request = factory.get("/")
request.META[ORGANISATION_ID] = organisation.id

authorised = IsPartyDocumentInOrganisation().has_object_permission(request, None, party_document)

self.assertTrue(authorised)

def test_permissions_failure(self):
organisation = OrganisationFactory()
other_organisation = OrganisationFactory()
party = PartyFactory(organisation=organisation)
party_document = PartyDocumentFactory(party=party, s3_key="somekey")
factory = RequestFactory()

request = factory.get("/")
request.META[ORGANISATION_ID] = other_organisation.id

authorised = IsPartyDocumentInOrganisation().has_object_permission(request, None, party_document)

self.assertFalse(authorised)
5 changes: 5 additions & 0 deletions api/applications/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@
party_documents.PartyDocumentView.as_view(),
name="party_document_view",
),
path(
"<uuid:pk>/parties/<uuid:party_pk>/document/<uuid:document_pk>/stream/",
party_documents.PartyDocumentStream.as_view(),
name="party_document_stream",
),
# Sites, locations and countries
path("<uuid:pk>/sites/", sites.ApplicationSites.as_view(), name="application_sites"),
path("<uuid:pk>/contract-types/", countries.ApplicationContractTypes.as_view(), name="contract_types"),
Expand Down
17 changes: 17 additions & 0 deletions api/applications/views/party_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from api.audit_trail.enums import AuditType
from api.applications.libraries.get_applications import get_application
from api.applications.libraries.document_helpers import upload_party_document, delete_party_document, get_party_document
from api.applications.permissions import IsPartyDocumentInOrganisation
from api.core.authentication import ExporterAuthentication
from api.core.views import DocumentStreamAPIView
from api.core.decorators import authorised_to_view_application
from api.parties.models import PartyDocument
from api.users.models import ExporterUser
Expand Down Expand Up @@ -64,3 +66,18 @@ def delete(self, request, **kwargs):
)

return HttpResponse(status=status.HTTP_204_NO_CONTENT)


class PartyDocumentStream(DocumentStreamAPIView):
authentication_classes = (ExporterAuthentication,)
lookup_url_kwarg = "document_pk"
permission_classes = (IsPartyDocumentInOrganisation,)

def get_queryset(self):
return PartyDocument.objects.filter(
party_id=self.kwargs["party_pk"],
party__parties_on_application__application__id=self.kwargs["pk"],
)

def get_document(self, instance):
return instance
114 changes: 114 additions & 0 deletions api/applications/views/tests/test_party_documents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from moto import mock_aws
from parameterized import parameterized

from django.http import FileResponse
from django.urls import reverse
from rest_framework import status

from api.applications.tests.factories import PartyOnApplicationFactory, StandardApplicationFactory
from api.parties.tests.factories import PartyFactory, PartyDocumentFactory
from test_helpers.clients import DataTestClient


@mock_aws
class PartyDocumentStreamTests(DataTestClient):
def setUp(self):
super().setUp()
self.party = PartyFactory(
organisation=self.organisation,
)
self.party_on_application = PartyOnApplicationFactory(party=self.party)
self.application = self.party_on_application.application
self.create_default_bucket()
self.put_object_in_default_bucket("thisisakey", b"test")

def test_get_party_document_stream(self):
party_document = PartyDocumentFactory(
party=self.party,
s3_key="thisisakey",
name="doc1.pdf",
safe=True,
)

url = reverse(
"applications:party_document_stream",
kwargs={
"pk": str(self.application.pk),
"party_pk": str(self.party.pk),
"document_pk": str(party_document.pk),
},
)
response = self.client.get(url, **self.exporter_headers)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIsInstance(response, FileResponse)
self.assertEqual(b"".join(response.streaming_content), b"test")

def test_get_party_document_stream_invalid_document_pk(self):
another_party = PartyFactory(
organisation=self.organisation,
)
party_document = PartyDocumentFactory(
party=self.party,
s3_key="thisisakey",
name="doc1.pdf",
safe=True,
)

url = reverse(
"applications:party_document_stream",
kwargs={
"pk": str(self.application.pk),
"party_pk": str(another_party.pk),
"document_pk": str(party_document.pk),
},
)
response = self.client.get(url, **self.exporter_headers)

self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_get_party_document_stream_invalid_application_pk(self):
another_application = StandardApplicationFactory()
party_document = PartyDocumentFactory(
party=self.party,
s3_key="thisisakey",
name="doc1.pdf",
safe=True,
)

url = reverse(
"applications:party_document_stream",
kwargs={
"pk": str(another_application.pk),
"party_pk": str(self.party.pk),
"document_pk": str(party_document.pk),
},
)
response = self.client.get(url, **self.exporter_headers)

self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_get_party_document_stream_forbidden_organisation(self):
other_organisation = self.create_organisation_with_exporter_user()[0]
self.party.organisation = other_organisation
self.party.save()
self.application.organisation = other_organisation
self.application.save()
party_document = PartyDocumentFactory(
party=self.party,
s3_key="thisisakey",
name="doc1.pdf",
safe=True,
)

url = reverse(
"applications:party_document_stream",
kwargs={
"pk": str(self.application.pk),
"party_pk": str(self.party.pk),
"document_pk": str(party_document.pk),
},
)
response = self.client.get(url, **self.exporter_headers)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
7 changes: 6 additions & 1 deletion api/parties/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from api.staticdata.countries.factories import CountryFactory
from api.parties.enums import SubType, PartyType
from api.parties.models import Party
from api.parties.models import Party, PartyDocument


class PartyFactory(factory.django.DjangoModelFactory):
Expand All @@ -17,6 +17,11 @@ class Meta:
model = Party


class PartyDocumentFactory(factory.django.DjangoModelFactory):
class Meta:
model = PartyDocument


class ConsigneeFactory(PartyFactory):
type = PartyType.CONSIGNEE
sub_type = SubType.GOVERNMENT
Expand Down

0 comments on commit 1b8e582

Please sign in to comment.