from __future__ import annotations import asyncio import time import pytest from yuxi.channels.adapters.slack.pairing import ( PairingManager, PendingPairing, _normalize_code, ) class TestNormalizeCode: def test_strips_slack_prefix(self): assert _normalize_code("slack:ABC123") == "ABC123" def test_strips_Slack_prefix(self): assert _normalize_code("Slack:ABC123") == "ABC123" def test_strips_SLACK_prefix(self): assert _normalize_code("SLACK:ABC123") == "ABC123" def test_no_prefix_passthrough(self): assert _normalize_code("ABC123") == "ABC123" def test_empty_string(self): assert _normalize_code("") == "" def test_other_prefix_passthrough(self): assert _normalize_code("other:ABC123") == "other:ABC123" class TestPendingPairing: def test_creation(self): pairing = PendingPairing(user_id="U001", pairing_code="ABC123") assert pairing.user_id == "U001" assert pairing.pairing_code == "ABC123" assert pairing.created_at > 0 def test_not_expired(self): pairing = PendingPairing(user_id="U001", pairing_code="ABC123") assert pairing.is_expired(ttl_seconds=300) is False def test_expired(self): pairing = PendingPairing( user_id="U001", pairing_code="ABC123", created_at=time.monotonic() - 301, ) assert pairing.is_expired(ttl_seconds=300) is True def test_expired_with_custom_ttl(self): pairing = PendingPairing( user_id="U001", pairing_code="ABC123", created_at=time.monotonic() - 11, ) assert pairing.is_expired(ttl_seconds=10) is True class TestPairingManager: @pytest.fixture def manager(self): return PairingManager(ttl_seconds=300) @pytest.mark.asyncio async def test_generate_pairing_new_user(self, manager): decision = await manager.generate_pairing("U001") assert decision.allowed is False assert decision.requires_pairing is True assert decision.pairing_code is not None assert len(decision.pairing_code) == 6 assert decision.reason == "pairing_required" @pytest.mark.asyncio async def test_generate_pairing_already_approved(self, manager): manager._approved_users.add("U001") decision = await manager.generate_pairing("U001") assert decision.allowed is True assert decision.reason == "user_approved" @pytest.mark.asyncio async def test_generate_pairing_duplicate_returns_existing_code(self, manager): decision1 = await manager.generate_pairing("U001") decision2 = await manager.generate_pairing("U001") assert decision2.pairing_code == decision1.pairing_code assert decision2.reason == "pending_pairing_exists" @pytest.mark.asyncio async def test_approve_with_correct_code(self, manager): decision = await manager.generate_pairing("U001") user_id = await manager.approve(decision.pairing_code) assert user_id == "U001" assert manager.is_approved("U001") is True @pytest.mark.asyncio async def test_approve_with_slack_prefix(self, manager): decision = await manager.generate_pairing("U001") user_id = await manager.approve(f"slack:{decision.pairing_code}") assert user_id == "U001" @pytest.mark.asyncio async def test_approve_with_wrong_code(self, manager): await manager.generate_pairing("U001") user_id = await manager.approve("WRONG") assert user_id is None @pytest.mark.asyncio async def test_approve_expired_pairing(self, manager): async with manager._lock: pairing = PendingPairing( user_id="U001", pairing_code="EXP123", created_at=time.monotonic() - 301, ) manager._pending["U001"] = pairing user_id = await manager.approve("EXP123") assert user_id is None assert "U001" not in manager._pending @pytest.mark.asyncio async def test_is_approved(self, manager): assert manager.is_approved("U001") is False manager._approved_users.add("U001") assert manager.is_approved("U001") is True @pytest.mark.asyncio async def test_clear_expired(self, manager): async with manager._lock: expired = PendingPairing( user_id="U001", pairing_code="OLD1", created_at=time.monotonic() - 301, ) fresh = PendingPairing( user_id="U002", pairing_code="NEW1", created_at=time.monotonic(), ) manager._pending["U001"] = expired manager._pending["U002"] = fresh await manager.clear_expired() async with manager._lock: assert "U001" not in manager._pending assert "U002" in manager._pending @pytest.mark.asyncio async def test_clear_all(self, manager): async with manager._lock: manager._pending["U001"] = PendingPairing(user_id="U001", pairing_code="C1") manager._pending["U002"] = PendingPairing(user_id="U002", pairing_code="C2") await manager.clear_all() async with manager._lock: assert len(manager._pending) == 0 @pytest.mark.asyncio async def test_pending_count(self, manager): assert await manager.pending_count() == 0 await manager.generate_pairing("U001") assert await manager.pending_count() == 1 await manager.generate_pairing("U002") assert await manager.pending_count() == 2 @pytest.mark.asyncio async def test_pending_count_clears_expired(self, manager): async with manager._lock: expired = PendingPairing( user_id="U001", pairing_code="OLD1", created_at=time.monotonic() - 301, ) manager._pending["U001"] = expired assert await manager.pending_count() == 0 @pytest.mark.asyncio async def test_multiple_users_concurrent(self, manager): d1 = await manager.generate_pairing("U001") d2 = await manager.generate_pairing("U002") assert d1.pairing_code != d2.pairing_code user = await manager.approve(d1.pairing_code) assert user == "U001" assert manager.is_approved("U001") is True assert manager.is_approved("U002") is False @pytest.mark.asyncio async def test_generate_pairing_code_format(self, manager): decision = await manager.generate_pairing("U001") code = decision.pairing_code assert len(code) == 6 assert code.isalnum() @pytest.mark.asyncio async def test_reapprove_after_clear(self, manager): d1 = await manager.generate_pairing("U001") await manager.approve(d1.pairing_code) assert manager.is_approved("U001") is True manager._approved_users.discard("U001") d2 = await manager.generate_pairing("U001") assert d2.allowed is False assert d2.requires_pairing is True