diff --git a/apps/api/plane/api/views/asset.py b/apps/api/plane/api/views/asset.py index abfa6bdc0d8..f5b17d20500 100644 --- a/apps/api/plane/api/views/asset.py +++ b/apps/api/plane/api/views/asset.py @@ -335,7 +335,7 @@ def post(self, request): ) # Get the presigned URL - storage = S3Storage(request=request, is_server=True) + storage = S3Storage(request=request) # Generate a presigned URL to share an S3 object presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit) # Return the presigned URL @@ -448,7 +448,7 @@ def get(self, request, slug, asset_id): # Force attachment disposition for script-capable MIME types (e.g. SVG) # to prevent same-origin XSS when the asset URL shares the app's origin # (default MinIO self-hosted setup). - storage = S3Storage(request=request, is_server=True) + storage = S3Storage(request=request) asset_mime_type = (asset.attributes.get("type") or "").split(";")[0].strip().lower() disposition = ( "attachment" if asset_mime_type in settings.SCRIPT_CAPABLE_MIME_TYPES else "inline" @@ -578,7 +578,7 @@ def post(self, request, slug): ) # Get the presigned URL - storage = S3Storage(request=request, is_server=True) + storage = S3Storage(request=request) presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit) return Response( diff --git a/apps/api/plane/tests/contract/api/test_asset_storage.py b/apps/api/plane/tests/contract/api/test_asset_storage.py new file mode 100644 index 00000000000..4bce1bb369d --- /dev/null +++ b/apps/api/plane/tests/contract/api/test_asset_storage.py @@ -0,0 +1,79 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +"""Contract tests for storage initialization in public asset endpoints.""" + +from unittest import mock + +import pytest +from rest_framework import status + +from plane.db.models import FileAsset + + +@pytest.mark.contract +class TestPublicAssetStorageInitialization: + """Public asset endpoints must construct ``S3Storage`` with its supported API.""" + + @pytest.mark.django_db + def test_member_can_generate_generic_asset_download(self, api_key_client, workspace, create_user): + """The generic download route returns a URL using the supported storage API.""" + asset = FileAsset.objects.create( + attributes={"name": "screenshot.png", "type": "image/png", "size": 1024}, + asset=f"{workspace.id}/screenshot.png", + size=1024, + workspace=workspace, + created_by=create_user, + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + is_uploaded=True, + storage_metadata={"size": 1024}, + ) + url = f"/api/v1/workspaces/{workspace.slug}/assets/{asset.id}/" + + with mock.patch("plane.api.views.asset.S3Storage", autospec=True) as mock_storage: + mock_storage.return_value.generate_presigned_url.return_value = "https://signed.example/download" + response = api_key_client.get(url) + + assert response.status_code == status.HTTP_200_OK, f"Got {response.status_code}: {response.data!r}" + assert response.data["asset_url"] == "https://signed.example/download" + mock_storage.assert_called_once_with(request=mock.ANY) + + @pytest.mark.django_db + def test_member_can_generate_generic_asset_upload(self, api_key_client, workspace): + """The generic upload route creates its asset using the supported storage API.""" + url = f"/api/v1/workspaces/{workspace.slug}/assets/" + payload = {"name": "screenshot.png", "type": "image/png", "size": 1024} + + with mock.patch("plane.api.views.asset.S3Storage", autospec=True) as mock_storage: + mock_storage.return_value.generate_presigned_post.return_value = { + "url": "https://signed.example/upload", + "fields": {}, + } + response = api_key_client.post(url, payload, format="json") + + assert response.status_code == status.HTTP_200_OK, f"Got {response.status_code}: {response.data!r}" + assert FileAsset.objects.filter(id=response.data["asset_id"], workspace=workspace).exists() + mock_storage.assert_called_once_with(request=mock.ANY) + + @pytest.mark.django_db + def test_user_can_generate_server_asset_upload(self, api_key_client, create_user): + """The server upload route creates a user asset using the supported storage API.""" + url = "/api/v1/assets/user-assets/server/" + payload = { + "name": "avatar.png", + "type": "image/png", + "size": 1024, + "entity_type": FileAsset.EntityTypeContext.USER_AVATAR, + } + + with mock.patch("plane.api.views.asset.S3Storage", autospec=True) as mock_storage: + mock_storage.return_value.generate_presigned_post.return_value = { + "url": "https://signed.example/upload", + "fields": {}, + } + response = api_key_client.post(url, payload, format="json") + + assert response.status_code == status.HTTP_200_OK, f"Got {response.status_code}: {response.data!r}" + assert FileAsset.objects.filter(id=response.data["asset_id"], user=create_user).exists() + mock_storage.assert_called_once_with(request=mock.ANY)