from datetime import datetime, timedelta import pytest from fastapi import HTTPException from banban.service.device import DeviceService from banban.service.ota_firmware import OtaFirmwareSelection from banban.service.device_setting import DeviceSettingService @pytest.mark.asyncio async def test_firmware_status_selects_gps_channel(monkeypatch): service = DeviceService() async def fake_get_device_status(**kwargs): del kwargs return { "device_id": "TalkingQ_XQSN00001005", "version": "1.0.0", "device_type": 2, "ota_channel": None, } async def fake_select_latest_for_device(status_row): assert status_row["device_type"] == 2 return OtaFirmwareSelection( channel_key="gps", version="1.1.0", firmware_url="https://example.com/gps-1.1.0.bin", firmware_id=10, force_update=True, ) async def fake_get_firmware_update_dict(device_id): del device_id return None monkeypatch.setattr(service, "get_device_status", fake_get_device_status) monkeypatch.setattr("banban.service.device.ota_firmware_service.select_latest_for_device", fake_select_latest_for_device) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.get_firmware_update_dict", fake_get_firmware_update_dict) result = await service.get_firmware_status(device_id="TalkingQ_XQSN00001005", user_id=1) assert result["ota_channel"] == "gps" assert result["firmware_id"] == 10 assert result["latest_version"] == "1.1.0" assert result["firmware_url"] == "https://example.com/gps-1.1.0.bin" assert result["force_update"] is True assert result["update_available"] is True assert result["can_update"] is True @pytest.mark.asyncio async def test_firmware_status_unknown_channel_cannot_update(monkeypatch): service = DeviceService() async def fake_get_device_status(**kwargs): del kwargs return { "device_id": "TalkingQ_XQSN00001005", "version": "1.0.0", "device_type": None, "ota_channel": None, } async def fake_select_latest_for_device(status_row): assert status_row["device_type"] is None return None async def fake_get_firmware_update_dict(device_id): del device_id return None monkeypatch.setattr(service, "get_device_status", fake_get_device_status) monkeypatch.setattr("banban.service.device.ota_firmware_service.select_latest_for_device", fake_select_latest_for_device) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.get_firmware_update_dict", fake_get_firmware_update_dict) result = await service.get_firmware_status(device_id="TalkingQ_XQSN00001005", user_id=1) assert result["ota_channel"] is None assert result["latest_version"] is None assert result["can_update"] is False @pytest.mark.asyncio async def test_firmware_status_settles_active_update_when_target_is_installed(monkeypatch): service = DeviceService() calls = [] update_record = { "device_id": "TalkingQ_ZHWK00003111", "firmware_version": "1.8.3", "target_version": "1.8.3", "firmware_url": "https://example.com/gps-1.8.3.bin", "firmware_id": 12, "update_status": "accepted", "progress": 0.0, "updated_at": datetime.now(), } async def fake_get_device_status(**kwargs): del kwargs return { "device_id": "TalkingQ_ZHWK00003111", "version": "1.8.3", "device_type": 2, "ota_channel": "gps", } async def fake_select_latest_for_device(status_row): del status_row return OtaFirmwareSelection( channel_key="gps", version="1.8.4", firmware_url="https://example.com/gps-1.8.4.bin", firmware_id=13, ) async def fake_get_firmware_update_dict(device_id): assert device_id == "TalkingQ_ZHWK00003111" return update_record async def fake_update_firmware_update(**kwargs): calls.append(kwargs) return True monkeypatch.setattr(service, "get_device_status", fake_get_device_status) monkeypatch.setattr("banban.service.device.ota_firmware_service.select_latest_for_device", fake_select_latest_for_device) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.get_firmware_update_dict", fake_get_firmware_update_dict) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.update_firmware_update", fake_update_firmware_update) result = await service.get_firmware_status(device_id="TalkingQ_ZHWK00003111", user_id=1) assert result["update_status"] == "success" assert result["progress"] == 100.0 assert result["update_available"] is True assert calls == [{ "device_id": "TalkingQ_ZHWK00003111", "firmware_version": "1.8.3", "update_status": "success", "progress": 100.0, }] @pytest.mark.asyncio @pytest.mark.parametrize("active_status", ["sent", "accepted", "updating"]) async def test_firmware_status_times_out_stale_active_update(monkeypatch, active_status): service = DeviceService() calls = [] update_record = { "device_id": "TalkingQ_ZHWK00003111", "firmware_version": "1.8.3", "target_version": "1.8.3", "firmware_url": "https://example.com/gps-1.8.3.bin", "firmware_id": 12, "update_status": active_status, "progress": 42.0, "updated_at": datetime.now() - timedelta(seconds=301), } async def fake_get_device_status(**kwargs): del kwargs return { "device_id": "TalkingQ_ZHWK00003111", "version": "1.8.2", "device_type": 2, "ota_channel": "gps", } async def fake_select_latest_for_device(status_row): del status_row return OtaFirmwareSelection( channel_key="gps", version="1.8.4", firmware_url="https://example.com/gps-1.8.4.bin", firmware_id=13, ) async def fake_get_firmware_update_dict(device_id): assert device_id == "TalkingQ_ZHWK00003111" return update_record async def fake_update_firmware_update(**kwargs): calls.append(kwargs) return True monkeypatch.setattr(service, "get_device_status", fake_get_device_status) monkeypatch.setattr("banban.service.device.ota_firmware_service.select_latest_for_device", fake_select_latest_for_device) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.get_firmware_update_dict", fake_get_firmware_update_dict) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.update_firmware_update", fake_update_firmware_update) result = await service.get_firmware_status(device_id="TalkingQ_ZHWK00003111", user_id=1) assert result["update_status"] == "failed" assert result["progress"] == 0.0 assert result["can_update"] is True assert calls == [{ "device_id": "TalkingQ_ZHWK00003111", "firmware_version": "1.8.3", "update_status": "failed", "progress": 0.0, }] def test_firmware_update_not_complete_until_target_version_is_reported(): service = DeviceService() assert not service._firmware_update_reached_target("1.8.2", "1.8.3") assert not service._firmware_update_reached_target("unknown", "1.8.3") assert not service._firmware_update_reached_target("1.8.4", "unknown") assert service._firmware_update_reached_target("1.8.3", "1.8.3") assert service._firmware_update_reached_target("1.8.4", "1.8.3") def test_firmware_update_timeout_does_not_fail_recent_progress(monkeypatch): service = DeviceService() monkeypatch.setattr("banban.service.device.settings.ota_sent_timeout_seconds", 300) assert not service._firmware_update_timed_out(datetime.now()) assert service._firmware_update_timed_out(datetime.now() - timedelta(seconds=301)) @pytest.mark.asyncio async def test_start_firmware_update_records_channel_and_keeps_mqtt_payload(monkeypatch): service = DeviceService() calls = [] async def fake_get_firmware_status(**kwargs): del kwargs return { "device_id": "TalkingQ_XQSN00001005", "current_version": "1.0.0", "latest_version": "1.1.0", "ota_channel": "no_gps", "firmware_id": 22, "firmware_url": "https://example.com/no-gps-1.1.0.bin", "update_available": True, "can_update": True, "update_status": "idle", "progress": 0.0, "target_version": None, "updated_at": None, } async def fake_update_firmware_update(**kwargs): calls.append(("record", kwargs)) return True class FakeMqttService: async def send_ota_command(self, device_id, url, version): calls.append(("mqtt", {"device_id": device_id, "url": url, "version": version})) return "003" async def fake_get_instance(): return FakeMqttService() monkeypatch.setattr(service, "get_firmware_status", fake_get_firmware_status) monkeypatch.setattr("banban.service.device.device_firmware_update_manager.update_firmware_update", fake_update_firmware_update) monkeypatch.setattr("handlers.mqtt_handler.TalkingQMQTTService.get_instance", fake_get_instance) result = await service.start_firmware_update(device_id="TalkingQ_XQSN00001005", user_id=1) assert calls == [ ( "record", { "device_id": "TalkingQ_XQSN00001005", "firmware_version": "1.1.0", "update_status": "sent", "progress": 0.0, "ota_channel": "no_gps", "target_version": "1.1.0", "firmware_url": "https://example.com/no-gps-1.1.0.bin", "firmware_id": 22, "source": "manual", }, ), ( "mqtt", { "device_id": "TalkingQ_XQSN00001005", "url": "https://example.com/no-gps-1.1.0.bin", "version": "1.1.0", }, ), ] assert result["msg_id"] == "003" assert result["ota_channel"] == "no_gps" assert result["target_version"] == "1.1.0" @pytest.mark.asyncio async def test_start_firmware_update_without_known_channel_is_blocked(monkeypatch): service = DeviceService() async def fake_get_firmware_status(**kwargs): del kwargs return { "device_id": "TalkingQ_XQSN00001005", "current_version": "1.0.0", "latest_version": None, "update_available": False, "can_update": False, "update_status": "idle", "progress": 0.0, "target_version": None, "updated_at": None, } monkeypatch.setattr(service, "get_firmware_status", fake_get_firmware_status) with pytest.raises(HTTPException) as exc_info: await service.start_firmware_update(device_id="TalkingQ_XQSN00001005", user_id=1) assert exc_info.value.status_code == 409 @pytest.mark.asyncio async def test_device_type_update_preserves_existing_manual_ota_channel(monkeypatch): service = DeviceSettingService() calls = [] async def fake_get_setting_by_device_id(device_id): assert device_id == "TalkingQ_XQSN00001005" return {"device_id": device_id, "ota_channel": "factory_gps"} async def fake_update_setting(**kwargs): calls.append(kwargs) monkeypatch.setattr(service, "get_setting_by_device_id", fake_get_setting_by_device_id) monkeypatch.setattr(service, "update_setting", fake_update_setting) await service.insert_or_update( device_id="TalkingQ_XQSN00001005", power=80, volume=60, signal_strength=4, version_str="1.0.0", device_type=1, ) assert calls[0]["device_type"] == 1 assert calls[0]["ota_channel"] is None