mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 21:46:36 +00:00
200 lines
9.6 KiB
Python
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() |