fix: ruff lint + format fixes in tests, ESLint fixes in frontend
This commit is contained in:
+64
-28
@@ -1,7 +1,6 @@
|
||||
"""Tests for file upload, list, download, delete, MIME validation, size limit, and thumbnail generation."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -12,16 +11,14 @@ from httpx import ASGITransport, AsyncClient
|
||||
from PIL import Image
|
||||
|
||||
from app.config import settings
|
||||
from app.database import Base, get_db
|
||||
from app.database import get_db
|
||||
from app.main import app
|
||||
from app.models.file import File
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.vehicle import Vehicle
|
||||
from app.services.auth_service import hash_password
|
||||
|
||||
|
||||
# ---- Test fixtures ----
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def sample_vehicle_data():
|
||||
"""Valid vehicle data for creation."""
|
||||
@@ -104,6 +101,7 @@ async def _create_test_vehicle(db_session) -> uuid.UUID:
|
||||
|
||||
# ---- Tests: File Upload ----
|
||||
|
||||
|
||||
class TestFileUpload:
|
||||
"""POST /api/v1/vehicles/:id/files tests."""
|
||||
|
||||
@@ -172,7 +170,9 @@ class TestFileUpload:
|
||||
assert data["thumbnail_path"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_invalid_mime_type_returns_422(self, admin_client, created_vehicle):
|
||||
async def test_upload_invalid_mime_type_returns_422(
|
||||
self, admin_client, created_vehicle
|
||||
):
|
||||
"""Upload a file with an unsupported MIME type and verify 422."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
files = {"file": ("malware.exe", b"MZ\x90\x00", "application/x-msdownload")}
|
||||
@@ -196,7 +196,9 @@ class TestFileUpload:
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_oversized_file_returns_413(self, admin_client, created_vehicle):
|
||||
async def test_upload_oversized_file_returns_413(
|
||||
self, admin_client, created_vehicle
|
||||
):
|
||||
"""Upload a file larger than 20MB and verify 413."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
# Create a 21MB file (21 * 1024 * 1024 bytes)
|
||||
@@ -223,7 +225,9 @@ class TestFileUpload:
|
||||
assert response.status_code == 404, response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_without_auth_returns_401(self, test_session_factory, created_vehicle):
|
||||
async def test_upload_without_auth_returns_401(
|
||||
self, test_session_factory, created_vehicle
|
||||
):
|
||||
"""Upload without authentication and verify 401."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
image_bytes = _make_image_bytes()
|
||||
@@ -243,7 +247,9 @@ class TestFileUpload:
|
||||
|
||||
app.dependency_overrides[get_db] = _override_get_db
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as unauth_client:
|
||||
async with AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
) as unauth_client:
|
||||
response = await unauth_client.post(
|
||||
f"/api/v1/vehicles/{vehicle_id}/files",
|
||||
files=files,
|
||||
@@ -254,11 +260,14 @@ class TestFileUpload:
|
||||
|
||||
# ---- Tests: File List ----
|
||||
|
||||
|
||||
class TestFileList:
|
||||
"""GET /api/v1/vehicles/:id/files tests."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_files_returns_200_with_pagination(self, admin_client, created_vehicle):
|
||||
async def test_list_files_returns_200_with_pagination(
|
||||
self, admin_client, created_vehicle
|
||||
):
|
||||
"""List files for a vehicle returns 200 with paginated response."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
# Upload a file first
|
||||
@@ -284,9 +293,7 @@ class TestFileList:
|
||||
async def test_list_files_empty_returns_200(self, admin_client, created_vehicle):
|
||||
"""List files for a vehicle with no files returns 200 with empty list."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
response = await admin_client.get(
|
||||
f"/api/v1/vehicles/{vehicle_id}/files"
|
||||
)
|
||||
response = await admin_client.get(f"/api/v1/vehicles/{vehicle_id}/files")
|
||||
assert response.status_code == 200, response.text
|
||||
data = response.json()
|
||||
assert data["total"] == 0
|
||||
@@ -302,6 +309,7 @@ class TestFileList:
|
||||
|
||||
# ---- Tests: File Download ----
|
||||
|
||||
|
||||
class TestFileDownload:
|
||||
"""GET /api/v1/vehicles/:id/files/:fileId tests."""
|
||||
|
||||
@@ -325,7 +333,9 @@ class TestFileDownload:
|
||||
assert len(response.content) == len(image_bytes)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_nonexistent_file_returns_404(self, admin_client, created_vehicle):
|
||||
async def test_download_nonexistent_file_returns_404(
|
||||
self, admin_client, created_vehicle
|
||||
):
|
||||
"""Download a non-existent file returns 404."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
fake_file_id = str(uuid.uuid4())
|
||||
@@ -337,6 +347,7 @@ class TestFileDownload:
|
||||
|
||||
# ---- Tests: File Delete ----
|
||||
|
||||
|
||||
class TestFileDelete:
|
||||
"""DELETE /api/v1/vehicles/:id/files/:fileId tests."""
|
||||
|
||||
@@ -361,14 +372,14 @@ class TestFileDelete:
|
||||
assert data["id"] == file_id
|
||||
|
||||
# Verify file is gone from list
|
||||
list_resp = await admin_client.get(
|
||||
f"/api/v1/vehicles/{vehicle_id}/files"
|
||||
)
|
||||
list_resp = await admin_client.get(f"/api/v1/vehicles/{vehicle_id}/files")
|
||||
assert list_resp.status_code == 200
|
||||
assert list_resp.json()["total"] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_file_returns_404(self, admin_client, created_vehicle):
|
||||
async def test_delete_nonexistent_file_returns_404(
|
||||
self, admin_client, created_vehicle
|
||||
):
|
||||
"""Delete a non-existent file returns 404."""
|
||||
vehicle_id = created_vehicle["id"]
|
||||
fake_file_id = str(uuid.uuid4())
|
||||
@@ -380,77 +391,96 @@ class TestFileDelete:
|
||||
|
||||
# ---- Tests: MIME Type Validation ----
|
||||
|
||||
|
||||
class TestMIMEValidation:
|
||||
"""Unit tests for MIME type validation."""
|
||||
|
||||
def test_validate_jpeg_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("image/jpeg", "photo.jpg") is True
|
||||
assert validate_mime_type("image/jpeg", "photo.jpeg") is True
|
||||
|
||||
def test_validate_png_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("image/png", "photo.png") is True
|
||||
|
||||
def test_validate_webp_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("image/webp", "photo.webp") is True
|
||||
|
||||
def test_validate_pdf_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("application/pdf", "doc.pdf") is True
|
||||
|
||||
def test_validate_doc_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("application/msword", "doc.doc") is True
|
||||
|
||||
def test_validate_docx_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
assert validate_mime_type(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"doc.docx",
|
||||
) is True
|
||||
|
||||
assert (
|
||||
validate_mime_type(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"doc.docx",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_reject_exe_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("application/x-msdownload", "malware.exe") is False
|
||||
|
||||
def test_reject_text_mime_type(self):
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("text/plain", "notes.txt") is False
|
||||
|
||||
def test_reject_mismatched_extension(self):
|
||||
"""MIME type image/jpeg with .png extension should fail."""
|
||||
from app.services.file_service import validate_mime_type
|
||||
|
||||
assert validate_mime_type("image/jpeg", "photo.png") is False
|
||||
|
||||
|
||||
# ---- Tests: File Size Validation ----
|
||||
|
||||
|
||||
class TestFileSizeValidation:
|
||||
"""Unit tests for file size validation."""
|
||||
|
||||
def test_validate_small_file_size(self):
|
||||
from app.services.file_service import validate_file_size
|
||||
|
||||
assert validate_file_size(1024, max_size_mb=20) is True
|
||||
|
||||
def test_validate_exact_20mb_file_size(self):
|
||||
from app.services.file_service import validate_file_size
|
||||
|
||||
exact_20mb = 20 * 1024 * 1024
|
||||
assert validate_file_size(exact_20mb, max_size_mb=20) is True
|
||||
|
||||
def test_reject_oversized_file(self):
|
||||
from app.services.file_service import validate_file_size
|
||||
|
||||
over_20mb = 20 * 1024 * 1024 + 1
|
||||
assert validate_file_size(over_20mb, max_size_mb=20) is False
|
||||
|
||||
def test_validate_zero_byte_file(self):
|
||||
from app.services.file_service import validate_file_size
|
||||
|
||||
assert validate_file_size(0, max_size_mb=20) is True
|
||||
|
||||
|
||||
# ---- Tests: Thumbnail Generation ----
|
||||
|
||||
|
||||
class TestThumbnailGeneration:
|
||||
"""Tests for thumbnail generation utility."""
|
||||
|
||||
@@ -534,6 +564,7 @@ class TestThumbnailGeneration:
|
||||
def test_is_image_mime_type(self):
|
||||
"""Test is_image_mime_type helper."""
|
||||
from app.utils.thumbnails import is_image_mime_type
|
||||
|
||||
assert is_image_mime_type("image/jpeg") is True
|
||||
assert is_image_mime_type("image/png") is True
|
||||
assert is_image_mime_type("image/webp") is True
|
||||
@@ -543,6 +574,7 @@ class TestThumbnailGeneration:
|
||||
|
||||
# ---- Tests: File Service Unit Tests ----
|
||||
|
||||
|
||||
class TestFileServiceUnit:
|
||||
"""Unit tests for file service functions."""
|
||||
|
||||
@@ -622,7 +654,9 @@ class TestFileServiceUnit:
|
||||
file_path = Path(file_record.file_path)
|
||||
assert file_path.exists()
|
||||
|
||||
deleted = await file_service.delete_file(db_session, vehicle_id, file_record.id)
|
||||
deleted = await file_service.delete_file(
|
||||
db_session, vehicle_id, file_record.id
|
||||
)
|
||||
assert deleted is not None
|
||||
assert not file_path.exists()
|
||||
|
||||
@@ -631,9 +665,7 @@ class TestFileServiceUnit:
|
||||
"""Test that delete_file returns None for non-existent file."""
|
||||
from app.services import file_service
|
||||
|
||||
result = await file_service.delete_file(
|
||||
db_session, uuid.uuid4(), uuid.uuid4()
|
||||
)
|
||||
result = await file_service.delete_file(db_session, uuid.uuid4(), uuid.uuid4())
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -682,7 +714,9 @@ class TestFileServiceUnit:
|
||||
mime_type="image/jpeg",
|
||||
)
|
||||
|
||||
retrieved = await file_service.get_file(db_session, vehicle_id, file_record.id)
|
||||
retrieved = await file_service.get_file(
|
||||
db_session, vehicle_id, file_record.id
|
||||
)
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == file_record.id
|
||||
assert retrieved.original_filename == "test.jpg"
|
||||
@@ -704,5 +738,7 @@ class TestFileServiceUnit:
|
||||
)
|
||||
|
||||
wrong_vehicle_id = uuid.uuid4()
|
||||
retrieved = await file_service.get_file(db_session, wrong_vehicle_id, file_record.id)
|
||||
retrieved = await file_service.get_file(
|
||||
db_session, wrong_vehicle_id, file_record.id
|
||||
)
|
||||
assert retrieved is None
|
||||
|
||||
Reference in New Issue
Block a user