168 lines
6.4 KiB
Python
168 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from yuxi.channel.security.identity import IdentityLinkResolver
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_repository() -> MagicMock:
|
|
repo = MagicMock()
|
|
repo.create = AsyncMock(return_value=None)
|
|
repo.delete_by_sender = AsyncMock(return_value=True)
|
|
repo.load_all = AsyncMock(return_value=[])
|
|
return repo
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_pg_manager(monkeypatch):
|
|
session = AsyncMock()
|
|
ctx = MagicMock()
|
|
ctx.__aenter__ = AsyncMock(return_value=session)
|
|
ctx.__aexit__ = AsyncMock(return_value=None)
|
|
manager = MagicMock()
|
|
manager.get_async_session_context = MagicMock(return_value=ctx)
|
|
monkeypatch.setattr("yuxi.channel.security.identity.pg_manager", manager)
|
|
return session
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestIdentityLinkResolver:
|
|
def test_resolve_by_full_key(self) -> None:
|
|
resolver = IdentityLinkResolver({"canonical_1": ["feishu:ou_xxx"]})
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|
|
|
|
def test_resolve_by_peer_id_without_channel(self) -> None:
|
|
resolver = IdentityLinkResolver({"canonical_1": ["12345"]})
|
|
assert resolver.resolve("dingtalk", "12345") == "canonical_1"
|
|
|
|
def test_full_key_takes_precedence_over_bare_peer_id(self) -> None:
|
|
resolver = IdentityLinkResolver(
|
|
{
|
|
"canonical_feishu": ["feishu:12345"],
|
|
"canonical_bare": ["12345"],
|
|
}
|
|
)
|
|
assert resolver.resolve("feishu", "12345") == "canonical_feishu"
|
|
assert resolver.resolve("dingtalk", "12345") == "canonical_bare"
|
|
|
|
def test_resolve_unknown_returns_peer_id(self) -> None:
|
|
resolver = IdentityLinkResolver({"canonical_1": ["feishu:ou_xxx"]})
|
|
assert resolver.resolve("feishu", "unknown_id") == "unknown_id"
|
|
|
|
def test_empty_links_returns_peer_id(self) -> None:
|
|
resolver = IdentityLinkResolver({})
|
|
assert resolver.resolve("feishu", "ou_xxx") == "ou_xxx"
|
|
|
|
def test_multiple_ids_per_canonical(self) -> None:
|
|
resolver = IdentityLinkResolver({"canonical_1": ["feishu:ou_xxx", "dingtalk:12345", "telegram:67890"]})
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|
|
assert resolver.resolve("dingtalk", "12345") == "canonical_1"
|
|
assert resolver.resolve("telegram", "67890") == "canonical_1"
|
|
|
|
def test_duplicate_linked_id_last_canonical_wins(self) -> None:
|
|
resolver = IdentityLinkResolver(
|
|
{
|
|
"first": ["feishu:ou_xxx"],
|
|
"second": ["feishu:ou_xxx"],
|
|
}
|
|
)
|
|
assert resolver.resolve("feishu", "ou_xxx") == "second"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestIdentityLinkResolverLink:
|
|
async def test_link_creates_record_and_updates_index(self, mock_repository: MagicMock) -> None:
|
|
resolver = IdentityLinkResolver(identity_repository=mock_repository)
|
|
|
|
await resolver.link("feishu", "ou_xxx", "canonical_1", "acc_1")
|
|
|
|
mock_repository.create.assert_awaited_once_with(
|
|
channel_type="feishu",
|
|
account_id="acc_1",
|
|
channel_sender_id="ou_xxx",
|
|
platform_user_id="canonical_1",
|
|
paired_by="qr",
|
|
)
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|
|
|
|
async def test_link_multiple_sender_ids_for_same_platform_user(self, mock_repository: MagicMock) -> None:
|
|
resolver = IdentityLinkResolver(identity_repository=mock_repository)
|
|
|
|
await resolver.link("feishu", "ou_xxx", "canonical_1", "acc_1")
|
|
await resolver.link("dingtalk", "12345", "canonical_1", "acc_1")
|
|
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|
|
assert resolver.resolve("dingtalk", "12345") == "canonical_1"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestIdentityLinkResolverUnlink:
|
|
async def test_unlink_removes_binding(self, mock_repository: MagicMock) -> None:
|
|
resolver = IdentityLinkResolver(
|
|
{"canonical_1": ["feishu:ou_xxx"]},
|
|
identity_repository=mock_repository,
|
|
)
|
|
mock_repository.delete_by_sender = AsyncMock(return_value=True)
|
|
|
|
result = await resolver.unlink("feishu", "acc_1", "ou_xxx")
|
|
|
|
assert result is True
|
|
mock_repository.delete_by_sender.assert_awaited_once_with(
|
|
channel_type="feishu",
|
|
account_id="acc_1",
|
|
channel_sender_id="ou_xxx",
|
|
)
|
|
assert resolver.resolve("feishu", "ou_xxx") == "ou_xxx"
|
|
|
|
async def test_unlink_nonexistent_binding_returns_false(self, mock_repository: MagicMock) -> None:
|
|
resolver = IdentityLinkResolver(
|
|
{"canonical_1": ["feishu:ou_xxx"]},
|
|
identity_repository=mock_repository,
|
|
)
|
|
mock_repository.delete_by_sender = AsyncMock(return_value=False)
|
|
|
|
result = await resolver.unlink("feishu", "acc_1", "unknown_id")
|
|
|
|
assert result is False
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestIdentityLinkResolverCreate:
|
|
async def test_create_builds_index_from_repository(self, mock_repository: MagicMock) -> None:
|
|
record1 = MagicMock()
|
|
record1.platform_user_id = "canonical_1"
|
|
record1.channel_type = "feishu"
|
|
record1.channel_sender_id = "ou_xxx"
|
|
record2 = MagicMock()
|
|
record2.platform_user_id = "canonical_1"
|
|
record2.channel_type = "dingtalk"
|
|
record2.channel_sender_id = "12345"
|
|
mock_repository.load_all = AsyncMock(return_value=[record1, record2])
|
|
|
|
resolver = await IdentityLinkResolver.create(mock_repository)
|
|
|
|
mock_repository.load_all.assert_awaited_once()
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|
|
assert resolver.resolve("dingtalk", "12345") == "canonical_1"
|
|
|
|
async def test_create_uses_pg_manager_when_repository_is_none(
|
|
self, mock_pg_manager: AsyncMock, monkeypatch
|
|
) -> None:
|
|
record = MagicMock()
|
|
record.platform_user_id = "canonical_1"
|
|
record.channel_type = "feishu"
|
|
record.channel_sender_id = "ou_xxx"
|
|
mock_repo = MagicMock()
|
|
mock_repo.load_all = AsyncMock(return_value=[record])
|
|
monkeypatch.setattr(
|
|
"yuxi.channel.security.identity.ChannelIdentityRepository",
|
|
MagicMock(return_value=mock_repo),
|
|
)
|
|
|
|
resolver = await IdentityLinkResolver.create()
|
|
|
|
assert resolver.resolve("feishu", "ou_xxx") == "canonical_1"
|