"""Tests for the TakeoverService — message listening, decision, reply execution.""" import pytest from unittest.mock import AsyncMock, patch, MagicMock from services.takeover_service import TakeoverService from models import Authorization, Avatar @pytest.fixture def mock_db(): db = MagicMock() return db @pytest.fixture def mock_boxim(): client = AsyncMock() client.get_credentials.return_value = {"accid": "owner_acc", "token": "tok"} client.send_p2p_message.return_value = True return client @pytest.fixture def mock_auth(): auth = MagicMock(spec=Authorization) auth.takeover_enabled = True auth.takeover_mode = "immediate" auth.takeover_delay_seconds = 30 auth.avatar_id = "avatar_123" auth.target_id = "target_user_123" auth.target_type = "user" auth.status = "active" auth.permissions = ["chat", "takeover"] return auth @pytest.fixture def mock_avatar(): avatar = MagicMock(spec=Avatar) avatar.id = "avatar_123" avatar.owner_id = "owner_huihui_123" return avatar # --- check_takeover_enabled --- def test_check_takeover_enabled_returns_auth_when_enabled(mock_db, mock_auth, mock_boxim, mock_avatar): avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter auth_filter = MagicMock() auth_filter.filter.return_value = auth_filter auth_filter.first.return_value = mock_auth def query_side_effect(model): if model == Avatar: return avatar_query return auth_filter mock_db.query.side_effect = query_side_effect service = TakeoverService(mock_db, mock_boxim) result = service.check_takeover_enabled("owner_huihui_123", "target_user_123") assert result == mock_auth def test_check_takeover_enabled_returns_none_when_no_avatar(mock_db, mock_boxim): avatar_filter = MagicMock() avatar_filter.first.return_value = None avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter mock_db.query.return_value = avatar_query service = TakeoverService(mock_db, mock_boxim) result = service.check_takeover_enabled("owner_123", "target_123") assert result is None def test_check_takeover_enabled_returns_none_when_disabled(mock_db, mock_boxim, mock_avatar): avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter disabled_auth = MagicMock(spec=Authorization) disabled_auth.takeover_enabled = False disabled_auth.permissions = [] auth_filter = MagicMock() auth_filter.filter.return_value = auth_filter auth_filter.first.return_value = disabled_auth def query_side_effect(model): if model == Avatar: return avatar_query return auth_filter mock_db.query.side_effect = query_side_effect service = TakeoverService(mock_db, mock_boxim) result = service.check_takeover_enabled("owner_123", "target_123") assert result is None def test_check_takeover_enabled_filters_by_owner_and_target(mock_db, mock_boxim, mock_avatar, mock_auth): """Verify that queries use the correct filter arguments.""" avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter auth_filter = MagicMock() auth_filter.filter.return_value = auth_filter auth_filter.first.return_value = mock_auth call_order = [] def query_side_effect(model): if model == Avatar: call_order.append("Avatar") return avatar_query call_order.append("Authorization") return auth_filter mock_db.query.side_effect = query_side_effect service = TakeoverService(mock_db, mock_boxim) service.check_takeover_enabled("owner_huihui_123", "target_user_123") assert "Avatar" in call_order assert "Authorization" in call_order # --- generate_reply --- @pytest.mark.asyncio async def test_generate_reply_returns_answer(mock_boxim): mock_db = MagicMock() with patch("services.takeover_service.httpx.AsyncClient") as mock_client_class: mock_response = MagicMock() mock_response.json.return_value = {"code": 200, "data": {"answer": "Hello back"}} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response service = TakeoverService(mock_db, mock_boxim) result = await service.generate_reply("avatar_123", "Hello") assert result == "Hello back" @pytest.mark.asyncio async def test_generate_reply_handles_empty_answer(mock_boxim): """generate_reply should return empty string when answer is missing.""" mock_db = MagicMock() with patch("services.takeover_service.httpx.AsyncClient") as mock_client_class: mock_response = MagicMock() mock_response.json.return_value = {"code": 200, "data": {}} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response service = TakeoverService(mock_db, mock_boxim) result = await service.generate_reply("avatar_123", "Hello") assert result == "" @pytest.mark.asyncio async def test_generate_reply_handles_error_code(mock_boxim): """generate_reply should return empty string when API returns error code.""" mock_db = MagicMock() with patch("services.takeover_service.httpx.AsyncClient") as mock_client_class: mock_response = MagicMock() mock_response.json.return_value = {"code": 500, "message": "Internal error"} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response service = TakeoverService(mock_db, mock_boxim) result = await service.generate_reply("avatar_123", "Hello") assert result == "" # --- execute_takeover --- @pytest.mark.asyncio async def test_execute_takeover_success(mock_db, mock_boxim, mock_auth, mock_avatar): avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter mock_db.query.return_value = avatar_query with patch("services.takeover_service.httpx.AsyncClient") as mock_client_class: mock_response = MagicMock() mock_response.json.return_value = {"code": 200, "data": {"answer": "Hello back"}} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response service = TakeoverService(mock_db, mock_boxim) message = {"from_accid": "user_acc", "content": "Hello"} result = await service.execute_takeover(mock_auth, message) assert result is True mock_boxim.get_credentials.assert_called_once_with("owner_huihui_123") mock_boxim.send_p2p_message.assert_called_once() @pytest.mark.asyncio async def test_execute_takeover_fails_when_avatar_not_found(mock_db, mock_boxim, mock_auth): """execute_takeover should return False when Avatar is not found.""" avatar_filter = MagicMock() avatar_filter.first.return_value = None avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter mock_db.query.return_value = avatar_query service = TakeoverService(mock_db, mock_boxim) message = {"from_accid": "user_acc", "content": "Hello"} result = await service.execute_takeover(mock_auth, message) assert result is False mock_boxim.get_credentials.assert_not_called() @pytest.mark.asyncio async def test_execute_takeover_fails_when_no_credentials(mock_db, mock_boxim, mock_auth, mock_avatar): """execute_takeover should return False when boxim.get_credentials returns None.""" avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter mock_db.query.return_value = avatar_query mock_boxim.get_credentials.return_value = None service = TakeoverService(mock_db, mock_boxim) message = {"from_accid": "user_acc", "content": "Hello"} result = await service.execute_takeover(mock_auth, message) assert result is False # --- enqueue_delayed_message --- def test_enqueue_delayed_message_with_redis(mock_db, mock_boxim, mock_auth, mock_avatar): mock_redis = MagicMock() avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter mock_db.query.return_value = avatar_query service = TakeoverService(mock_db, mock_boxim, mock_redis) message = {"msg_id": "msg_1", "from_accid": "user_acc", "content": "Hello"} service.enqueue_delayed_message(mock_auth, message) mock_redis.setex.assert_called_once() call_args = mock_redis.setex.call_args value = call_args[0][1] import json payload = json.loads(call_args[0][2]) assert payload["owner_huihui_id"] == "owner_huihui_123" def test_enqueue_delayed_message_without_redis_logs_warning(mock_db, mock_boxim, mock_auth, mock_avatar): """When Redis is not configured, enqueue_delayed_message should log a warning and not crash.""" avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter mock_db.query.return_value = avatar_query service = TakeoverService(mock_db, mock_boxim) message = {"msg_id": "msg_1", "from_accid": "user_acc", "content": "Hello"} service.enqueue_delayed_message(mock_auth, message) # --- process_delayed_queue --- @pytest.mark.asyncio async def test_process_delayed_queue_no_redis(mock_db, mock_boxim): """process_delayed_queue should return immediately without Redis.""" service = TakeoverService(mock_db, mock_boxim) await service.process_delayed_queue() mock_db.query.assert_not_called() @pytest.mark.asyncio async def test_process_delayed_queue_processes_messages(mock_db, mock_boxim, mock_auth, mock_avatar): """process_delayed_queue should read from Redis, resolve auth, and execute takeover.""" mock_redis = MagicMock() mock_redis.keys.return_value = ["takeover:delayed:target_user_123:msg_1"] mock_redis.get.return_value = '{"from_accid": "user_acc", "content": "Hello"}' avatar_filter = MagicMock() avatar_filter.first.return_value = mock_avatar avatar_query = MagicMock() avatar_query.filter.return_value = avatar_filter auth_filter = MagicMock() auth_filter.filter.return_value = auth_filter auth_filter.first.return_value = mock_auth def query_side_effect(model): if model == Avatar: return avatar_query return auth_filter mock_db.query.side_effect = query_side_effect with patch("services.takeover_service.httpx.AsyncClient") as mock_client_class: mock_response = MagicMock() mock_response.json.return_value = {"code": 200, "data": {"answer": "Hello back"}} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response service = TakeoverService(mock_db, mock_boxim, mock_redis) await service.process_delayed_queue() mock_boxim.send_p2p_message.assert_called_once() mock_redis.delete.assert_called_once()