ForcePilot/backend/test/unit/channel/security/test_identity.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

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"