Files
intercept/tests/test_weather_sat_routes.py
T
James Smith d4652017f5 fix: stabilize test suite and repair frontend/backend wiring
- meshcore pin >=2.3.0 (EventType.STATS_CORE floor); setup.sh derives
  optional packages from requirements.txt; Python 3.10 warning
- agent-mode wifi clients proxy route + bare-array response handling
- remove dead AIS/ACARS/VDL2 SPA wiring and orphaned partials/CSS
- agent TLE download to data/tle/ (was littering repo root as gp.php)
- gate deferred background init off under pytest (mock-pollution race)
- complete Popen mocks (context manager protocol, communicate tuples)
- real pipe fds in weather-sat decoder tests (fd 10/11 collision caused
  10s SQLite stalls); satellite tests no longer rewrite data/satellites.py
- register 'live' pytest marker, excluded by default
- update stale test assertions to current APIs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:42:33 +01:00

842 lines
34 KiB
Python

"""Tests for weather satellite routes.
Covers all weather_sat endpoints: /status, /satellites, /start, /test-decode,
/stop, /images, /passes, and scheduler endpoints.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from utils.weather_sat import WeatherSatImage
@pytest.fixture
def client(client):
"""Authenticated client for weather-sat route tests."""
with client.session_transaction() as sess:
sess["logged_in"] = True
return client
class TestWeatherSatRoutes:
"""Tests for weather satellite routes."""
def test_get_status(self, client):
"""GET /weather-sat/status returns decoder status."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_decoder.get_status.return_value = {
"available": True,
"decoder": "satdump",
"running": False,
"satellite": "",
"frequency": 0.0,
"mode": "",
"elapsed_seconds": 0,
"image_count": 0,
}
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/status")
assert response.status_code == 200
data = response.get_json()
assert data["available"] is True
assert data["decoder"] == "satdump"
assert data["running"] is False
def test_list_satellites(self, client):
"""GET /weather-sat/satellites returns satellite list."""
response = client.get("/weather-sat/satellites")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert "satellites" in data
assert len(data["satellites"]) > 0
# Check structure
sat = data["satellites"][0]
assert "key" in sat
assert "name" in sat
assert "frequency" in sat
assert "mode" in sat
assert "description" in sat
assert "active" in sat
# Verify NOAA-18 is in list
noaa_18 = next((s for s in data["satellites"] if s["key"] == "NOAA-18"), None)
assert noaa_18 is not None
assert noaa_18["frequency"] == 137.9125
assert noaa_18["mode"] == "APT"
def test_start_capture_success(self, client):
"""POST /weather-sat/start successfully starts capture."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("routes.weather_sat.queue.Queue"),
patch("app.claim_sdr_device", return_value=None),
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = (True, None)
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"device": 0,
"gain": 40.0,
"bias_t": False,
}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "started"
assert data["satellite"] == "NOAA-18"
assert data["frequency"] == 137.9125
assert data["mode"] == "APT"
assert data["device"] == 0
mock_decoder.start.assert_called_once()
call_kwargs = mock_decoder.start.call_args[1]
assert call_kwargs["satellite"] == "NOAA-18"
assert call_kwargs["device_index"] == 0
assert call_kwargs["gain"] == 40.0
assert call_kwargs["bias_t"] is False
def test_start_capture_no_satdump(self, client):
"""POST /weather-sat/start returns error when SatDump unavailable."""
with patch("routes.weather_sat.is_weather_sat_available", return_value=False):
payload = {"satellite": "NOAA-18"}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "SatDump not installed" in data["message"]
def test_start_capture_already_running(self, client):
"""POST /weather-sat/start when already running."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
):
mock_decoder = MagicMock()
mock_decoder.is_running = True
mock_decoder.current_satellite = "NOAA-19"
mock_decoder.current_frequency = 137.100
mock_get.return_value = mock_decoder
payload = {"satellite": "NOAA-18"}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "already_running"
assert data["satellite"] == "NOAA-19"
def test_start_capture_invalid_satellite(self, client):
"""POST /weather-sat/start with invalid satellite."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {"satellite": "FAKE-SAT-99"}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "Invalid satellite" in data["message"]
def test_start_capture_invalid_device(self, client):
"""POST /weather-sat/start with invalid device index."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {"satellite": "NOAA-18", "device": -1}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
def test_start_capture_invalid_gain(self, client):
"""POST /weather-sat/start with invalid gain."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {"satellite": "NOAA-18", "gain": 999}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
def test_start_capture_device_busy(self, client):
"""POST /weather-sat/start when SDR device is busy."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("app.claim_sdr_device", return_value="Device busy with pager"),
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {"satellite": "NOAA-18"}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 409
data = response.get_json()
assert data["status"] == "error"
assert data["error_type"] == "DEVICE_BUSY"
assert "Device busy" in data["message"]
def test_start_capture_rtl_tcp_success(self, client):
"""POST /weather-sat/start with rtl_tcp remote SDR."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("app.claim_sdr_device") as mock_claim,
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = (True, None)
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"device": 0,
"gain": 40.0,
"rtl_tcp_host": "192.168.1.100",
"rtl_tcp_port": 1234,
}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "started"
# Device claim should NOT be called for remote SDR
mock_claim.assert_not_called()
# Verify rtl_tcp params passed to decoder
mock_decoder.start.assert_called_once()
call_kwargs = mock_decoder.start.call_args
assert call_kwargs[1]["rtl_tcp_host"] == "192.168.1.100"
assert call_kwargs[1]["rtl_tcp_port"] == 1234
def test_start_capture_rtl_tcp_invalid_host(self, client):
"""POST /weather-sat/start with invalid rtl_tcp host."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"rtl_tcp_host": "not a valid host!@#",
}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
def test_start_capture_start_failure(self, client):
"""POST /weather-sat/start when decoder.start() fails."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("app.claim_sdr_device", return_value=None),
):
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = (False, "SatDump exited immediately (code 1)")
mock_get.return_value = mock_decoder
payload = {"satellite": "NOAA-18"}
response = client.post("/weather-sat/start", data=json.dumps(payload), content_type="application/json")
assert response.status_code == 500
data = response.get_json()
assert data["status"] == "error"
assert "SatDump exited immediately" in data["message"]
def test_test_decode_success(self, client):
"""POST /weather-sat/test-decode successfully starts file decode."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("pathlib.Path.is_file", return_value=True),
patch("pathlib.Path.resolve") as mock_resolve,
):
# Mock path resolution to be under data/
mock_path = MagicMock()
mock_path.is_relative_to.return_value = True
mock_resolve.return_value = mock_path
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start_from_file.return_value = (True, None)
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"input_file": "data/weather_sat/test.wav",
"sample_rate": 1000000,
}
response = client.post(
"/weather-sat/test-decode", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "started"
assert data["satellite"] == "NOAA-18"
assert data["source"] == "file"
def test_test_decode_invalid_path(self, client):
"""POST /weather-sat/test-decode with path outside data/."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("pathlib.Path.resolve") as mock_resolve,
):
# Mock path outside allowed directory
mock_path = MagicMock()
mock_path.is_relative_to.return_value = False
mock_resolve.return_value = mock_path
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"input_file": "/etc/passwd",
}
response = client.post(
"/weather-sat/test-decode", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 403
data = response.get_json()
assert data["status"] == "error"
assert "must be under INTERCEPT data" in data["message"]
def test_test_decode_file_not_found(self, client):
"""POST /weather-sat/test-decode with non-existent file."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("pathlib.Path.is_file", return_value=False),
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_path = MagicMock()
mock_path.is_relative_to.return_value = True
mock_resolve.return_value = mock_path
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"input_file": "data/missing.wav",
}
response = client.post(
"/weather-sat/test-decode", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 404
data = response.get_json()
assert data["status"] == "error"
assert "not found" in data["message"].lower()
def test_test_decode_invalid_sample_rate(self, client):
"""POST /weather-sat/test-decode with invalid sample rate."""
with (
patch("routes.weather_sat.is_weather_sat_available", return_value=True),
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("pathlib.Path.is_file", return_value=True),
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_path = MagicMock()
mock_path.is_relative_to.return_value = True
mock_resolve.return_value = mock_path
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {
"satellite": "NOAA-18",
"input_file": "data/test.wav",
"sample_rate": 100, # Too low
}
response = client.post(
"/weather-sat/test-decode", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "sample_rate" in data["message"]
def test_stop_capture(self, client):
"""POST /weather-sat/stop stops capture."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_decoder.device_index = 0
mock_get.return_value = mock_decoder
response = client.post("/weather-sat/stop")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "stopped"
mock_decoder.stop.assert_called_once()
def test_list_images_empty(self, client):
"""GET /weather-sat/images with no images."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_decoder.get_images.return_value = []
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert data["images"] == []
assert data["count"] == 0
def test_list_images_with_data(self, client):
"""GET /weather-sat/images with images."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
image = WeatherSatImage(
filename="NOAA-18_test.png",
path=Path("/tmp/test.png"),
satellite="NOAA-18",
mode="APT",
timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
frequency=137.9125,
size_bytes=12345,
product="RGB Composite",
)
mock_decoder.get_images.return_value = [image]
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert data["count"] == 1
assert data["images"][0]["filename"] == "NOAA-18_test.png"
assert data["images"][0]["satellite"] == "NOAA-18"
def test_list_images_with_filter(self, client):
"""GET /weather-sat/images with satellite filter."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
image1 = WeatherSatImage(
filename="NOAA-18_test.png",
path=Path("/tmp/test1.png"),
satellite="NOAA-18",
mode="APT",
timestamp=datetime.now(timezone.utc),
frequency=137.9125,
)
image2 = WeatherSatImage(
filename="NOAA-19_test.png",
path=Path("/tmp/test2.png"),
satellite="NOAA-19",
mode="APT",
timestamp=datetime.now(timezone.utc),
frequency=137.100,
)
mock_decoder.get_images.return_value = [image1, image2]
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images?satellite=NOAA-18")
assert response.status_code == 200
data = response.get_json()
assert data["count"] == 1
assert data["images"][0]["satellite"] == "NOAA-18"
def test_list_images_with_limit(self, client):
"""GET /weather-sat/images with limit."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
images = [
WeatherSatImage(
filename=f"test{i}.png",
path=Path(f"/tmp/test{i}.png"),
satellite="NOAA-18",
mode="APT",
timestamp=datetime.now(timezone.utc),
frequency=137.9125,
)
for i in range(10)
]
mock_decoder.get_images.return_value = images
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images?limit=5")
assert response.status_code == 200
data = response.get_json()
assert data["count"] == 5
def test_get_image_success(self, client):
"""GET /weather-sat/images/<filename> serves image."""
with (
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("routes.weather_sat.send_file") as mock_send,
patch("pathlib.Path.exists", return_value=True),
):
mock_decoder = MagicMock()
mock_decoder._output_dir = Path("/tmp")
mock_get.return_value = mock_decoder
mock_send.return_value = MagicMock()
client.get("/weather-sat/images/test_image.png")
mock_send.assert_called_once()
call_args = mock_send.call_args
assert call_args[1]["mimetype"] == "image/png"
def test_get_image_invalid_filename(self, client):
"""GET /weather-sat/images/<filename> with invalid filename."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images/bad!file@name.png")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "Invalid filename" in data["message"]
def test_get_image_wrong_extension(self, client):
"""GET /weather-sat/images/<filename> with wrong extension."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images/test.txt")
assert response.status_code == 400
data = response.get_json()
assert "PNG/JPG" in data["message"]
def test_get_image_not_found(self, client):
"""GET /weather-sat/images/<filename> for non-existent image."""
with (
patch("routes.weather_sat.get_weather_sat_decoder") as mock_get,
patch("pathlib.Path.exists", return_value=False),
):
mock_decoder = MagicMock()
mock_decoder._output_dir = Path("/tmp")
mock_get.return_value = mock_decoder
response = client.get("/weather-sat/images/missing.png")
assert response.status_code == 404
def test_delete_image_success(self, client):
"""DELETE /weather-sat/images/<filename> deletes image."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_decoder.delete_image.return_value = True
mock_get.return_value = mock_decoder
response = client.delete("/weather-sat/images/test.png")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "deleted"
assert data["filename"] == "test.png"
def test_delete_image_not_found(self, client):
"""DELETE /weather-sat/images/<filename> for non-existent image."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_decoder.delete_image.return_value = False
mock_get.return_value = mock_decoder
response = client.delete("/weather-sat/images/missing.png")
assert response.status_code == 404
def test_delete_all_images(self, client):
"""DELETE /weather-sat/images deletes all images."""
with patch("routes.weather_sat.get_weather_sat_decoder") as mock_get:
mock_decoder = MagicMock()
mock_decoder.delete_all_images.return_value = 5
mock_get.return_value = mock_decoder
response = client.delete("/weather-sat/images")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert data["deleted"] == 5
def test_stream_progress(self, client):
"""GET /weather-sat/stream returns SSE stream."""
response = client.get("/weather-sat/stream")
assert response.status_code == 200
assert response.mimetype == "text/event-stream"
assert response.headers["Cache-Control"] == "no-cache"
def test_get_passes_missing_params(self, client):
"""GET /weather-sat/passes without required params."""
response = client.get("/weather-sat/passes")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "latitude and longitude" in data["message"]
def test_get_passes_invalid_coords(self, client):
"""GET /weather-sat/passes with invalid coordinates."""
response = client.get("/weather-sat/passes?latitude=999&longitude=0")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
def test_get_passes_success(self, client):
"""GET /weather-sat/passes successfully predicts passes."""
with patch("utils.weather_sat_predict.predict_passes") as mock_predict:
mock_predict.return_value = [
{
"id": "NOAA-18_202401011200",
"satellite": "NOAA-18",
"name": "NOAA 18",
"frequency": 137.9125,
"mode": "APT",
"startTime": "2024-01-01 12:00 UTC",
"startTimeISO": "2024-01-01T12:00:00+00:00",
"endTimeISO": "2024-01-01T12:15:00+00:00",
"maxEl": 45.0,
"maxElAz": 180.0,
"riseAz": 160.0,
"setAz": 200.0,
"duration": 15.0,
"quality": "good",
}
]
response = client.get("/weather-sat/passes?latitude=51.5&longitude=-0.1")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert data["count"] == 1
assert data["passes"][0]["satellite"] == "NOAA-18"
def test_get_passes_with_options(self, client):
"""GET /weather-sat/passes with trajectory and ground track."""
with patch("utils.weather_sat_predict.predict_passes") as mock_predict:
mock_predict.return_value = []
response = client.get(
"/weather-sat/passes?latitude=51.5&longitude=-0.1&"
"hours=48&min_elevation=20&trajectory=true&ground_track=true"
)
assert response.status_code == 200
mock_predict.assert_called_once()
call_kwargs = mock_predict.call_args[1]
assert call_kwargs["lat"] == 51.5
assert call_kwargs["lon"] == -0.1
assert call_kwargs["hours"] == 48
assert call_kwargs["min_elevation"] == 20.0
assert call_kwargs["include_trajectory"] is True
assert call_kwargs["include_ground_track"] is True
def test_get_passes_import_error(self, client):
"""GET /weather-sat/passes when skyfield not installed."""
with patch("utils.weather_sat_predict.predict_passes", side_effect=ImportError):
response = client.get("/weather-sat/passes?latitude=51.5&longitude=-0.1")
assert response.status_code == 503
data = response.get_json()
assert data["status"] == "error"
assert "skyfield" in data["message"]
def test_get_passes_prediction_error(self, client):
"""GET /weather-sat/passes when prediction fails."""
with patch("utils.weather_sat_predict.predict_passes", side_effect=Exception("TLE error")):
response = client.get("/weather-sat/passes?latitude=51.5&longitude=-0.1")
assert response.status_code == 500
data = response.get_json()
assert data["status"] == "error"
class TestWeatherSatScheduler:
"""Tests for weather satellite scheduler endpoints."""
def test_enable_schedule_success(self, client):
"""POST /weather-sat/schedule/enable enables scheduler."""
with patch("utils.weather_sat_scheduler.get_weather_sat_scheduler") as mock_get:
mock_scheduler = MagicMock()
mock_scheduler.enable.return_value = {
"enabled": True,
"observer": {"latitude": 51.5, "longitude": -0.1},
"device": 0,
"gain": 40.0,
"bias_t": False,
"min_elevation": 15.0,
"scheduled_count": 3,
"total_passes": 3,
}
mock_get.return_value = mock_scheduler
payload = {
"latitude": 51.5,
"longitude": -0.1,
"min_elevation": 15,
"device": 0,
"gain": 40.0,
"bias_t": False,
}
response = client.post(
"/weather-sat/schedule/enable", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert data["enabled"] is True
def test_enable_schedule_missing_coords(self, client):
"""POST /weather-sat/schedule/enable without coordinates."""
payload = {"device": 0}
response = client.post(
"/weather-sat/schedule/enable", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "latitude and longitude" in data["message"]
def test_enable_schedule_invalid_coords(self, client):
"""POST /weather-sat/schedule/enable with invalid coordinates."""
payload = {"latitude": 999, "longitude": 0}
response = client.post(
"/weather-sat/schedule/enable", data=json.dumps(payload), content_type="application/json"
)
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
def test_disable_schedule(self, client):
"""POST /weather-sat/schedule/disable disables scheduler."""
with patch("utils.weather_sat_scheduler.get_weather_sat_scheduler") as mock_get:
mock_scheduler = MagicMock()
mock_scheduler.disable.return_value = {"status": "disabled"}
mock_get.return_value = mock_scheduler
response = client.post("/weather-sat/schedule/disable")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "disabled"
def test_schedule_status(self, client):
"""GET /weather-sat/schedule/status returns scheduler status."""
with patch("utils.weather_sat_scheduler.get_weather_sat_scheduler") as mock_get:
mock_scheduler = MagicMock()
mock_scheduler.get_status.return_value = {
"enabled": False,
"observer": {"latitude": 0, "longitude": 0},
"device": 0,
"gain": 40.0,
"bias_t": False,
"min_elevation": 15.0,
"scheduled_count": 0,
"total_passes": 0,
}
mock_get.return_value = mock_scheduler
response = client.get("/weather-sat/schedule/status")
assert response.status_code == 200
data = response.get_json()
assert "enabled" in data
def test_schedule_passes(self, client):
"""GET /weather-sat/schedule/passes lists scheduled passes."""
with patch("utils.weather_sat_scheduler.get_weather_sat_scheduler") as mock_get:
mock_scheduler = MagicMock()
mock_scheduler.get_passes.return_value = [
{
"id": "NOAA-18_202401011200",
"satellite": "NOAA-18",
"status": "scheduled",
}
]
mock_get.return_value = mock_scheduler
response = client.get("/weather-sat/schedule/passes")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "ok"
assert data["count"] == 1
def test_skip_pass_success(self, client):
"""POST /weather-sat/schedule/skip/<id> skips a pass."""
with patch("utils.weather_sat_scheduler.get_weather_sat_scheduler") as mock_get:
mock_scheduler = MagicMock()
mock_scheduler.skip_pass.return_value = True
mock_get.return_value = mock_scheduler
response = client.post("/weather-sat/schedule/skip/NOAA-18_202401011200")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "skipped"
assert data["pass_id"] == "NOAA-18_202401011200"
def test_skip_pass_not_found(self, client):
"""POST /weather-sat/schedule/skip/<id> for non-existent pass."""
with patch("utils.weather_sat_scheduler.get_weather_sat_scheduler") as mock_get:
mock_scheduler = MagicMock()
mock_scheduler.skip_pass.return_value = False
mock_get.return_value = mock_scheduler
response = client.post("/weather-sat/schedule/skip/nonexistent")
assert response.status_code == 404
def test_skip_pass_invalid_id(self, client):
"""POST /weather-sat/schedule/skip/<id> with invalid ID."""
response = client.post("/weather-sat/schedule/skip/invalid!pass@id")
assert response.status_code == 400
data = response.get_json()
assert data["status"] == "error"
assert "Invalid pass ID" in data["message"]