from sqlmodel import Session, SQLModel, create_engine from app.models import TerminalDeviceState, TerminalMessageRaw from app.mqtt.parser_worker import process_pending_batch from app.mqtt.repository import insert_raw_message def _session() -> Session: engine = create_engine("sqlite://") SQLModel.metadata.create_all(engine) return Session(engine) def test_insert_raw_message_sets_unparsed_defaults() -> None: with _session() as session: row = insert_raw_message( session=session, device_id="device-001", topic="terminal/device-001/raw", payload=b"$GPRMC,123520,A,3723.2475,N,12158.3416,W,0.13,309.62,120598,,,A*74\r\n", qos=1, retain=False, ) assert row.parsed_at is None assert row.parse_error is None assert row.payload_size > 0 assert len(row.payload_sha256) == 64 def test_worker_marks_success_and_updates_state() -> None: with _session() as session: raw = insert_raw_message( session=session, device_id="device-001", topic="terminal/device-001/raw", payload=b"$GPGGA,123519,3723.2475,N,12158.3416,W,1,12,0.8,10.0,M,-25.0,M,,*65\r\n", qos=1, retain=False, ) processed = process_pending_batch(session=session, batch_size=10) assert processed == 1 refreshed = session.get(TerminalMessageRaw, raw.id) assert refreshed is not None assert refreshed.parsed_at is not None assert refreshed.parse_error is None state = session.get(TerminalDeviceState, "device-001") assert state is not None assert state.lat is not None assert state.lon is not None def test_worker_marks_failed_without_retry_schedule(monkeypatch) -> None: def _raise_parse_error(_payload: bytes) -> list[dict[str, object]]: raise ValueError("boom") monkeypatch.setattr( "app.mqtt.parser_worker.parse_mixed_payload", _raise_parse_error ) with _session() as session: raw = insert_raw_message( session=session, device_id="device-err", topic="terminal/device-err/raw", payload=b"$GPGGA,broken*00\r\n", qos=1, retain=False, ) processed = process_pending_batch(session=session, batch_size=10) assert processed == 1 refreshed = session.get(TerminalMessageRaw, raw.id) assert refreshed is not None assert refreshed.parsed_at is not None assert refreshed.parse_error == "boom" processed_again = process_pending_batch(session=session, batch_size=10) assert processed_again == 0 def test_schema_excludes_terminal_observation_event_table() -> None: assert "terminal_observation_event" not in SQLModel.metadata.tables def test_ingest_and_parse_real_nmea_sample() -> None: from pathlib import Path sample_path = Path(__file__).parent / "nmea.txt" payload = sample_path.read_bytes() with _session() as session: raw = insert_raw_message( session=session, device_id="device-real-sample", topic="terminal/device-real-sample/raw", payload=payload, qos=1, retain=False, ) processed = process_pending_batch(session=session, batch_size=10) assert processed == 1 refreshed = session.get(TerminalMessageRaw, raw.id) assert refreshed is not None assert refreshed.parsed_at is not None assert refreshed.parse_error is None state = session.get(TerminalDeviceState, "device-real-sample") assert state is not None assert state.lat is not None assert state.lon is not None def test_worker_builds_satellite_distribution_summary() -> None: payload = ( b"$GNGGA,123519,3723.2475,N,12158.3416,W,4,12,0.8,10.0,M,-25.0,M,,*65\r\n" b"$GPGSA,A,3,02,05,12,,,,,,,,,,1.50,0.90,1.20*00\r\n" b"$GPGSV,1,1,04,02,17,213,40,05,29,121,37,12,66,314,44,25,14,048,39*71\r\n" ) with _session() as session: insert_raw_message( session=session, device_id="device-gsv", topic="terminal/device-gsv/raw", payload=payload, qos=1, retain=False, ) processed = process_pending_batch(session=session, batch_size=10) assert processed == 1 state = session.get(TerminalDeviceState, "device-gsv") assert state is not None assert state.fix_status == "4" summary = state.satellite_signal_summary assert summary is not None assert summary["satellites_in_view"] == 4 assert summary["snr_values"] == [40, 37, 44, 39] assert summary["used_satellite_ids"] == ["02", "05", "12"] assert summary["constellations"]["GPS"]["count"] == 4 assert summary["constellations"]["GPS"]["used_in_navigation"] == 3