Files
gps-denied-onboard/test_01_03_feature_user_interaction.py
T
Denys Zaitsev d7e1066c60 Initial commit
2026-04-03 23:25:54 +03:00

200 lines
9.6 KiB
Python

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()