162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
|
|
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 == []
|