import pytest import io from fastapi.testclient import TestClient from unittest.mock import Mock from fastapi import FastAPI from f01_flight_api import router, get_lifecycle_manager from f02_1_flight_lifecycle_manager import FlightLifecycleManager # --- Setup --- app = FastAPI() app.include_router(router) mock_manager = Mock(spec=FlightLifecycleManager) app.dependency_overrides[get_lifecycle_manager] = lambda: mock_manager client = TestClient(app) @pytest.fixture(autouse=True) def reset_mocks(): mock_manager.reset_mock() mock_manager.queue_images.return_value = True def create_dummy_file(filename: str, content: bytes = b"dummy_image_data", content_type: str = "image/jpeg"): return ("images", (filename, content, content_type)) # --- Unit Tests --- class TestUnitImageUpload: """Unit tests defined in 01.02_feature_image_upload.md""" def test_batch_size_validation(self): flight_id = "flight_123" # 1. Too few (9 images) -> 400 files_too_few = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(1, 10)] resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 9, "batch_number": 1}, files=files_too_few) assert resp.status_code == 400 assert "between 10 and 50" in resp.json()["detail"] # 2. Too many (51 images) -> 400 files_too_many = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(1, 52)] resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 51, "batch_number": 1}, files=files_too_many) assert resp.status_code == 400 # 3. Exactly 10 (Accepted) files_valid = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(1, 11)] resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 10, "batch_number": 1}, files=files_valid) assert resp.status_code == 202 assert resp.json()["accepted"] is True assert len(resp.json()["sequences"]) == 10 def test_sequence_validation(self): flight_id = "flight_123" files = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(100, 110)] # 1. Valid sequence resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 100, "end_sequence": 109, "batch_number": 1}, files=files) assert resp.status_code == 202 # 2. Invalid sequence (start > end) resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 110, "end_sequence": 100, "batch_number": 1}, files=files) assert resp.status_code == 400 # 3. Gap in sequence (mismatch between count and sequence span) resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 100, "end_sequence": 115, "batch_number": 1}, files=files) assert resp.status_code == 400 def test_image_format_validation(self): flight_id = "flight_123" # 1. Invalid format (Text file) files = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(1, 10)] files.append(create_dummy_file("AD000010.txt", content_type="text/plain")) resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 10, "batch_number": 1}, files=files) assert resp.status_code == 400 assert "Invalid image format" in resp.json()["detail"] def test_size_limits(self): flight_id = "flight_123" # Simulate 500MB+ payload by mocking read # FastAPI validates the actual bytes read. We will construct 10 files of 51MB each large_content = b"0" * (51 * 1024 * 1024) files = [create_dummy_file(f"AD{i:06d}.jpg", content=large_content) for i in range(1, 11)] resp = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 10, "batch_number": 1}, files=files) assert resp.status_code == 413 assert "exceeds 500MB" in resp.json()["detail"] # --- Integration Tests --- class TestIntegrationImageUpload: """Integration tests defined in 01.02_feature_image_upload.md""" def test_sequential_batch_uploads(self): flight_id = "flight_123" # Batch 1 (1-15) files_1 = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(1, 16)] resp1 = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 15, "batch_number": 1}, files=files_1) assert resp1.status_code == 202 assert resp1.json()["next_expected"] == 16 # Batch 2 (16-30) files_2 = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(16, 31)] resp2 = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 16, "end_sequence": 30, "batch_number": 2}, files=files_2) assert resp2.status_code == 202 assert resp2.json()["next_expected"] == 31 assert mock_manager.queue_images.call_count == 2 def test_concurrent_uploads_to_same_flight(self): """Simulates parallel handling (FastAPI inherently handles concurrent requests).""" flight_id = "flight_123" files_1 = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(1, 11)] files_2 = [create_dummy_file(f"AD{i:06d}.jpg") for i in range(11, 21)] r1 = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 1, "end_sequence": 10, "batch_number": 1}, files=files_1) r2 = client.post(f"/api/v1/flights/{flight_id}/images/batch", data={"start_sequence": 11, "end_sequence": 20, "batch_number": 2}, files=files_2) assert r1.status_code == 202 assert r2.status_code == 202 assert mock_manager.queue_images.call_count == 2