mirror of
https://github.com/rommapp/romm.git
synced 2026-06-27 22:35:57 +00:00
test(backend): cover artwork upload validation for roms and collections
Adds rejection + acceptance tests for update_rom, add_collection, and update_collection artwork uploads, mirroring the existing avatar tests: non-image content returns 400, and a real PNG uploaded under a misleading filename like payload.html is stored with the trusted .png extension. Also fixes two `return HTTPException(...)` → `raise` in raw.py so the 404 path actually surfaces instead of silently returning the exception object. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,7 @@ def head_raw_asset(request: Request, path: str):
|
||||
|
||||
# Check if file exists and is a file (not directory)
|
||||
if not resolved_path.exists() or not resolved_path.is_file():
|
||||
return HTTPException(status_code=404, detail="Asset not found")
|
||||
raise HTTPException(status_code=404, detail="Asset not found")
|
||||
|
||||
return _build_asset_response(resolved_path)
|
||||
|
||||
@@ -69,6 +69,6 @@ def get_raw_asset(request: Request, path: str):
|
||||
|
||||
# Check if file exists and is a file (not directory)
|
||||
if not resolved_path.exists() or not resolved_path.is_file():
|
||||
return HTTPException(status_code=404, detail="Asset not found")
|
||||
raise HTTPException(status_code=404, detail="Asset not found")
|
||||
|
||||
return _build_asset_response(resolved_path)
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from handler.filesystem.resources_handler import FSResourcesHandler
|
||||
from handler.filesystem.roms_handler import FSRomsHandler
|
||||
from handler.metadata.flashpoint_handler import FlashpointHandler, FlashpointRom
|
||||
from handler.metadata.igdb_handler import IGDBHandler, IGDBRom
|
||||
@@ -127,6 +128,54 @@ def test_update_rom_reparses_tags_on_fs_name_change(
|
||||
assert body["tags"] == []
|
||||
|
||||
|
||||
# Minimal valid PNG (1x1 transparent pixel)
|
||||
_PNG_BYTES = (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
|
||||
b"\x00\x00\x00\rIDATx\x9cc\xfc\xff\xff?\x00\x05\xfe\x02\xfe\xa75\x81\x84"
|
||||
b"\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
|
||||
|
||||
def test_update_rom_rejects_non_image_artwork(
|
||||
client: TestClient, access_token: str, rom: Rom
|
||||
):
|
||||
response = client.put(
|
||||
f"/api/roms/{rom.id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
files={"artwork": ("cover.png", b"<script>alert(1)</script>", "image/png")},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "PNG, JPEG, WebP, or GIF" in response.json()["detail"]
|
||||
|
||||
|
||||
@patch.object(
|
||||
FSResourcesHandler,
|
||||
"store_artwork",
|
||||
new_callable=AsyncMock,
|
||||
return_value=("path/to/big.png", "path/to/small.png"),
|
||||
)
|
||||
def test_update_rom_artwork_uses_detected_extension(
|
||||
store_artwork_mock: AsyncMock,
|
||||
client: TestClient,
|
||||
access_token: str,
|
||||
rom: Rom,
|
||||
):
|
||||
response = client.put(
|
||||
f"/api/roms/{rom.id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
files={"artwork": ("payload.html", _PNG_BYTES, "image/png")},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# The handler is called with the trusted extension from libmagic, not the
|
||||
# extension parsed off the user-supplied filename.
|
||||
assert store_artwork_mock.called
|
||||
_, _, file_ext = store_artwork_mock.call_args.args
|
||||
assert file_ext == "png"
|
||||
|
||||
|
||||
def test_delete_roms(client: TestClient, access_token: str, rom: Rom):
|
||||
response = client.post(
|
||||
"/api/roms/delete",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import status
|
||||
@@ -6,11 +7,21 @@ from fastapi import status
|
||||
from config import OAUTH_ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
from handler.auth import oauth_handler
|
||||
from handler.database import db_collection_handler, db_rom_handler
|
||||
from handler.filesystem.resources_handler import FSResourcesHandler
|
||||
from models.collection import Collection
|
||||
from models.platform import Platform
|
||||
from models.rom import Rom
|
||||
from models.user import User
|
||||
|
||||
# Minimal valid PNG (1x1 transparent pixel)
|
||||
_PNG_BYTES = (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
|
||||
b"\x00\x00\x00\rIDATx\x9cc\xfc\xff\xff?\x00\x05\xfe\x02\xfe\xa75\x81\x84"
|
||||
b"\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -564,3 +575,79 @@ class TestAtomicBehavior:
|
||||
refreshed = db_collection_handler.get_collection(collection.id)
|
||||
assert refreshed is not None
|
||||
assert set(refreshed.rom_ids) == {rom.id, second_rom.id}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Artwork upload validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestArtworkUpload:
|
||||
def test_add_collection_rejects_non_image_artwork(self, client, access_token: str):
|
||||
response = client.post(
|
||||
"/api/collections",
|
||||
data={"name": "Bad Cover Collection"},
|
||||
files={"artwork": ("cover.png", b"<script>alert(1)</script>", "image/png")},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "PNG, JPEG, WebP, or GIF" in response.json()["detail"]
|
||||
|
||||
@patch.object(
|
||||
FSResourcesHandler,
|
||||
"store_artwork",
|
||||
new_callable=AsyncMock,
|
||||
return_value=("path/to/big.png", "path/to/small.png"),
|
||||
)
|
||||
def test_add_collection_artwork_uses_detected_extension(
|
||||
self,
|
||||
store_artwork_mock: AsyncMock,
|
||||
client,
|
||||
access_token: str,
|
||||
):
|
||||
response = client.post(
|
||||
"/api/collections",
|
||||
data={"name": "Good Cover Collection"},
|
||||
files={"artwork": ("payload.html", _PNG_BYTES, "image/png")},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert store_artwork_mock.called
|
||||
_, _, file_ext = store_artwork_mock.call_args.args
|
||||
assert file_ext == "png"
|
||||
|
||||
def test_update_collection_rejects_non_image_artwork(
|
||||
self, client, access_token: str, collection: Collection
|
||||
):
|
||||
response = client.put(
|
||||
f"/api/collections/{collection.id}",
|
||||
data={"rom_ids": "[]"},
|
||||
files={"artwork": ("cover.png", b"<script>alert(1)</script>", "image/png")},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "PNG, JPEG, WebP, or GIF" in response.json()["detail"]
|
||||
|
||||
@patch.object(
|
||||
FSResourcesHandler,
|
||||
"store_artwork",
|
||||
new_callable=AsyncMock,
|
||||
return_value=("path/to/big.png", "path/to/small.png"),
|
||||
)
|
||||
def test_update_collection_artwork_uses_detected_extension(
|
||||
self,
|
||||
store_artwork_mock: AsyncMock,
|
||||
client,
|
||||
access_token: str,
|
||||
collection: Collection,
|
||||
):
|
||||
response = client.put(
|
||||
f"/api/collections/{collection.id}",
|
||||
data={"rom_ids": "[]"},
|
||||
files={"artwork": ("payload.html", _PNG_BYTES, "image/png")},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert store_artwork_mock.called
|
||||
_, _, file_ext = store_artwork_mock.call_args.args
|
||||
assert file_ext == "png"
|
||||
|
||||
Reference in New Issue
Block a user