持久化设备留言待收听状态并支持重启恢复
This commit is contained in:
7
talkingq-url/tests/conftest.py
Normal file
7
talkingq-url/tests/conftest.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
42
talkingq-url/tests/test_database_service_logging.py
Normal file
42
talkingq-url/tests/test_database_service_logging.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from services import database_service_base
|
||||
from services.database_service_base import DatabaseServiceBase
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_init_success_log_is_once_per_service(monkeypatch):
|
||||
logs = []
|
||||
|
||||
async def fake_get_db_manager():
|
||||
return object()
|
||||
|
||||
def fake_info(device_id, session_id, message):
|
||||
logs.append((device_id, session_id, message))
|
||||
|
||||
DatabaseServiceBase._logged_initialized_services.clear()
|
||||
monkeypatch.setattr(database_service_base, "get_db_manager", fake_get_db_manager)
|
||||
monkeypatch.setattr(database_service_base.session_logger, "info", fake_info)
|
||||
|
||||
first = DatabaseServiceBase(service_name="binding_service")
|
||||
second = DatabaseServiceBase(service_name="binding_service")
|
||||
|
||||
await first._init_database()
|
||||
await second._init_database()
|
||||
|
||||
assert logs == [
|
||||
(
|
||||
"system",
|
||||
"binding_service",
|
||||
"binding_service数据库连接初始化成功",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_binding_service_singleton_is_used_by_router_and_mqtt_handler():
|
||||
from banban.routers import bindings
|
||||
from banban.service.binding import binding_service
|
||||
from handlers import mqtt_handler
|
||||
|
||||
assert bindings.binding_service is binding_service
|
||||
assert mqtt_handler.binding_service is binding_service
|
||||
230
talkingq-url/tests/test_pending_voice_messages.py
Normal file
230
talkingq-url/tests/test_pending_voice_messages.py
Normal file
@@ -0,0 +1,230 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from banban.dao.pending_voice_message import PendingVoiceMessageDAO
|
||||
from banban.service.pending_voice_message import (
|
||||
PendingVoiceMessageService,
|
||||
PendingVoicePlaybackItem,
|
||||
)
|
||||
from handlers.mqtt_handler import TalkingQMQTTService
|
||||
|
||||
|
||||
class FakeResult:
|
||||
def __init__(self, rows=None):
|
||||
self._rows = rows or []
|
||||
|
||||
def mappings(self):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return self._rows
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, rows=None):
|
||||
self.rows = rows or []
|
||||
self.calls = []
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
self.closed = False
|
||||
|
||||
async def execute(self, statement, params=None):
|
||||
self.calls.append((str(statement), params or {}))
|
||||
return FakeResult(self.rows)
|
||||
|
||||
async def commit(self):
|
||||
self.commits += 1
|
||||
|
||||
async def rollback(self):
|
||||
self.rollbacks += 1
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_pending_keeps_original_media_file_key_on_duplicate():
|
||||
session = FakeSession()
|
||||
dao = PendingVoiceMessageDAO(session)
|
||||
|
||||
await dao.upsert_pending(
|
||||
target_device_id="TalkingQ_XQSN00001001",
|
||||
sender_device_id="TalkingQ_XQSN00001002",
|
||||
im_message_id=123,
|
||||
media_file_key="audio/original.mp3",
|
||||
audio_url="http://example.test/audio.mp3",
|
||||
source="device_peer_voice",
|
||||
)
|
||||
|
||||
sql, params = session.calls[0]
|
||||
update_sql = sql.split("ON DUPLICATE KEY UPDATE", 1)[1]
|
||||
assert "ON DUPLICATE KEY UPDATE" in sql
|
||||
assert "media_file_key" not in update_sql
|
||||
assert params["target_device_id"] == "TalkingQ_XQSN00001001"
|
||||
assert params["im_message_id"] == 123
|
||||
assert params["media_file_key"] == "audio/original.mp3"
|
||||
assert params["status"] == "pending"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_delivered_only_advances_pending_rows():
|
||||
session = FakeSession()
|
||||
dao = PendingVoiceMessageDAO(session)
|
||||
|
||||
await dao.mark_delivered([7, 9])
|
||||
|
||||
sql, params = session.calls[0]
|
||||
assert "SET status = :status" in sql
|
||||
assert "delivery_count = delivery_count + 1" in sql
|
||||
assert "AND status = :pending_status" in sql
|
||||
assert params == {
|
||||
"id_0": 7,
|
||||
"id_1": 9,
|
||||
"status": "delivered",
|
||||
"pending_status": "pending",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_playback_items_uses_fallback_url_when_signed_url_fails(monkeypatch):
|
||||
service = PendingVoiceMessageService()
|
||||
session = FakeSession(
|
||||
rows=[
|
||||
{
|
||||
"id": 11,
|
||||
"target_device_id": "TalkingQ_XQSN00001001",
|
||||
"sender_device_id": "TalkingQ_XQSN00001002",
|
||||
"im_message_id": 123,
|
||||
"media_file_key": "cos/audio.mp3",
|
||||
"audio_url": "http://cached.example/audio.mp3",
|
||||
"source": "device_peer_voice",
|
||||
"status": "pending",
|
||||
"delivery_count": 0,
|
||||
"created_at": None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
async def fake_get_session():
|
||||
return session
|
||||
|
||||
class FailingAudioCache:
|
||||
async def get_device_audio_url(self, media_file_key, device_id=None):
|
||||
raise RuntimeError("cos unavailable")
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr(
|
||||
"banban.service.pending_voice_message.device_audio_cache_service",
|
||||
FailingAudioCache(),
|
||||
)
|
||||
|
||||
items = await service.get_playback_items(target_device_id="TalkingQ_XQSN00001001")
|
||||
|
||||
assert items == [
|
||||
PendingVoicePlaybackItem(
|
||||
pending_id=11,
|
||||
audio_url="http://cached.example/audio.mp3",
|
||||
im_message_id=123,
|
||||
media_file_key="cos/audio.mp3",
|
||||
source="device_peer_voice",
|
||||
)
|
||||
]
|
||||
assert session.closed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nfc_owner_listen_marks_pending_delivered_only_after_publish(monkeypatch):
|
||||
service = TalkingQMQTTService({"device_prefix": "TalkingQ"})
|
||||
published = []
|
||||
marked = []
|
||||
cleared = []
|
||||
|
||||
class FakeCardService:
|
||||
async def get_card_by_uuid(self, nfc_uuid):
|
||||
return SimpleNamespace(device_id="TalkingQ_XQSN00001001")
|
||||
|
||||
async def check_card_ownership(self, nfc_uuid, device_id):
|
||||
return True
|
||||
|
||||
class FakePendingVoiceService:
|
||||
async def get_playback_items(self, target_device_id):
|
||||
return [
|
||||
PendingVoicePlaybackItem(
|
||||
pending_id=11,
|
||||
audio_url="http://cached.example/audio.mp3",
|
||||
im_message_id=123,
|
||||
media_file_key="cos/audio.mp3",
|
||||
source="device_peer_voice",
|
||||
)
|
||||
]
|
||||
|
||||
async def mark_delivered(self, pending_ids):
|
||||
marked.extend(pending_ids)
|
||||
|
||||
class FakeOfflineAudioCache:
|
||||
async def clear_audio_urls(self, device_id):
|
||||
cleared.append(device_id)
|
||||
|
||||
async def fake_publish(topic, payload):
|
||||
published.append((topic, payload))
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("handlers.mqtt_handler.card_service", FakeCardService())
|
||||
monkeypatch.setattr("handlers.mqtt_handler.pending_voice_message_service", FakePendingVoiceService())
|
||||
monkeypatch.setattr("handlers.mqtt_handler.offline_audio_cache", FakeOfflineAudioCache())
|
||||
monkeypatch.setattr(service, "_publish", fake_publish)
|
||||
|
||||
await service._send_nfc_listen_response("TalkingQ_XQSN00001001", "53DA2B6DA20001")
|
||||
|
||||
assert published == [
|
||||
(
|
||||
"device/TalkingQ_XQSN00001001/event_resp",
|
||||
{
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": {"url_1": "http://cached.example/audio.mp3"},
|
||||
},
|
||||
)
|
||||
]
|
||||
assert marked == [11]
|
||||
assert cleared == ["TalkingQ_XQSN00001001"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nfc_owner_listen_keeps_pending_when_publish_fails(monkeypatch):
|
||||
service = TalkingQMQTTService({"device_prefix": "TalkingQ"})
|
||||
marked = []
|
||||
|
||||
class FakeCardService:
|
||||
async def get_card_by_uuid(self, nfc_uuid):
|
||||
return SimpleNamespace(device_id="TalkingQ_XQSN00001001")
|
||||
|
||||
async def check_card_ownership(self, nfc_uuid, device_id):
|
||||
return True
|
||||
|
||||
class FakePendingVoiceService:
|
||||
async def get_playback_items(self, target_device_id):
|
||||
return [
|
||||
PendingVoicePlaybackItem(
|
||||
pending_id=11,
|
||||
audio_url="http://cached.example/audio.mp3",
|
||||
im_message_id=123,
|
||||
media_file_key="cos/audio.mp3",
|
||||
source="device_peer_voice",
|
||||
)
|
||||
]
|
||||
|
||||
async def mark_delivered(self, pending_ids):
|
||||
marked.extend(pending_ids)
|
||||
|
||||
async def fake_publish(topic, payload):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr("handlers.mqtt_handler.card_service", FakeCardService())
|
||||
monkeypatch.setattr("handlers.mqtt_handler.pending_voice_message_service", FakePendingVoiceService())
|
||||
monkeypatch.setattr(service, "_publish", fake_publish)
|
||||
|
||||
await service._send_nfc_listen_response("TalkingQ_XQSN00001001", "53DA2B6DA20001")
|
||||
|
||||
assert marked == []
|
||||
Reference in New Issue
Block a user