from app.core.frz import point_inside_swansea_frz from app.models.drone_request import DroneRequest def drone_payload(**overrides): payload = { "operator_name": "Rotor Ops", "operator_id": "OP-123", "flyer_name": "Remote Pilot", "flyer_id": "FLY-456", "email": "pilot@example.com", "phone": "0123456789", "flight_date": "2026-06-20", "estimated_takeoff_time": "10:00", "estimated_completion_time": "10:30", "estimated_takeoff_at": "2026-06-20T10:00:00", "estimated_completion_at": "2026-06-20T10:30:00", "maximum_elevation_ft_agl": 250, "location_description": "North apron", "location_latitude": 51.623389, "location_longitude": -4.069231, "location_inside_frz": "no", "notes": "Survey flight", "prototype_overlay": { "airport_reference_point": {"lat": 0, "lng": 0}, "frz_radius_metres": 1, }, } payload.update(overrides) return payload def test_public_drone_request_create_edit_cancel_and_journal(client, db, monkeypatch): sent_emails = [] async def fake_send_email(**kwargs): sent_emails.append(kwargs) return True monkeypatch.setattr("app.api.endpoints.drone_requests.email_service.send_email", fake_send_email) create_response = client.post("/api/v1/drone-requests/public", json=drone_payload()) assert create_response.status_code == 200 created = create_response.json() assert created["reference_number"].startswith("DRN-") assert created["status"] == "NEW" assert created["location_inside_frz"] is True assert created["created_by"] == "public" assert len(sent_emails) == 2 assert sent_emails[1]["to_email"] == "tower@swansea-airport.wales" assert sent_emails[1]["template_name"] == "drone_request_tower_notification.html" db_request = db.query(DroneRequest).filter(DroneRequest.id == created["id"]).one() assert db_request.public_token get_response = client.get(f"/api/v1/drone-requests/public/edit/{db_request.public_token}") patch_response = client.patch( f"/api/v1/drone-requests/public/edit/{db_request.public_token}", json={"operator_name": "Updated Rotor Ops", "notes": "Updated notes"}, ) cancel_response = client.delete(f"/api/v1/drone-requests/public/cancel/{db_request.public_token}") assert get_response.status_code == 200 assert patch_response.status_code == 200 assert patch_response.json()["operator_name"] == "Updated Rotor Ops" assert cancel_response.status_code == 200 assert cancel_response.json()["status"] == "CANCELED" assert len(sent_emails) == 3 blocked_patch = client.patch( f"/api/v1/drone-requests/public/edit/{db_request.public_token}", json={"operator_name": "Too Late"}, ) blocked_cancel = client.delete(f"/api/v1/drone-requests/public/cancel/{db_request.public_token}") assert blocked_patch.status_code == 400 assert blocked_cancel.status_code == 400 assert client.get("/api/v1/drone-requests/public/edit/missing-token").status_code == 404 assert client.patch("/api/v1/drone-requests/public/edit/missing-token", json={}).status_code == 404 assert client.delete("/api/v1/drone-requests/public/cancel/missing-token").status_code == 404 def test_drone_request_accepts_legacy_amsl_altitude_key(client, db, monkeypatch): async def fake_send_email(**kwargs): return True monkeypatch.setattr("app.api.endpoints.drone_requests.email_service.send_email", fake_send_email) payload = drone_payload() payload["maximum_elevation_ft_amsl"] = payload.pop("maximum_elevation_ft_agl") create_response = client.post("/api/v1/drone-requests/public", json=payload) assert create_response.status_code == 200 assert create_response.json()["maximum_elevation_ft_agl"] == 250 db_request = db.query(DroneRequest).filter(DroneRequest.id == create_response.json()["id"]).one() assert db_request.maximum_elevation_ft_agl == 250 def test_authenticated_drone_request_list_update_status_comment_and_journal(auth_client, db, monkeypatch): sent_emails = [] async def fake_send_email(**kwargs): sent_emails.append(kwargs) return True monkeypatch.setattr("app.api.endpoints.drone_requests.email_service.send_email", fake_send_email) create_response = auth_client.post("/api/v1/drone-requests/public", json=drone_payload()) created = create_response.json() list_response = auth_client.get( "/api/v1/drone-requests/", params={"status": "NEW", "date_from": "2026-06-20", "date_to": "2026-06-20"}, ) get_response = auth_client.get(f"/api/v1/drone-requests/{created['id']}") update_response = auth_client.patch( f"/api/v1/drone-requests/{created['id']}", json={"operator_comments": "Needs tower review", "maximum_elevation_ft_agl": 200}, ) status_response = auth_client.patch( f"/api/v1/drone-requests/{created['id']}/status", json={"status": "APPROVED", "comment": "Approved below 200ft"}, ) comment_response = auth_client.post( f"/api/v1/drone-requests/{created['id']}/comments", json={"comment": "Call tower before launch", "email_applicant": True}, ) journal_response = auth_client.get(f"/api/v1/drone-requests/{created['id']}/journal") assert list_response.status_code == 200 assert [request["id"] for request in list_response.json()] == [created["id"]] assert get_response.status_code == 200 assert update_response.status_code == 200 assert update_response.json()["maximum_elevation_ft_agl"] == 200 assert status_response.status_code == 200 assert status_response.json()["status"] == "APPROVED" assert status_response.json()["operator_comments"] == "Approved below 200ft" assert comment_response.status_code == 200 assert comment_response.json()["operator_comments"] == "Call tower before launch" assert journal_response.status_code == 200 entries = [entry["entry"] for entry in journal_response.json()] assert any("Drone request" in entry and "created" in entry for entry in entries) assert any("Status changed from NEW to APPROVED" in entry for entry in entries) assert any("Comment added" in entry for entry in entries) assert len(sent_emails) == 4 def test_drone_request_not_found_and_validation_paths(auth_client, client): invalid_response = client.post( "/api/v1/drone-requests/public", json=drone_payload(operator_name=" ", location_latitude=100, maximum_elevation_ft_agl=-1), ) assert invalid_response.status_code == 422 assert auth_client.get("/api/v1/drone-requests/404").status_code == 404 assert auth_client.patch("/api/v1/drone-requests/404", json={"operator_name": "Missing"}).status_code == 404 assert auth_client.patch("/api/v1/drone-requests/404/status", json={"status": "APPROVED"}).status_code == 404 assert ( auth_client.post( "/api/v1/drone-requests/404/comments", json={"comment": "Missing", "email_applicant": False}, ).status_code == 404 ) assert auth_client.get("/api/v1/drone-requests/404/journal").status_code == 404 def test_swansea_frz_runway_extensions_start_at_thresholds(): assert point_inside_swansea_frz(51.626825, -4.037672) is True assert point_inside_swansea_frz(51.583775, -4.097928) is True assert point_inside_swansea_frz(51.603007, -4.025604) is True assert point_inside_swansea_frz(51.607593, -4.109996) is True assert point_inside_swansea_frz(51.68000, -4.06780) is False