Skip to content

Commit 69f0742

Browse files
committed
- tests for uploads
- fixed issues found via tests - disallow uploads for banned maps, temporary maps, and legacy maps
1 parent f6387a5 commit 69f0742

File tree

9 files changed

+192
-13
lines changed

9 files changed

+192
-13
lines changed

kirovy/constants/api_codes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ class LegacyUploadApiCodes(enum.StrEnum):
2626
class FileUploadApiCodes(enum.StrEnum):
2727
MISSING_FOREIGN_ID = "missing-foreign-id"
2828
INVALID = "file-failed-validation"
29+
UNSUPPORTED = "parent-does-not-support-this-upload"
30+
"""attr: Raised when the parent object for the file does not allow this upload.
31+
32+
e.g. a temporary map does not support custom image uploads.
33+
"""

kirovy/models/cnc_map.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def generate_upload_to(instance: "CncMapImageFile", filename: str) -> pathlib.Pa
292292
Path to upload map to relative to :attr:`~kirovy.settings.base.MEDIA_ROOT`.
293293
"""
294294
filename = pathlib.Path(filename)
295-
final_file_name = f"{instance.created.isoformat('-')}_{instance.id.hex}{filename.suffix}"
295+
final_file_name = f"{instance.created.date().isoformat()}_{instance.id.hex}{filename.suffix}"
296296

297297
# e.g. "yr/map_images/CNC_NET_MAP_ID_HEX/2020-01-01_somelongid.jpg
298298
return pathlib.Path(instance.cnc_map.get_map_directory_path(instance.UPLOAD_TYPE), final_file_name)

kirovy/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def _get_url_patterns() -> list[_DjangoPath]:
108108
path("<uuid:pk>/", cnc_map_views.MapRetrieveUpdateView.as_view()),
109109
path("delete/<uuid:pk>/", cnc_map_views.MapDeleteView.as_view()),
110110
path("search/", cnc_map_views.MapListView.as_view()),
111-
path("img", cnc_map_views.MapImageFileUploadView.as_view()),
111+
path("img/", cnc_map_views.MapImageFileUploadView.as_view()),
112112
# path("img/<uuid:map_id>/", ...),
113113
# path("search/")
114114
]

kirovy/views/base_views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def get_parent_object(self, request: KirovyRequest) -> GameScopedUserOwnedModel:
194194
def post(self, request: KirovyRequest, format=None) -> KirovyResponse[ui_objects.ResultResponseData]:
195195
uploaded_file: UploadedFile = request.data["file"]
196196
parent_object = self.get_parent_object(request)
197+
self.extra_verification(request, uploaded_file, parent_object)
197198

198199
extension_id = CncFileExtension.get_extension_id_for_upload(
199200
uploaded_file,
@@ -206,7 +207,7 @@ def post(self, request: KirovyRequest, format=None) -> KirovyResponse[ui_objects
206207
data={
207208
"cnc_game_id": parent_object.cnc_game_id,
208209
self.file_parent_attr_name: parent_object.id,
209-
"name": request.get("name"),
210+
"name": uploaded_file.name,
210211
"file": uploaded_file,
211212
"file_extension_id": extension_id,
212213
**self.extra_serializer_data(request, uploaded_file, parent_object),
@@ -223,13 +224,25 @@ def post(self, request: KirovyRequest, format=None) -> KirovyResponse[ui_objects
223224
ui_objects.ResultResponseData(
224225
message=self.success_message,
225226
result={
227+
"file_id": saved.id,
226228
"file_url": saved.file.url,
227229
"parent_object_id": parent_object.id,
228230
},
229-
)
231+
),
232+
status=status.HTTP_201_CREATED,
230233
)
231234

232235
def extra_serializer_data(
233236
self, request: KirovyRequest, uploaded_file: UploadedFile, parent_object: GameScopedUserOwnedModel
234237
) -> t.Dict[str, t.Any]:
235238
raise NotImplementedError()
239+
240+
def extra_verification(
241+
self, request: KirovyRequest, uploaded_file: UploadedFile, parent_object: GameScopedUserOwnedModel
242+
) -> None:
243+
"""Any extra verification that the file needs.
244+
245+
:raises kirovy.exceptions.view_exceptions.KirovyValidationError:
246+
Raised for any issues.
247+
"""
248+
raise NotImplementedError()

kirovy/views/cnc_map_views.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,14 @@ class MapImageFileUploadView(base_views.FileUploadBaseView):
328328
success_message = "Map image uploaded successfully."
329329

330330
def extra_serializer_data(
331-
self, request: KirovyRequest, uploaded_file: UploadedFile, parent_object: GameScopedUserOwnedModel
331+
self, request: KirovyRequest, uploaded_file: UploadedFile, parent_object: CncMap
332332
) -> t.Dict[str, t.Any]:
333+
latest_image: CncMapImageFile | None = (
334+
CncMapImageFile.objects.filter(cnc_map_id=parent_object.id)
335+
.order_by("-image_order")
336+
.only("image_order")
337+
.first()
338+
)
333339
try:
334340
with Image.open(uploaded_file) as image:
335341
height = image.height
@@ -340,4 +346,15 @@ def extra_serializer_data(
340346
{"user_id": request.user.id, "username": request.user.username, "e": str(e)},
341347
)
342348
raise KirovyValidationError("Image is invalid", code=api_codes.FileUploadApiCodes.INVALID)
343-
return {"height": height, "width": width}
349+
return {
350+
"height": height,
351+
"width": width,
352+
"is_extracted": False,
353+
"image_order": latest_image.image_order if latest_image else 0,
354+
}
355+
356+
def extra_verification(self, request: KirovyRequest, uploaded_file: UploadedFile, parent_object: CncMap) -> None:
357+
if parent_object.is_legacy or parent_object.is_temporary:
358+
raise KirovyValidationError(
359+
"Map type does not support custom preview images", code=api_codes.FileUploadApiCodes.UNSUPPORTED
360+
)

tests/fixtures/file_fixtures.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,40 @@ def _inner(file: File | io.BytesIO) -> t.Tuple[ContentFile, ZipContentsSha1]:
188188
return ContentFile(zip_bytes.read(), f"{file_sha1}.zip"), file_sha1
189189

190190
return _inner
191+
192+
193+
@pytest.fixture
194+
def file_map_image(load_test_file) -> Generator[File, Any, None]:
195+
"""Return a valid map preview image."""
196+
file = load_test_file("test_images/2_across_the_frost.png")
197+
yield file
198+
file.close()
199+
200+
201+
@pytest.fixture
202+
def file_map_image_jpg(load_test_file) -> Generator[File, Any, None]:
203+
"""Return a valid map preview image in jpg."""
204+
file = load_test_file("test_images/uptown.jpg")
205+
yield file
206+
file.close()
207+
208+
209+
@pytest.fixture
210+
def get_file_path_for_uploaded_file_url(settings, tmp_media_root):
211+
"""Returns a function to convert an uploaded file's URL to the actual file path in the tmp folder.
212+
213+
Used to check that an uploaded file actually exists on disk after uploading in tests.
214+
"""
215+
216+
def _inner(uploaded_file_url: str) -> pathlib.Path:
217+
"""Convert an uploaded file's URL to its physical filepath in the tmp directory.
218+
219+
:param uploaded_file_url:
220+
The URL path for an uploaded file.
221+
:return:
222+
The file path to the uploaded file.
223+
"""
224+
strip_media_url = f"/{settings.MEDIA_URL}"
225+
return pathlib.Path(tmp_media_root) / uploaded_file_url.lstrip(strip_media_url)
226+
227+
return _inner
236 KB
Loading
338 KB
Loading

tests/test_views/test_map_upload.py

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22
import zipfile
33
import io
44

5+
import pytest
56
from django.core.files.uploadedfile import UploadedFile
67
from rest_framework import status
78

89
from kirovy import settings, typing as t
9-
from kirovy.constants.api_codes import UploadApiCodes
10+
from kirovy.constants.api_codes import UploadApiCodes, FileUploadApiCodes
1011
from kirovy.models.cnc_map import CncMapImageFile
1112
from kirovy.utils import file_utils
1213
from kirovy.models import CncMap, CncMapFile, MapCategory
1314
from kirovy.response import KirovyResponse
1415
from kirovy.services.cnc_gen_2_services import CncGen2MapParser
16+
from kirovy.views.cnc_map_views import MapImageFileUploadView
1517

1618
_UPLOAD_URL = "/maps/upload/"
1719
_CLIENT_URL = "/maps/client/upload/"
1820

1921

20-
def test_map_file_upload_happy_path(client_user, file_map_desert, game_yuri, extension_map, tmp_media_root):
22+
def test_map_file_upload_happy_path(
23+
client_user, file_map_desert, game_yuri, extension_map, tmp_media_root, get_file_path_for_uploaded_file_url
24+
):
2125
response: KirovyResponse = client_user.post(
2226
_UPLOAD_URL,
2327
{"file": file_map_desert, "game_id": str(game_yuri.id)},
@@ -30,11 +34,9 @@ def test_map_file_upload_happy_path(client_user, file_map_desert, game_yuri, ext
3034
uploaded_file_url: str = response.data["result"]["cnc_map_file"]
3135
uploaded_image_url: str = response.data["result"]["extracted_preview_file"]
3236

33-
# We need to strip the url path off of the files,
34-
# then check the tmp directory to make sure the uploaded files were saved
35-
strip_media_url = f"/{settings.MEDIA_URL}"
36-
uploaded_file_path = pathlib.Path(tmp_media_root) / uploaded_file_url.lstrip(strip_media_url)
37-
uploaded_image = pathlib.Path(tmp_media_root) / uploaded_image_url.lstrip(strip_media_url)
37+
# We need to check the tmp directory to make sure the uploaded files were saved
38+
uploaded_file_path = get_file_path_for_uploaded_file_url(uploaded_file_url)
39+
uploaded_image = get_file_path_for_uploaded_file_url(uploaded_image_url)
3840
assert uploaded_file_path.exists()
3941
assert uploaded_image.exists()
4042

@@ -115,3 +117,108 @@ def test_map_file_upload_banned_map(banned_cheat_map, file_map_unfair, client_an
115117
assert response.status_code == status.HTTP_400_BAD_REQUEST
116118
assert response.data["code"] == UploadApiCodes.DUPLICATE_MAP
117119
assert response.data["additional"]["existing_map_id"] == str(banned_cheat_map.id)
120+
121+
122+
def test_map_image_upload__happy_path(create_cnc_map, file_map_image, client_user, get_file_path_for_uploaded_file_url):
123+
"""Test that we can upload an image as a verified user, who has created a map"""
124+
cnc_map = create_cnc_map(user_id=client_user.kirovy_user.id, is_legacy=False, is_published=True, is_temporary=False)
125+
original_image_count = cnc_map.cncmapimagefile_set.select_related().count()
126+
response = client_user.post(
127+
"/maps/img/",
128+
{"file": file_map_image, "cnc_map_id": str(cnc_map.id)},
129+
format="multipart",
130+
content_type=None,
131+
)
132+
133+
assert response.status_code == status.HTTP_201_CREATED
134+
assert response.data["message"] == MapImageFileUploadView.success_message
135+
saved_file = CncMapImageFile.objects.get(id=response.data["result"]["file_id"])
136+
image_url: str = response.data["result"]["file_url"]
137+
parent_id: str = response.data["result"]["parent_object_id"]
138+
139+
assert parent_id == cnc_map.id
140+
expected_date = saved_file.created.date().isoformat()
141+
assert image_url == f"/silo/yr/map_images/{cnc_map.id.hex}/{expected_date}_{saved_file.id.hex}.png"
142+
143+
assert get_file_path_for_uploaded_file_url(image_url).exists()
144+
145+
assert cnc_map.cncmapimagefile_set.select_related().count() == original_image_count + 1
146+
# Image order starts at 0, then gets incremented, so image_order should be current_count - 1
147+
assert saved_file.image_order == original_image_count
148+
assert saved_file.name == pathlib.Path(file_map_image.name).name
149+
assert saved_file.file_extension.extension == "png"
150+
# Width and height are from the image itself.
151+
assert saved_file.width == 768
152+
assert saved_file.height == 494
153+
154+
155+
def test_map_image_upload__jpg(create_cnc_map, file_map_image_jpg, client_user, get_file_path_for_uploaded_file_url):
156+
"""Test that we can upload a jpg."""
157+
cnc_map = create_cnc_map(user_id=client_user.kirovy_user.id, is_legacy=False, is_published=True, is_temporary=False)
158+
response = client_user.post(
159+
"/maps/img/",
160+
{"file": file_map_image_jpg, "cnc_map_id": str(cnc_map.id)},
161+
format="multipart",
162+
content_type=None,
163+
)
164+
165+
assert response.status_code == status.HTTP_201_CREATED
166+
assert response.data["message"] == MapImageFileUploadView.success_message
167+
saved_file = CncMapImageFile.objects.get(id=response.data["result"]["file_id"])
168+
assert saved_file.name == pathlib.Path(file_map_image_jpg.name).name
169+
assert saved_file.file_extension.extension == "jpg"
170+
171+
# Width and height are from the image itself.
172+
assert saved_file.width == 993
173+
assert saved_file.height == 740
174+
175+
176+
@pytest.mark.parametrize("map_kwargs", [{"is_legacy": True}, {"is_temporary": True}])
177+
def test_map_image_upload__unsupported_map(create_cnc_map, file_map_image, client_user, map_kwargs):
178+
"""Test that map image uploads fail for legacy and temporary maps."""
179+
cnc_map = create_cnc_map(user_id=client_user.kirovy_user.id, **map_kwargs)
180+
original_image_count = cnc_map.cncmapimagefile_set.select_related().count()
181+
response = client_user.post(
182+
"/maps/img/",
183+
{"file": file_map_image, "cnc_map_id": str(cnc_map.id)},
184+
format="multipart",
185+
content_type=None,
186+
)
187+
188+
assert response.status_code == status.HTTP_400_BAD_REQUEST
189+
assert response.data["message"] == "Map type does not support custom preview images"
190+
assert response.data["code"] == FileUploadApiCodes.UNSUPPORTED
191+
192+
assert cnc_map.cncmapimagefile_set.select_related().count() == original_image_count
193+
194+
195+
def test_map_image_upload__user_is_banned(create_cnc_map, file_map_image, client_banned):
196+
"""Test that map image uploads fail for banned users."""
197+
cnc_map = create_cnc_map(user_id=client_banned.kirovy_user.id)
198+
original_image_count = cnc_map.cncmapimagefile_set.select_related().count()
199+
response = client_banned.post(
200+
"/maps/img/",
201+
{"file": file_map_image, "cnc_map_id": str(cnc_map.id)},
202+
format="multipart",
203+
content_type=None,
204+
)
205+
206+
assert response.status_code == status.HTTP_403_FORBIDDEN
207+
208+
assert cnc_map.cncmapimagefile_set.select_related().count() == original_image_count
209+
210+
211+
def test_map_image_upload__map_is_banned(create_cnc_map, file_map_image, client_user):
212+
"""Test that map image uploads fail for banned maps."""
213+
cnc_map = create_cnc_map(user_id=client_user.kirovy_user.id, is_banned=True)
214+
original_image_count = cnc_map.cncmapimagefile_set.select_related().count()
215+
response = client_user.post(
216+
"/maps/img/",
217+
{"file": file_map_image, "cnc_map_id": str(cnc_map.id)},
218+
format="multipart",
219+
content_type=None,
220+
)
221+
222+
assert response.status_code == status.HTTP_403_FORBIDDEN
223+
224+
assert cnc_map.cncmapimagefile_set.select_related().count() == original_image_count

0 commit comments

Comments
 (0)