import pytest import asyncio from fastapi.testclient import TestClient from unittest.mock import Mock, MagicMock from datetime import datetime from fastapi import FastAPI from f01_flight_api import router, get_lifecycle_manager, get_flight_database from f02_1_flight_lifecycle_manager import FlightLifecycleManager, Flight, GPSPoint, CameraParameters, FlightState, Waypoint # --- Setup --- app = FastAPI() app.include_router(router) mock_manager = Mock(spec=FlightLifecycleManager) mock_db = Mock() app.dependency_overrides[get_lifecycle_manager] = lambda: mock_manager app.dependency_overrides[get_flight_database] = lambda: mock_db client = TestClient(app) @pytest.fixture(autouse=True) def reset_mocks(): mock_manager.reset_mock() mock_db.reset_mock() # --- Unit Tests --- class TestUnitFlightManagement: """Unit tests defined in 01.01_feature_flight_management.md""" def test_create_flight_validation(self): valid_payload = { "name": "Test Flight", "description": "Validation test", "start_gps": {"lat": 48.0, "lon": 37.0}, "altitude": 400.0, "camera_params": {"focal_length_mm": 25.0, "sensor_width_mm": 36.0, "resolution": {"width": 6000, "height": 4000}} } # 1. Valid request mock_manager.create_flight.return_value = "flight_123" resp = client.post("/api/v1/flights", json=valid_payload) assert resp.status_code == 201 assert resp.json()["flight_id"] == "flight_123" # 2. Missing required field (name) invalid_payload = valid_payload.copy() del invalid_payload["name"] resp = client.post("/api/v1/flights", json=invalid_payload) assert resp.status_code == 422 # 3. Invalid camera params (missing required resolution) invalid_camera = valid_payload.copy() invalid_camera["camera_params"] = {"focal_length_mm": 25.0} resp = client.post("/api/v1/flights", json=invalid_camera) assert resp.status_code == 422 def test_get_flight(self): # 1. Valid flight_id mock_flight = Flight( flight_id="flight_123", flight_name="Test Mission", start_gps=GPSPoint(lat=48.0, lon=37.0), altitude_m=400.0, camera_params=CameraParameters(focal_length_mm=25, sensor_width_mm=23.5, resolution={"width": 6000, "height": 4000}) ) mock_manager.get_flight.return_value = mock_flight mock_manager.get_flight_state.return_value = FlightState(flight_id="flight_123", state="active", processed_images=10, total_images=100, has_active_engine=True) mock_db.get_waypoints.return_value = [] resp = client.get("/api/v1/flights/flight_123") assert resp.status_code == 200 assert resp.json()["flight_id"] == "flight_123" assert resp.json()["status"] == "active" # 2. Non-existent flight_id mock_manager.get_flight.return_value = None resp = client.get("/api/v1/flights/missing_flight") assert resp.status_code == 404 def test_delete_flight(self): # 1. Valid flight_id mock_manager.delete_flight.return_value = True resp = client.delete("/api/v1/flights/flight_123") assert resp.status_code == 200 assert resp.json()["deleted"] is True # 2. Non-existent flight_id mock_manager.delete_flight.return_value = False resp = client.delete("/api/v1/flights/missing_flight") assert resp.status_code == 404 def test_get_flight_status(self): # 1. Valid flight_id mock_manager.get_flight_state.return_value = FlightState(flight_id="flight_123", state="processing", processed_images=50, total_images=100, has_active_engine=True) resp = client.get("/api/v1/flights/flight_123/status") assert resp.status_code == 200 assert resp.json()["status"] == "processing" assert resp.json()["frames_processed"] == 50 # 2. Non-existent flight_id mock_manager.get_flight_state.return_value = None resp = client.get("/api/v1/flights/missing_flight/status") assert resp.status_code == 404 def test_update_waypoint(self): waypoint_payload = { "id": "wp1", "lat": 48.0, "lon": 37.0, "confidence": 0.9, "timestamp": datetime.utcnow().isoformat(), "refined": False } # 1. Valid update mock_db.update_waypoint.return_value = True resp = client.put("/api/v1/flights/flight_123/waypoints/wp1", json=waypoint_payload) assert resp.status_code == 200 assert resp.json()["updated"] is True # 2. Invalid waypoint_id mock_db.update_waypoint.return_value = False resp = client.put("/api/v1/flights/flight_123/waypoints/missing_wp", json=waypoint_payload) assert resp.status_code == 404 def test_batch_update_waypoints(self): payload = [ {"id": "wp1", "lat": 48.0, "lon": 37.0, "altitude": 400.0, "confidence": 0.9, "timestamp": datetime.utcnow().isoformat(), "refined": False}, {"id": "wp2", "lat": 48.1, "lon": 37.1, "altitude": 400.0, "confidence": 0.9, "timestamp": datetime.utcnow().isoformat(), "refined": False} ] # 1. Valid batch mock_db.batch_update_waypoints.return_value = Mock(failed_ids=[], updated_count=2) resp = client.put("/api/v1/flights/flight_123/waypoints/batch", json=payload) assert resp.status_code == 200 assert resp.json()["success"] is True assert resp.json()["updated_count"] == 2 # 2. Partial failures mock_db.batch_update_waypoints.return_value = Mock(failed_ids=["wp2"], updated_count=1) resp = client.put("/api/v1/flights/flight_123/waypoints/batch", json=payload) assert resp.status_code == 200 assert resp.json()["success"] is False assert "wp2" in resp.json()["failed_ids"] # --- Integration Tests --- class TestIntegrationFlightManagement: """Integration tests defined in 01.01_feature_flight_management.md""" def test_flight_lifecycle(self): """Tests POST -> GET -> DELETE lifecycle""" valid_payload = { "name": "Lifecycle Mission", "start_gps": {"lat": 48.0, "lon": 37.0}, "altitude": 400.0, "camera_params": {"focal_length_mm": 25.0, "sensor_width_mm": 36.0, "resolution": {"width": 640, "height": 480}} } mock_manager.create_flight.return_value = "lifecycle_123" post_resp = client.post("/api/v1/flights", json=valid_payload) assert post_resp.status_code == 201 mock_manager.get_flight.return_value = Flight(flight_id="lifecycle_123", flight_name="Lifecycle Mission", start_gps=GPSPoint(lat=48.0, lon=37.0), altitude_m=400.0, camera_params=CameraParameters(**valid_payload["camera_params"])) mock_manager.get_flight_state.return_value = FlightState(flight_id="lifecycle_123", state="prefetching", processed_images=0, total_images=0, has_active_engine=False) get_resp = client.get("/api/v1/flights/lifecycle_123") assert get_resp.status_code == 200 assert get_resp.json()["name"] == "Lifecycle Mission"