from __future__ import annotations from unittest.mock import AsyncMock import pytest from slack_sdk.errors import SlackApiError from yuxi.channels.adapters.slack.scopes import ( ScopesInfo, fetch_scopes, validate_required_scopes, ) class TestScopesInfo: def test_defaults(self): info = ScopesInfo() assert info.bot_scopes == [] assert info.user_scopes == [] assert info.error == "" def test_has_chat_write(self): info = ScopesInfo(bot_scopes=["chat:write", "files:read"]) assert info.has_chat_write is True assert info.has_chat_write_customize is False assert info.has_files_write is False def test_has_chat_write_customize(self): info = ScopesInfo(bot_scopes=["chat:write.customize"]) assert info.has_chat_write_customize is True assert info.has_chat_write is False def test_has_files_write(self): info = ScopesInfo(bot_scopes=["files:write"]) assert info.has_files_write is True def test_has_channels_manage(self): info = ScopesInfo(bot_scopes=["channels:manage"]) assert info.has_channels_manage is True def test_empty_scopes(self): info = ScopesInfo(bot_scopes=[]) assert info.has_chat_write is False assert info.has_chat_write_customize is False assert info.has_files_write is False assert info.has_channels_manage is False def test_to_dict(self): info = ScopesInfo( bot_scopes=["chat:write", "files:write"], user_scopes=["identity.basic"], error="", ) d = info.to_dict() assert d["bot_scopes"] == ["chat:write", "files:write"] assert d["has_chat_write"] is True assert d["has_files_write"] is True assert d["has_channels_manage"] is False class TestFetchScopes: @pytest.mark.asyncio async def test_fetch_scopes_success(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock( return_value={"headers": {"x-oauth-scopes": "chat:write, files:read, channels:read"}} ) result = await fetch_scopes(mock_client) assert result.error == "" assert "chat:write" in result.bot_scopes assert "files:read" in result.bot_scopes assert "channels:read" in result.bot_scopes @pytest.mark.asyncio async def test_fetch_scopes_auth_test_fails(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": False, "error": "invalid_auth"}) result = await fetch_scopes(mock_client) assert "invalid_auth" in result.error assert result.bot_scopes == [] @pytest.mark.asyncio async def test_fetch_scopes_slack_api_error(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(side_effect=SlackApiError("error", {"ok": False, "error": "token_revoked"})) result = await fetch_scopes(mock_client) assert result.error != "" assert result.bot_scopes == [] @pytest.mark.asyncio async def test_fetch_scopes_generic_exception(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(side_effect=Exception("network error")) result = await fetch_scopes(mock_client) assert "network error" in result.error @pytest.mark.asyncio async def test_fetch_scopes_scopes_as_list(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock(return_value={"headers": {"x-oauth-scopes": ["chat:write", "files:read"]}}) result = await fetch_scopes(mock_client) assert result.bot_scopes == ["chat:write", "files:read"] @pytest.mark.asyncio async def test_fetch_scopes_empty_scopes(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock(return_value={"headers": {"x-oauth-scopes": ""}}) result = await fetch_scopes(mock_client) assert result.bot_scopes == [] class TestValidateRequiredScopes: @pytest.mark.asyncio async def test_all_present(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock( return_value={"headers": {"x-oauth-scopes": "chat:write, files:write, channels:manage"}} ) ok, missing = await validate_required_scopes(mock_client, ["chat:write", "files:write"]) assert ok is True assert missing == [] @pytest.mark.asyncio async def test_some_missing(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock(return_value={"headers": {"x-oauth-scopes": "chat:write"}}) ok, missing = await validate_required_scopes(mock_client, ["chat:write", "files:write"]) assert ok is False assert "files:write" in missing @pytest.mark.asyncio async def test_all_missing(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock(return_value={"headers": {"x-oauth-scopes": "chat:write"}}) ok, missing = await validate_required_scopes(mock_client, ["admin", "audit:read"]) assert ok is False assert set(missing) == {"admin", "audit:read"} @pytest.mark.asyncio async def test_empty_required(self): mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={"ok": True}) mock_client.api_call = AsyncMock(return_value={"headers": {"x-oauth-scopes": "chat:write"}}) ok, missing = await validate_required_scopes(mock_client, []) assert ok is True assert missing == []