""" Every radio on a call reaches the call doc (server-26#304) — from /upload, which starts the pipeline, and from call_end, which used to drop srcaddr. """ import json from unittest.mock import AsyncMock, patch import pytest from fastapi.testclient import TestClient from app.internal import radios from app.internal.mqtt_handler import MQTTHandler from app.main import app from app.routers import upload LIST = [{"radio": 555, "start": 0.0, "end": 1.0}, {"radio": 777, "start": 2.0, "end": 2.5}, {"radio": 555, "start": 3.5, "end": 4.0}] def test_parse_accepts_json_and_drops_junk(): raw = json.dumps(LIST + [{"radio": 0, "start": 1, "end": 2}, {"radio": "x"}, "junk", {"radio": 9, "start": -1, "end": 2}, {"radio": 8, "start": 5, "end": 4}]) assert radios.parse(raw) == LIST + [{"radio": 8, "start": 5.0, "end": 5.0}] assert radios.parse("not json") == [] assert radios.parse({"radio": 1}) == [] assert radios.parse(None) == [] def test_call_fields_keeps_distinct_ids_in_first_heard_order(): assert radios.call_fields(LIST)["radio_ids"] == [555, 777] assert radios.call_fields([]) == {} @pytest.mark.asyncio async def test_call_end_writes_srcaddr_and_radios(): """call_start used to be the only writer of srcaddr; the talker is usually learned later.""" handler = MQTTHandler() with patch("app.internal.mqtt_handler.fstore") as fs: fs.doc_get_cached = AsyncMock(return_value=None) fs.doc_set = AsyncMock() await handler._on_call_end("node-01", {"call_id": "c1", "srcaddr": 555, "radios": LIST}) updates = fs.doc_set.call_args[0][2] assert updates["srcaddr"] == 555 assert updates["radios"] == LIST assert updates["radio_ids"] == [555, 777] @pytest.mark.asyncio async def test_call_end_without_radios_leaves_them_alone(): """An older node sends neither field; a merge write must not blank what /upload stored.""" handler = MQTTHandler() with patch("app.internal.mqtt_handler.fstore") as fs: fs.doc_get_cached = AsyncMock(return_value=None) fs.doc_set = AsyncMock() await handler._on_call_end("node-01", {"call_id": "c1", "srcaddr": None}) updates = fs.doc_set.call_args[0][2] assert "srcaddr" not in updates and "radios" not in updates def _upload(existing_call, form_radios): store = { "node_keys": {"api_key": "k"}, "nodes": {"approval_status": "approved", "org_id": "org"}, "calls": existing_call, } writes = [] async def doc_get(c, i): return store.get(c) async def doc_set(c, i, data, merge=True): writes.append((c, data)) data = {"call_id": "c1", "node_id": "n1"} if form_radios is not None: data["radios"] = form_radios with patch.object(upload.fstore, "doc_get", doc_get), \ patch.object(upload.fstore, "doc_set", doc_set), \ patch.object(upload, "upload_audio", AsyncMock(return_value="gs://b/c1.mp3")), \ patch.object(upload.dedup, "find_duplicate_of", AsyncMock(return_value="other")): resp = TestClient(app).post( "/upload", data=data, files={"file": ("a.mp3", b"\x00\x01", "audio/mpeg")}, headers={"Authorization": "Bearer k"}, ) assert resp.status_code == 200 return writes[0][1] def test_upload_stores_the_radio_list_before_the_pipeline_runs(): first = _upload({}, json.dumps(LIST)) assert first["radios"] == LIST assert first["radio_ids"] == [555, 777] # call_start had no talker yet: take the latest one, as call_end would. assert first["srcaddr"] == 555 def test_upload_keeps_an_existing_srcaddr_and_tolerates_no_list(): assert "srcaddr" not in _upload({"srcaddr": 999}, json.dumps(LIST)) assert "radios" not in _upload({}, None)