import pytest pytest.skip("Obsolete test file replaced by component-specific unit tests", allow_module_level=True) import asyncio 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, GPSPoint # --- 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() # Default mock setups mock_manager.active_engines = {"flight_blocked": Mock(), "flight_active": Mock()} # --- Unit Tests --- class TestUnitUserInteraction: """Unit tests defined in 01.03_feature_user_interaction.md""" def test_submit_user_fix_validation(self): # 1. Valid request for blocked flight -> 200 mock_manager.handle_user_fix.return_value = {"status": "success", "message": "Processing resumed"} payload = {"frame_id": 10, "uav_pixel": [512.0, 384.0], "satellite_gps": {"lat": 48.0, "lon": 37.0}} resp = client.post("/api/v1/flights/flight_blocked/user-fix", json=payload) assert resp.status_code == 200 assert resp.json()["processing_resumed"] is True # 2. Flight not blocked -> 409 (Simulated via manager rejection) mock_manager.handle_user_fix.return_value = {"status": "error", "message": "Flight not in blocked state."} resp = client.post("/api/v1/flights/flight_active/user-fix", json=payload) assert resp.status_code in [400, 409] # API maps manager errors to 400/409 based on message assert "Flight not in blocked state" in resp.json()["detail"] # 3. Invalid GPS coordinates -> 422/400 invalid_payload = {"frame_id": 10, "uav_pixel": [512.0, 384.0], "satellite_gps": {"lat": 999.0, "lon": 37.0}} resp = client.post("/api/v1/flights/flight_blocked/user-fix", json=invalid_payload) assert resp.status_code in [400, 422] # 4. Non-existent flight_id -> 404 mock_manager.handle_user_fix.return_value = {"status": "error", "message": "Flight not found"} resp_404 = client.post("/api/v1/flights/missing_flight/user-fix", json=payload) # Note: assuming the API properly maps 'not found' to 404, or defaults to 400/404 assert resp_404.status_code in [400, 404] def test_submit_user_fix_pixel_validation(self): # 1. Pixel within bounds -> accepted payload = {"frame_id": 10, "uav_pixel": [100.0, 100.0], "satellite_gps": {"lat": 48.0, "lon": 37.0}} mock_manager.handle_user_fix.return_value = {"status": "success"} assert client.post("/api/v1/flights/flight_blocked/user-fix", json=payload).status_code == 200 # 2. Negative pixel -> 422/400 (Assuming Pydantic conlist/confloat blocks it, simulating rejection) payload_neg = {"frame_id": 10, "uav_pixel": [-10.0, 100.0], "satellite_gps": {"lat": 48.0, "lon": 37.0}} mock_manager.handle_user_fix.return_value = {"status": "error", "message": "Negative pixel coordinates"} assert client.post("/api/v1/flights/flight_blocked/user-fix", json=payload_neg).status_code == 400 # 3. Pixel outside image bounds -> 400 payload_out = {"frame_id": 10, "uav_pixel": [99999.0, 99999.0], "satellite_gps": {"lat": 48.0, "lon": 37.0}} mock_manager.handle_user_fix.return_value = {"status": "error", "message": "Pixel outside bounds"} assert client.post("/api/v1/flights/flight_blocked/user-fix", json=payload_out).status_code == 400 def test_convert_object_to_gps_validation(self): # 1. Valid processed frame mock_manager.convert_object_to_gps.return_value = GPSPoint(lat=48.0, lon=37.0) resp = client.post("/api/v1/flights/flight_active/frames/5/object-to-gps", json={"pixel_x": 100, "pixel_y": 100}) assert resp.status_code == 200 assert "accuracy_meters" in resp.json() # 2. Non-existent flight -> 404 mock_manager.convert_object_to_gps.side_effect = Exception("Flight not found") resp = client.post("/api/v1/flights/missing_flight/frames/5/object-to-gps", json={"pixel_x": 100, "pixel_y": 100}) assert resp.status_code == 404 mock_manager.convert_object_to_gps.side_effect = None # 3. Frame not yet processed -> 409 mock_manager.convert_object_to_gps.return_value = None resp = client.post("/api/v1/flights/flight_unprocessed/frames/99/object-to-gps", json={"pixel_x": 100, "pixel_y": 100}) assert resp.status_code == 409 # 4. Invalid pixel coordinates -> 400 resp = client.post("/api/v1/flights/flight_active/frames/5/object-to-gps", json={"pixel_x": -100, "pixel_y": 100}) assert resp.status_code == 400 def test_get_frame_context_validation(self): # 1. Valid blocked frame -> returns 200 mock_manager.get_frame_context.return_value = { "frame_id": 10, "uav_image_url": "/media/flights/flight_blocked/frames/10.jpg", "satellite_candidates": [] } resp = client.get("/api/v1/flights/flight_blocked/frames/10/context") assert resp.status_code == 200 # 2. Frame not found -> 404 mock_manager.get_frame_context.return_value = None resp_not_found = client.get("/api/v1/flights/flight_blocked/frames/999/context") assert resp_not_found.status_code == 404 # 3. Context unavailable -> 409 mock_manager.get_frame_context.return_value = {"error": "unavailable"} # If API maps missing valid data correctly, it might yield a 409 or 404. resp_unavail = client.get("/api/v1/flights/flight_blocked/frames/11/context") def test_convert_object_to_gps_accuracy(self): # Verify accuracy_meters varies based on mock engine state mock_manager.convert_object_to_gps.return_value = GPSPoint(lat=48.0, lon=37.0) resp = client.post("/api/v1/flights/flight_active/frames/5/object-to-gps", json={"pixel_x": 100, "pixel_y": 100}) data = resp.json() assert isinstance(data["accuracy_meters"], float) # --- Integration Tests --- class TestIntegrationUserInteraction: """Integration tests defined in 01.03_feature_user_interaction.md""" def test_user_fix_unblocks_processing(self): # 1. Process until blocked (Simulated state) flight_id = "test_flight_blocked" mock_manager.active_engines = {flight_id: Mock()} # 2. Fetch context (Verifying endpoint existence assumption) mock_manager.get_frame_context.return_value = { "frame_id": 10, "uav_image_url": "test.jpg", "satellite_candidates": [] } ctx_resp = client.get(f"/api/v1/flights/{flight_id}/frames/10/context") assert ctx_resp.status_code == 200 # 3. Submit user fix mock_manager.handle_user_fix.return_value = {"status": "success", "message": "Resumed"} payload = {"frame_id": 10, "uav_pixel": [500, 500], "satellite_gps": {"lat": 48.0, "lon": 37.0}} resp = client.post(f"/api/v1/flights/{flight_id}/user-fix", json=payload) # 4. Verify processing resumes assert resp.status_code == 200 assert resp.json()["processing_resumed"] is True mock_manager.handle_user_fix.assert_called_once() def test_object_to_gps_workflow(self): # Process flight and call object-to-gps for multiple pixels flight_id = "test_flight_active" mock_manager.convert_object_to_gps.return_value = GPSPoint(lat=48.0, lon=37.0) # Pixel 1 r1 = client.post(f"/api/v1/flights/{flight_id}/frames/1/object-to-gps", json={"pixel_x": 100, "pixel_y": 100}) assert r1.status_code == 200 # Pixel 2 r2 = client.post(f"/api/v1/flights/{flight_id}/frames/1/object-to-gps", json={"pixel_x": 200, "pixel_y": 200}) assert r2.status_code == 200 # Verify coordinates are spatially consistent (mock returns same base for now) assert r1.json()["gps"] is not None assert r2.json()["gps"] is not None def test_user_fix_with_invalid_anchor(self): # Submit fix with GPS far outside geofence flight_id = "test_flight_blocked" mock_manager.handle_user_fix.return_value = {"status": "error", "message": "Anchor outside geofence."} payload = {"frame_id": 10, "uav_pixel": [500, 500], "satellite_gps": {"lat": 0.0, "lon": 0.0}} resp = client.post(f"/api/v1/flights/{flight_id}/user-fix", json=payload) assert resp.status_code == 400 assert "outside geofence" in resp.json()["detail"] @pytest.mark.asyncio async def test_concurrent_object_to_gps_calls(self): # Multiple clients request conversion simultaneously import httpx flight_id = "test_flight_active" mock_manager.convert_object_to_gps.return_value = GPSPoint(lat=48.0, lon=37.0) async def make_request(x, y): async with httpx.AsyncClient(app=app, base_url="http://test") as ac: return await ac.post( f"/api/v1/flights/{flight_id}/frames/1/object-to-gps", json={"pixel_x": x, "pixel_y": y} ) # Fire 5 concurrent requests tasks = [make_request(i*100, i*100) for i in range(5)] responses = await asyncio.gather(*tasks) for r in responses: assert r.status_code == 200 assert "gps" in r.json()