208 lines
7.0 KiB
Python
208 lines
7.0 KiB
Python
|
|
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
|