379 lines
12 KiB
Python
379 lines
12 KiB
Python
import pytest
|
|
|
|
from banban.routers.devices import _row_to_current_location_response
|
|
from banban.service.device_alarm import DeviceAlarmService
|
|
from banban.service.location import LocationService, is_valid_coordinate_pair
|
|
from handlers.mqtt_handler import TalkingQMQTTService
|
|
|
|
|
|
def test_zero_zero_coordinate_is_invalid():
|
|
assert is_valid_coordinate_pair(23.2413923, 113.6051866) is True
|
|
assert is_valid_coordinate_pair(0, 0) is False
|
|
assert is_valid_coordinate_pair("0.0000000", "0.0000000") is False
|
|
assert is_valid_coordinate_pair(None, 113.6051866) is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alarm_requests_fresh_gps_before_sms(monkeypatch):
|
|
service = TalkingQMQTTService({"broker": "127.0.0.1"})
|
|
published = []
|
|
calls = []
|
|
|
|
async def fake_publish(topic, payload):
|
|
published.append((topic, payload))
|
|
return True
|
|
|
|
async def fake_schedule_persistence(device_id, label, coro):
|
|
calls.append(("schedule", label))
|
|
await coro
|
|
|
|
async def fake_record_alarm_event(**kwargs):
|
|
calls.append(("record_alarm", kwargs))
|
|
return 123
|
|
|
|
async def fake_query_gps_and_wait_for_location(device_id, *, timeout_seconds):
|
|
calls.append(("query_gps", device_id, timeout_seconds))
|
|
return {
|
|
"child_id": 9,
|
|
"device_id": device_id,
|
|
"coord_type": "gcj02",
|
|
"lat": 30.245,
|
|
"lng": 120.215,
|
|
"updated_at": "2026-06-08 12:00:00",
|
|
"address": "浙江省杭州市萧山区扬帆路附近",
|
|
}
|
|
|
|
async def fake_apply_alarm_location(**kwargs):
|
|
calls.append(("apply_alarm_location", kwargs))
|
|
return True
|
|
|
|
def fake_schedule_alarm_notification(**kwargs):
|
|
calls.append(("sms", kwargs))
|
|
|
|
monkeypatch.setattr(service, "_publish", fake_publish)
|
|
monkeypatch.setattr(service, "_schedule_persistence", fake_schedule_persistence)
|
|
monkeypatch.setattr(service, "query_gps_and_wait_for_location", fake_query_gps_and_wait_for_location)
|
|
monkeypatch.setattr("handlers.mqtt_handler.settings.alarm_gps_query_timeout_seconds", 2.5)
|
|
monkeypatch.setattr("handlers.mqtt_handler.device_alarm_service.record_alarm_event", fake_record_alarm_event)
|
|
monkeypatch.setattr("handlers.mqtt_handler.device_alarm_service.apply_alarm_location", fake_apply_alarm_location)
|
|
monkeypatch.setattr("handlers.mqtt_handler.sms_notification_service.schedule_alarm_notification", fake_schedule_alarm_notification)
|
|
|
|
await service._handle_alarm_report("TalkingQ_device001", {"msg_id": "010"})
|
|
|
|
assert published == [
|
|
(
|
|
"device/TalkingQ_device001/event_resp",
|
|
{
|
|
"msg_id": "010",
|
|
"status": "success",
|
|
"type": 0,
|
|
"params": {
|
|
"url": "http://192.168.101.78:8080/assets/roles/banban/warn_parent_zh.mp3",
|
|
},
|
|
},
|
|
),
|
|
]
|
|
assert calls == [
|
|
("schedule", "alarm_event"),
|
|
(
|
|
"record_alarm",
|
|
{
|
|
"device_id": "TalkingQ_device001",
|
|
"source_msg_id": "010",
|
|
"resolve_address": False,
|
|
},
|
|
),
|
|
("query_gps", "TalkingQ_device001", 2.5),
|
|
(
|
|
"apply_alarm_location",
|
|
{
|
|
"alarm_id": 123,
|
|
"device_id": "TalkingQ_device001",
|
|
"location": {
|
|
"child_id": 9,
|
|
"device_id": "TalkingQ_device001",
|
|
"coord_type": "gcj02",
|
|
"lat": 30.245,
|
|
"lng": 120.215,
|
|
"updated_at": "2026-06-08 12:00:00",
|
|
"address": "浙江省杭州市萧山区扬帆路附近",
|
|
},
|
|
},
|
|
),
|
|
("sms", {"device_id": "TalkingQ_device001", "alarm_id": 123}),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alarm_ignores_invalid_zero_zero_location(monkeypatch):
|
|
service = DeviceAlarmService()
|
|
calls = []
|
|
|
|
async def fake_update_alarm_location(**kwargs):
|
|
calls.append(("update_alarm_location", kwargs))
|
|
return True
|
|
|
|
async def fake_resolve_alarm_address(**kwargs):
|
|
calls.append(("resolve_alarm_address", kwargs))
|
|
|
|
monkeypatch.setattr(service, "update_alarm_location", fake_update_alarm_location)
|
|
monkeypatch.setattr(service, "resolve_alarm_address", fake_resolve_alarm_address)
|
|
|
|
updated = await service.apply_alarm_location(
|
|
alarm_id=7,
|
|
device_id="TalkingQ_device001",
|
|
location={
|
|
"coord_type": "gcj02",
|
|
"lat": 0,
|
|
"lng": 0,
|
|
"updated_at": "2026-06-22 10:00:00",
|
|
},
|
|
)
|
|
|
|
assert updated is False
|
|
assert calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alarm_current_location_reuses_existing_address(monkeypatch):
|
|
service = DeviceAlarmService()
|
|
calls = []
|
|
|
|
async def fake_get_alarm_event(**kwargs):
|
|
calls.append(("get_alarm_event", kwargs))
|
|
return {
|
|
"alarm_id": 7,
|
|
"device_id": "TalkingQ_device001",
|
|
"lat": 23.2413923,
|
|
"lng": 113.6051866,
|
|
"address": "广东省广州市增城区中新镇叶岭新村街",
|
|
}
|
|
|
|
async def fake_resolve_alarm_address(**kwargs):
|
|
calls.append(("resolve_alarm_address", kwargs))
|
|
|
|
monkeypatch.setattr(service, "get_alarm_event", fake_get_alarm_event)
|
|
monkeypatch.setattr(service, "resolve_alarm_address", fake_resolve_alarm_address)
|
|
|
|
resolved = await service.resolve_alarm_current_location(
|
|
alarm_id=7,
|
|
device_id="TalkingQ_device001",
|
|
)
|
|
|
|
assert resolved is True
|
|
assert calls == [
|
|
("get_alarm_event", {"alarm_id": 7, "device_id": "TalkingQ_device001"}),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alarm_record_ignores_invalid_current_location_snapshot(monkeypatch):
|
|
service = DeviceAlarmService()
|
|
calls = []
|
|
|
|
class FakeDAO:
|
|
def __init__(self, session):
|
|
del session
|
|
|
|
async def get_active_binding_context(self, *, device_id):
|
|
calls.append(("binding", device_id))
|
|
return {"owner_user_id": 3, "child_id": 9}
|
|
|
|
async def get_current_location_snapshot(self, *, child_id):
|
|
calls.append(("snapshot", child_id))
|
|
return {
|
|
"coord_type": "gcj02",
|
|
"lat": 0,
|
|
"lng": 0,
|
|
"updated_at": "2026-06-22 10:00:00",
|
|
"address": None,
|
|
}
|
|
|
|
async def create(self, **kwargs):
|
|
calls.append(("create", kwargs))
|
|
return 7
|
|
|
|
class FakeSession:
|
|
async def close(self):
|
|
calls.append(("close",))
|
|
|
|
async def fake_get_session():
|
|
return FakeSession()
|
|
|
|
monkeypatch.setattr(service, "get_session", fake_get_session)
|
|
monkeypatch.setattr("banban.service.device_alarm.DeviceAlarmDAO", FakeDAO)
|
|
|
|
alarm_id = await service.record_alarm_event(device_id="TalkingQ_device001")
|
|
|
|
assert alarm_id == 7
|
|
assert calls == [
|
|
("binding", "TalkingQ_device001"),
|
|
("snapshot", 9),
|
|
(
|
|
"create",
|
|
{
|
|
"device_id": "TalkingQ_device001",
|
|
"owner_user_id": 3,
|
|
"child_id": 9,
|
|
"source_msg_id": "010",
|
|
"location": None,
|
|
},
|
|
),
|
|
("close",),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alarm_falls_back_to_existing_alarm_location_when_gps_times_out(monkeypatch):
|
|
service = TalkingQMQTTService({"broker": "127.0.0.1"})
|
|
calls = []
|
|
|
|
async def fake_publish(topic, payload):
|
|
del topic, payload
|
|
return True
|
|
|
|
async def fake_schedule_persistence(device_id, label, coro):
|
|
calls.append(("schedule", label))
|
|
await coro
|
|
|
|
async def fake_record_alarm_event(**kwargs):
|
|
calls.append(("record_alarm", kwargs))
|
|
return 456
|
|
|
|
async def fake_query_gps_and_wait_for_location(device_id, *, timeout_seconds):
|
|
calls.append(("query_gps", device_id, timeout_seconds))
|
|
return None
|
|
|
|
async def fake_resolve_alarm_current_location(**kwargs):
|
|
calls.append(("resolve_existing", kwargs))
|
|
return True
|
|
|
|
def fake_schedule_alarm_notification(**kwargs):
|
|
calls.append(("sms", kwargs))
|
|
|
|
monkeypatch.setattr(service, "_publish", fake_publish)
|
|
monkeypatch.setattr(service, "_schedule_persistence", fake_schedule_persistence)
|
|
monkeypatch.setattr(service, "query_gps_and_wait_for_location", fake_query_gps_and_wait_for_location)
|
|
monkeypatch.setattr("handlers.mqtt_handler.settings.alarm_gps_query_timeout_seconds", 1.0)
|
|
monkeypatch.setattr("handlers.mqtt_handler.device_alarm_service.record_alarm_event", fake_record_alarm_event)
|
|
monkeypatch.setattr("handlers.mqtt_handler.device_alarm_service.resolve_alarm_current_location", fake_resolve_alarm_current_location)
|
|
monkeypatch.setattr("handlers.mqtt_handler.sms_notification_service.schedule_alarm_notification", fake_schedule_alarm_notification)
|
|
|
|
await service._handle_alarm_report("TalkingQ_device001", {"msg_id": "010"})
|
|
|
|
assert calls == [
|
|
("schedule", "alarm_event"),
|
|
(
|
|
"record_alarm",
|
|
{
|
|
"device_id": "TalkingQ_device001",
|
|
"source_msg_id": "010",
|
|
"resolve_address": False,
|
|
},
|
|
),
|
|
("query_gps", "TalkingQ_device001", 1.0),
|
|
("resolve_existing", {"alarm_id": 456, "device_id": "TalkingQ_device001"}),
|
|
("sms", {"device_id": "TalkingQ_device001", "alarm_id": 456}),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mqtt_location_report_ignores_zero_zero(monkeypatch):
|
|
service = LocationService()
|
|
sessions = []
|
|
|
|
async def fail_get_session():
|
|
sessions.append("called")
|
|
raise AssertionError("invalid location should not open a database session")
|
|
|
|
monkeypatch.setattr(service, "get_session", fail_get_session)
|
|
|
|
row = await service.report_mqtt_device_location(
|
|
device_id="TalkingQ_device001",
|
|
latitude=0,
|
|
longitude=0,
|
|
)
|
|
|
|
assert row is None
|
|
assert sessions == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gps_waiter_returns_persisted_location(monkeypatch):
|
|
service = TalkingQMQTTService({"broker": "127.0.0.1"})
|
|
published = []
|
|
|
|
async def fake_publish(topic, payload):
|
|
published.append((topic, payload))
|
|
return True
|
|
|
|
async def fake_schedule_persistence(device_id, label, coro):
|
|
del device_id, label
|
|
await coro
|
|
|
|
async def fake_report_mqtt_device_location(**kwargs):
|
|
return {
|
|
"child_id": 9,
|
|
"device_id": kwargs["device_id"],
|
|
"lat": kwargs["latitude"],
|
|
"lng": kwargs["longitude"],
|
|
"updated_at": "2026-06-08 12:00:00",
|
|
}
|
|
|
|
monkeypatch.setattr(service, "_publish", fake_publish)
|
|
monkeypatch.setattr(service, "_schedule_persistence", fake_schedule_persistence)
|
|
monkeypatch.setattr("handlers.mqtt_handler.location_service.report_mqtt_device_location", fake_report_mqtt_device_location)
|
|
|
|
wait_task = __import__("asyncio").create_task(
|
|
service.query_gps_and_wait_for_location("TalkingQ_device001", timeout_seconds=1.0)
|
|
)
|
|
await __import__("asyncio").sleep(0)
|
|
|
|
await service._handle_gps_response(
|
|
"TalkingQ_device001",
|
|
{
|
|
"msg_id": "001",
|
|
"status": "success",
|
|
"data": {
|
|
"latitude": 30.245,
|
|
"longitude": 120.215,
|
|
},
|
|
},
|
|
)
|
|
|
|
location = await wait_task
|
|
|
|
assert published == [
|
|
("device/TalkingQ_device001/command", {"msg_id": "001"}),
|
|
]
|
|
assert location["lat"] == 30.245
|
|
assert location["lng"] == 120.215
|
|
|
|
|
|
def test_current_location_without_address_displays_pending_text():
|
|
row = {
|
|
"child_id": 9,
|
|
"child_name": "测试孩子",
|
|
"device_id": "TalkingQ_device001",
|
|
"coord_type": "gcj02",
|
|
"lat": 23.2413923,
|
|
"lng": 113.6051866,
|
|
"accuracy_m": None,
|
|
"altitude_m": None,
|
|
"speed_mps": None,
|
|
"heading_deg": None,
|
|
"source": 1,
|
|
"battery_pct": 80,
|
|
"device_time": "2026-06-22 10:00:00",
|
|
"server_time": "2026-06-22 10:00:01",
|
|
"updated_at": "2026-06-22 10:00:01",
|
|
"address": None,
|
|
"address_resolved_at": None,
|
|
"address_resolve_status": 2,
|
|
}
|
|
|
|
response = _row_to_current_location_response(row)
|
|
|
|
assert response.address == "地址解析中"
|
|
assert response.address_resolve_status == 2
|