Files
intercept/tests/test_weather_sat_routes.py
James Smith cbe7f591e3 Fix weather sat test failures: login auth, timer refs, and SDR device mocking
- Add authenticated client fixture to test_weather_sat_routes.py so
  require_login() before_request doesn't redirect test clients to /login
- Save timer mock references before disable()/skip_pass() clear _timer = None
- Patch app.claim_sdr_device to return None in execute_capture and
  scheduling cycle tests to avoid real USB hardware probing in CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 20:56:01 +00:00

881 lines
35 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 'data/ directory' 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']