""" Comprehensive integration tests for SlackEnhancedService Tests cover: - Authentication (OAuth, token management, encryption) - Messaging (send, edit, DM, threaded, blocks, attachments) - Channels (list, create, join, invite, archive) - Webhooks (event handling, signature verification) - Files (upload, download, list, delete) - Users (list, info, search) - Error handling (rate limits, API errors, network failures) - Rate limiting (Redis and local) - Caching (messages, channels, files) Target: 550+ lines, 25-30 tests, 80%+ coverage """ import pytest import asyncio import json import time import hmac import hashlib import base64 from datetime import datetime from unittest.mock import Mock, AsyncMock, MagicMock, patch from slack_sdk.errors import SlackApiError from integrations.slack_enhanced_service import ( SlackEnhancedService, SlackWorkspace, SlackChannel, SlackMessage, SlackFile, SlackEventType, SlackConnectionStatus, SlackRateLimiter ) # ============================================================================ # Test Fixtures # ============================================================================ @pytest.fixture def mock_config(): """Mock configuration for SlackEnhancedService""" # Generate valid Fernet key (32 bytes, then base64-encoded) import os from cryptography.fernet import Fernet encryption_key = Fernet.generate_key().decode() return { 'client_id': 'test_client_id', 'client_secret': 'test_client_secret', 'signing_secret': 'test_signing_secret', 'redirect_uri': 'http://localhost:3000/callback', 'encryption_key': encryption_key, 'redis': { 'enabled': False, 'host': 'localhost', 'port': 6379, 'db': 0 }, 'database': None } @pytest.fixture def mock_workspace(): """Mock Slack workspace""" return SlackWorkspace( team_id='T123456', team_name='Test Workspace', domain='test-workspace', url='https://test-workspace.slack.com', access_token='xoxb-test-token', bot_token='xoxb-test-bot-token', user_id='U123456', bot_id='B123456', scopes=['channels:read', 'chat:write'], is_active=True ) @pytest.fixture def mock_channel(): """Mock Slack channel""" return SlackChannel( channel_id='C123456', name='test-channel', display_name='Test Channel', purpose='Testing purposes', topic='Testing', is_private=False, is_archived=False, workspace_id='T123456', num_members=5 ) @pytest.fixture def mock_message(): """Mock Slack message""" return SlackMessage( message_id='1234567890.123456', text='Test message', user_id='U123456', user_name='testuser', channel_id='C123456', channel_name='test-channel', workspace_id='T123456', timestamp='1234567890.123456', thread_ts=None, reply_count=0 ) @pytest.fixture def slack_service(mock_config): """Create SlackEnhancedService instance for testing""" service = SlackEnhancedService(mock_config) yield service # Cleanup asyncio.run(service.close()) # ============================================================================ # Test Class 1: TestSlackAuthentication (4 tests) # ============================================================================ class TestSlackAuthentication: """Test Slack authentication and token management""" def test_generate_oauth_url(self, slack_service): """Test OAuth URL generation with custom scopes""" state = 'test_state_123' user_id = 'U123456' scopes = ['channels:read', 'chat:write'] url = slack_service.generate_oauth_url(state, user_id, scopes) assert url.startswith('https://slack.com/oauth/v2/authorize') assert 'client_id=test_client_id' in url assert 'scope=channels:read+chat:write' in url or 'scope=channels:read chat:write' in url assert f'state={state}' in url assert f'user={user_id}' in url def test_generate_oauth_url_default_scopes(self, slack_service): """Test OAuth URL generation with default scopes""" url = slack_service.generate_oauth_url('state', 'user_id') # Should include required scopes assert 'client_id=test_client_id' in url assert 'redirect_uri' in url # Default scopes should be extensive assert 'channels:read' in url def test_token_encryption_decryption(self, slack_service): """Test token encryption and decryption cycle""" original_token = 'xoxb-test-encrypted-token' # Encrypt encrypted = slack_service._encrypt_token(original_token) assert encrypted != original_token assert len(encrypted) > 0 # Decrypt decrypted = slack_service._decrypt_token(encrypted) assert decrypted == original_token def test_token_encryption_without_cipher(self, mock_config): """Test token handling when encryption is not configured""" mock_config['encryption_key'] = None service = SlackEnhancedService(mock_config) # Should return token as-is when no cipher token = 'xoxb-test-token' assert service._encrypt_token(token) == token assert service._decrypt_token(token) == token # ============================================================================ # Test Class 2: TestSlackMessaging (8 tests) # ============================================================================ class TestSlackMessaging: """Test Slack messaging functionality""" @pytest.mark.asyncio async def test_send_message_success(self, slack_service, mock_workspace): """Test sending a message to a channel successfully""" workspace_id = 'T123456' channel_id = 'C123456' text = 'Hello, Slack!' # Mock client and workspace with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123456', 'channel': channel_id, 'message': {'text': text} }) mock_get_client.return_value = mock_client result = await slack_service.send_message(workspace_id, channel_id, text) assert result['ok'] is True assert result['message_id'] == '1234567890.123456' assert result['channel_id'] == channel_id mock_client.chat_postMessage.assert_called_once() @pytest.mark.asyncio async def test_send_message_with_blocks(self, slack_service, mock_workspace): """Test sending a message with block kit blocks""" workspace_id = 'T123456' channel_id = 'C123456' text = 'Block message' blocks = [ { "type": "section", "text": { "type": "mrkdwn", "text": "Test block" } } ] with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123456', 'channel': channel_id, 'message': {'blocks': blocks} }) mock_get_client.return_value = mock_client result = await slack_service.send_message( workspace_id, channel_id, text, blocks=blocks ) assert result['ok'] is True mock_client.chat_postMessage.assert_called_once() call_args = mock_client.chat_postMessage.call_args[1] assert 'blocks' in call_args assert call_args['blocks'] == blocks @pytest.mark.asyncio async def test_send_threaded_message(self, slack_service, mock_workspace): """Test sending a threaded reply""" workspace_id = 'T123456' channel_id = 'C123456' thread_ts = '1234567890.123456' text = 'Thread reply' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123457', 'channel': channel_id, 'message': {'text': text} }) mock_get_client.return_value = mock_client result = await slack_service.send_message( workspace_id, channel_id, text, thread_ts=thread_ts ) assert result['ok'] is True mock_client.chat_postMessage.assert_called_once() # Check if thread_ts was passed (may be in kwargs) if mock_client.chat_postMessage.call_args: call_kwargs = mock_client.chat_postMessage.call_args[1] assert 'thread_ts' in call_kwargs assert call_kwargs['thread_ts'] == thread_ts @pytest.mark.asyncio async def test_send_message_rate_limit_error(self, slack_service, mock_workspace): """Test handling rate limit errors when sending messages""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(side_effect=SlackApiError( message='Rate limited', response={'data': {'error': 'ratelimited'}} )) mock_get_client.return_value = mock_client result = await slack_service.send_message('T123', 'C123', 'test') assert result['ok'] is False assert 'error' in result @pytest.mark.asyncio async def test_send_dm_success(self, slack_service, mock_workspace): """Test sending direct message to user""" workspace_id = 'T123456' user_id = 'U123456' text = 'Direct message' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() # Mock conversations_open for DM mock_client.conversations_open = AsyncMock(return_value={ 'ok': True, 'channel': {'id': 'D123456'} }) # Mock chat_postMessage mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123456', 'channel': 'D123456', 'message': {'text': text, 'ts': '1234567890.123456'} }) mock_get_client.return_value = mock_client result = await slack_service.send_dm(workspace_id, user_id, text) assert result['ok'] is True assert result['channel'] == 'D123456' assert result['user_id'] == user_id mock_client.conversations_open.assert_called_once() mock_client.chat_postMessage.assert_called_once() @pytest.mark.asyncio async def test_add_reaction(self, slack_service, mock_workspace): """Test adding reaction to message""" workspace_id = 'T123456' channel_id = 'C123456' timestamp = '1234567890.123456' reaction = 'thumbsup' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.reactions_add = AsyncMock(return_value={'ok': True}) mock_get_client.return_value = mock_client result = await slack_service.add_reaction( workspace_id, channel_id, timestamp, reaction ) assert result['ok'] is True assert result['reaction'] == 'thumbsup' # Colons stripped mock_client.reactions_add.assert_called_once() @pytest.mark.asyncio async def test_pin_message(self, slack_service, mock_workspace): """Test pinning a message""" workspace_id = 'T123456' channel_id = 'C123456' timestamp = '1234567890.123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.pins_add = AsyncMock(return_value={'ok': True}) mock_get_client.return_value = mock_client result = await slack_service.pin_message( workspace_id, channel_id, timestamp ) assert result['ok'] is True mock_client.pins_add.assert_called_once() @pytest.mark.asyncio async def test_search_messages(self, slack_service, mock_workspace): """Test searching messages""" workspace_id = 'T123456' query = 'test query' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.search_messages = AsyncMock(return_value={ 'ok': True, 'messages': { 'matches': [ { 'ts': '1234567890.123456', 'text': 'Test message', 'user': 'U123456', 'channel': {'id': 'C123456', 'name': 'test-channel'} } ], 'total': 1, 'paging': {'page': 1, 'pages': 1} } }) mock_get_client.return_value = mock_client result = await slack_service.search_messages(workspace_id, query) assert result['ok'] is True assert len(result['messages']) == 1 assert result['messages'][0].text == 'Test message' # ============================================================================ # Test Class 3: TestSlackChannels (6 tests) # ============================================================================ class TestSlackChannels: """Test Slack channel operations""" @pytest.mark.asyncio async def test_get_channels_success(self, slack_service, mock_workspace): """Test getting list of channels""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_list = AsyncMock(return_value={ 'ok': True, 'channels': [ { 'id': 'C123456', 'name': 'test-channel', 'is_private': False, 'is_archived': False, 'is_general': False, 'is_shared': False, 'num_members': 5, 'created': 1234567890, 'purpose': {'value': 'Testing'}, 'topic': {'value': 'Test topic'} } ] }) mock_get_client.return_value = mock_client channels = await slack_service.get_channels(workspace_id) assert len(channels) == 1 assert channels[0].channel_id == 'C123456' assert channels[0].name == 'test-channel' assert channels[0].num_members == 5 @pytest.mark.asyncio async def test_get_channels_with_private(self, slack_service, mock_workspace): """Test getting channels including private channels""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_list = AsyncMock(return_value={ 'ok': True, 'channels': [ { 'id': 'C123456', 'name': 'private-channel', 'is_private': True, 'is_im': False, 'is_mpim': False, 'created': 1234567890 } ] }) mock_get_client.return_value = mock_client channels = await slack_service.get_channels( workspace_id, include_private=True ) assert len(channels) == 1 assert channels[0].is_private is True @pytest.mark.asyncio async def test_create_channel(self, slack_service, mock_workspace): """Test creating a new channel""" workspace_id = 'T123456' name = 'new-test-channel' description = 'New test channel' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_create = AsyncMock(return_value={ 'ok': True, 'channel': { 'id': 'C789012', 'name': name, 'is_private': False, 'created': 1234567890 } }) mock_client.conversations_setTopic = AsyncMock(return_value={'ok': True}) mock_get_client.return_value = mock_client result = await slack_service.create_channel( workspace_id, name, is_private=False, description=description ) assert result['ok'] is True assert result['channel_id'] == 'C789012' assert result['channel_name'] == name @pytest.mark.asyncio async def test_invite_to_channel(self, slack_service, mock_workspace): """Test inviting users to a channel""" workspace_id = 'T123456' channel_id = 'C123456' user_ids = ['U123456', 'U789012'] with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_invite = AsyncMock(return_value={ 'ok': True, 'channel': {'id': channel_id} }) mock_get_client.return_value = mock_client result = await slack_service.invite_to_channel( workspace_id, channel_id, user_ids ) assert result['ok'] is True assert len(result['invited_users']) == 2 assert len(result['failed_users']) == 0 @pytest.mark.asyncio async def test_invite_to_channel_partial_failure(self, slack_service, mock_workspace): """Test inviting users with some failures""" workspace_id = 'T123456' channel_id = 'C123456' user_ids = ['U123456', 'U789012'] with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() # First invite succeeds, second fails mock_client.conversations_invite = AsyncMock( side_effect=[ {'ok': True, 'channel': {'id': channel_id}}, SlackApiError('User not found', {'data': {'error': 'user_not_found'}}) ] ) mock_get_client.return_value = mock_client result = await slack_service.invite_to_channel( workspace_id, channel_id, user_ids ) assert result['ok'] is True # Partial success assert len(result['invited_users']) == 1 assert len(result['failed_users']) == 1 @pytest.mark.asyncio async def test_get_channel_history(self, slack_service, mock_workspace): """Test getting channel message history""" workspace_id = 'T123456' channel_id = 'C123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_history = AsyncMock(return_value={ 'ok': True, 'messages': [ { 'ts': '1234567890.123456', 'text': 'Test message', 'user': 'U123456', 'type': 'message' } ] }) mock_get_client.return_value = mock_client messages = await slack_service.get_channel_history(workspace_id, channel_id) assert len(messages) == 1 assert messages[0].text == 'Test message' assert messages[0].user_id == 'U123456' # ============================================================================ # Test Class 4: TestSlackFiles (4 tests) # ============================================================================ class TestSlackFiles: """Test Slack file operations""" @pytest.mark.asyncio async def test_upload_file(self, slack_service, mock_workspace, tmp_path): """Test uploading file to channel""" workspace_id = 'T123456' channel_id = 'C123456' # Create temporary file test_file = tmp_path / "test.txt" test_file.write_text("Test content") with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() # Note: SlackFile.__init__ requires 'created' parameter # We're mocking the API response, not creating the object directly mock_client.files_upload_v2 = AsyncMock(return_value={ 'ok': True, 'file': { 'id': 'F123456', 'name': 'test.txt', 'title': 'test.txt', 'mimetype': 'text/plain', 'filetype': 'text', 'pretty_type': 'Plain Text', 'size': 12, 'url_private': 'https://files.slack.com/files-pri/T123/F123/test.txt', 'permalink': 'https://test-workspace.slack.com/files/F123/test.txt', 'user': 'U123456', 'timestamp': 1234567890.123456, # Unix timestamp (float) 'created': 1234567890, # Unix timestamp (int) for created field 'is_public': False, 'is_editable': True } }) mock_get_client.return_value = mock_client result = await slack_service.upload_file( workspace_id, channel_id, str(test_file) ) assert result['ok'] is True assert result['file']['file_id'] == 'F123456' @pytest.mark.asyncio async def test_upload_file_with_comment(self, slack_service, mock_workspace, tmp_path): """Test uploading file with initial comment""" workspace_id = 'T123456' channel_id = 'C123456' title = 'Test File Title' comment = 'Here is the file' test_file = tmp_path / "test.txt" test_file.write_text("Test content") with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.files_upload_v2 = AsyncMock(return_value={ 'ok': True, 'file': { 'id': 'F123456', 'name': 'test.txt', 'title': title, 'mimetype': 'text/plain', 'filetype': 'text', 'pretty_type': 'Plain Text', 'size': 12, 'url_private': 'https://files.slack.com/files-pri/T123/F123/test.txt', 'permalink': 'https://test-workspace.slack.com/files/F123/test.txt', 'user': 'U123456', 'timestamp': 1234567890.123456, 'created': 1234567890 } }) mock_get_client.return_value = mock_client result = await slack_service.upload_file( workspace_id, channel_id, str(test_file), title=title, initial_comment=comment ) assert result['ok'] is True mock_client.files_upload_v2.assert_called_once() call_args = mock_client.files_upload_v2.call_args[1] assert call_args['title'] == title assert call_args['initial_comment'] == comment @pytest.mark.asyncio async def test_upload_file_rate_limit_error(self, slack_service, mock_workspace, tmp_path): """Test handling rate limit when uploading file""" test_file = tmp_path / "test.txt" test_file.write_text("Test content") with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.files_upload_v2 = AsyncMock( side_effect=SlackApiError('Rate limited', {'data': {'error': 'ratelimited'}}) ) mock_get_client.return_value = mock_client result = await slack_service.upload_file( 'T123', 'C123', str(test_file) ) assert result['ok'] is False assert 'error' in result @pytest.mark.asyncio async def test_upload_file_api_error(self, slack_service, mock_workspace, tmp_path): """Test handling API error when uploading file""" test_file = tmp_path / "test.txt" test_file.write_text("Test content") with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.files_upload_v2 = AsyncMock(return_value={ 'ok': False, 'error': 'file_not_found' }) mock_get_client.return_value = mock_client result = await slack_service.upload_file( 'T123', 'C123', str(test_file) ) assert result['ok'] is False assert 'error' in result # ============================================================================ # Test Class 5: TestSlackWebhooks (6 tests) # ============================================================================ class TestSlackWebhooks: """Test Slack webhook handling""" @pytest.mark.asyncio async def test_verify_webhook_signature_valid(self, slack_service): """Test verifying valid webhook signature""" body = b'{"type":"url_verification","token":"test"}' timestamp = str(int(time.time())) signing_secret = 'test_signing_secret' # Create signature sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}" signature = 'v0=' + hmac.new( signing_secret.encode(), sig_basestring.encode(), hashlib.sha256 ).hexdigest() result = await slack_service.verify_webhook_signature(body, timestamp, signature) assert result is True @pytest.mark.asyncio async def test_verify_webhook_signature_invalid(self, slack_service): """Test rejecting invalid webhook signature""" body = b'{"type":"url_verification","token":"test"}' timestamp = str(int(time.time())) invalid_signature = 'v0=invalid_signature' result = await slack_service.verify_webhook_signature(body, timestamp, invalid_signature) assert result is False @pytest.mark.asyncio async def test_verify_webhook_signature_expired(self, slack_service): """Test rejecting expired webhook timestamp""" body = b'{"type":"url_verification","token":"test"}' old_timestamp = str(int(time.time()) - 400) # 6+ minutes ago signing_secret = 'test_signing_secret' sig_basestring = f"v0:{old_timestamp}:{body.decode('utf-8')}" signature = 'v0=' + hmac.new( signing_secret.encode(), sig_basestring.encode(), hashlib.sha256 ).hexdigest() result = await slack_service.verify_webhook_signature(body, old_timestamp, signature) assert result is False @pytest.mark.asyncio async def test_verify_webhook_signature_no_secret(self, mock_config): """Test signature verification fails without signing secret""" mock_config['signing_secret'] = None service = SlackEnhancedService(mock_config) result = await service.verify_webhook_signature(b'body', 'timestamp', 'signature') assert result is False @pytest.mark.asyncio async def test_handle_webhook_event_message(self, slack_service): """Test handling message webhook event""" event_data = { 'team_id': 'T123456', 'event': { 'type': 'message', 'user': 'U123456', 'text': 'Hello', 'ts': '1234567890.123456', 'channel': 'C123456' } } result = await slack_service.handle_webhook_event(event_data) assert result['ok'] is True assert result['event_type'] == 'message' assert result['handled'] is True @pytest.mark.asyncio async def test_handle_webhook_event_with_custom_handler(self, slack_service): """Test webhook event with registered custom handler""" event_data = { 'team_id': 'T123456', 'event': { 'type': 'reaction_added', 'user': 'U123456', 'reaction': 'thumbsup', 'item': {'type': 'message', 'channel': 'C123456'} } } # Register custom handler handler_called = [] async def custom_handler(event): handler_called.append(event['event']['type']) slack_service.register_event_handler(SlackEventType.REACTION_ADD, custom_handler) result = await slack_service.handle_webhook_event(event_data) assert result['ok'] is True assert len(handler_called) == 1 assert handler_called[0] == 'reaction_added' # ============================================================================ # Test Class 6: TestSlackErrorHandling (5 tests) # ============================================================================ class TestSlackErrorHandling: """Test Slack error handling and retry logic""" @pytest.mark.asyncio async def test_connection_test_success(self, slack_service, mock_workspace): """Test successful connection test""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.auth_test = AsyncMock(return_value={ 'ok': True, 'team_id': 'T123456', 'team': 'Test Workspace', 'user_id': 'U123456', 'user': 'testuser' }) mock_get_client.return_value = mock_client result = await slack_service.test_connection('T123456') assert result['connected'] is True assert result['status'] == 'connected' assert slack_service.connection_status['T123456'] == SlackConnectionStatus.CONNECTED @pytest.mark.asyncio async def test_connection_test_failure(self, slack_service): """Test connection test failure""" with patch.object(slack_service, '_get_workspace', return_value=None): result = await slack_service.test_connection('T999999') assert result['connected'] is False assert result['status'] == 'error' assert slack_service.connection_status['T999999'] == SlackConnectionStatus.ERROR @pytest.mark.asyncio async def test_connection_test_rate_limited(self, slack_service, mock_workspace): """Test connection test with rate limit""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() # Create proper SlackApiError with response dict containing 'data' mock_response = {'data': {'error': 'ratelimited'}, 'headers': {'Retry-After': '60'}} mock_client.auth_test = AsyncMock( side_effect=SlackApiError('Rate limited', response=mock_response) ) mock_get_client.return_value = mock_client result = await slack_service.test_connection('T123456') assert result['connected'] is False assert result['status'] == 'rate_limited' assert 'retry_after' in result assert slack_service.connection_status['T123456'] == SlackConnectionStatus.RATE_LIMITED @pytest.mark.asyncio async def test_workspace_not_found(self, slack_service): """Test handling when workspace is not found""" with patch.object(slack_service, '_get_workspace', return_value=None): result = await slack_service.send_message('T999', 'C123', 'test') assert result['ok'] is False assert 'error' in result @pytest.mark.asyncio async def test_client_creation_error(self, slack_service): """Test error when client creation fails""" # Mock workspace exists but client creation fails with patch.object(slack_service, '_get_workspace', return_value=Mock()): with patch.object(slack_service, '_get_client', return_value=None): result = await slack_service.send_message('T123', 'C123', 'test') assert result['ok'] is False # ============================================================================ # Test Class 7: TestSlackRateLimiter (4 tests) # ============================================================================ class TestSlackRateLimiter: """Test Slack API rate limiting""" @pytest.mark.asyncio async def test_rate_limit_check_under_limit(self): """Test rate limit check when under limit""" limiter = SlackRateLimiter(redis_client=None) # First request should pass result = await limiter.check_limit('T123456', 'chat.postMessage') assert result is True @pytest.mark.asyncio async def test_rate_limit_exceeded(self): """Test rate limit when exceeded""" limiter = SlackRateLimiter(redis_client=None) # Make requests up to limit (1 per second for chat.postMessage) await limiter.check_limit('T123456', 'chat.postMessage') # Immediate second request should fail result = await limiter.check_limit('T123456', 'chat.postMessage') assert result is False @pytest.mark.asyncio async def test_rate_limit_reset_after_window(self): """Test rate limit resets after time window""" limiter = SlackRateLimiter(redis_client=None) # First request await limiter.check_limit('T123456', 'chat.postMessage') # Wait for window to expire (1 second for chat.postMessage) await asyncio.sleep(1.1) # Should be allowed again result = await limiter.check_limit('T123456', 'chat.postMessage') assert result is True @pytest.mark.asyncio async def test_rate_limit_different_endpoints(self): """Test rate limits are per-endpoint""" limiter = SlackRateLimiter(redis_client=None) # Same workspace, different endpoints result1 = await limiter.check_limit('T123456', 'chat.postMessage') result2 = await limiter.check_limit('T123456', 'conversations.list') # Should both pass (different endpoints) assert result1 is True assert result2 is True # ============================================================================ # Test Class 8: TestSlackWorkspace (4 tests) # ============================================================================ class TestSlackWorkspace: """Test workspace management""" @pytest.mark.asyncio async def test_get_workspaces_empty(self, slack_service): """Test getting workspaces when none exist""" with patch.object(slack_service, '_get_workspace', return_value=None): workspaces = await slack_service.get_workspaces() assert workspaces == [] @pytest.mark.asyncio async def test_get_workspaces_with_filter(self, slack_service, mock_workspace): """Test getting workspaces filtered by user""" user_id = 'U123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): # Mock redis for this test slack_service.redis_client = Mock() slack_service.redis_client.keys = Mock(return_value=[]) slack_service.redis_client.get = Mock(return_value=None) workspaces = await slack_service.get_workspaces(user_id=user_id) # Since we mock db=None and redis has no keys, returns empty assert isinstance(workspaces, list) @pytest.mark.asyncio async def test_save_workspace_success(self, slack_service, mock_workspace): """Test saving workspace successfully""" slack_service.redis_client = Mock() slack_service.redis_client.setex = Mock() result = slack_service._save_workspace(mock_workspace) assert result is True assert slack_service.connection_status[mock_workspace.team_id] == SlackConnectionStatus.CONNECTED @pytest.mark.asyncio async def test_save_workspace_with_encryption(self, slack_service, mock_workspace): """Test workspace token is encrypted when saved""" slack_service.redis_client = Mock() slack_service.redis_client.setex = Mock() original_token = mock_workspace.access_token slack_service._save_workspace(mock_workspace) # Verify encryption happened encrypted_token = slack_service._encrypt_token(original_token) assert encrypted_token != original_token # ============================================================================ # Test Class 9: TestSlackMentions (2 tests) # ============================================================================ class TestSlackMentions: """Test mention extraction from messages""" def test_extract_mentions_single(self, slack_service): """Test extracting single user mention""" text = "Hello <@U123456>, how are you?" mentions = slack_service._extract_mentions(text) assert len(mentions) == 1 assert 'U123456' in mentions def test_extract_mentions_multiple(self, slack_service): """Test extracting multiple user mentions""" text = "<@U123456> and <@U789012> and <@WABCDEF>" mentions = slack_service._extract_mentions(text) assert len(mentions) == 3 assert 'U123456' in mentions assert 'U789012' in mentions assert 'WABCDEF' in mentions # ============================================================================ # Test Class 10: TestSlackServiceInfo (1 test) # ============================================================================ class TestSlackServiceInfo: """Test service information""" @pytest.mark.asyncio async def test_get_service_info(self, slack_service): """Test getting service information""" info = await slack_service.get_service_info() assert info['name'] == 'Slack Enhanced Service' assert info['version'] == '3.0.0' assert isinstance(info['features'], list) assert 'message_management' in info['features'] assert 'rate_limiting' in info['features'] assert isinstance(info['supported_operations'], list) assert 'send_message' in info['supported_operations'] # ============================================================================ # Test Class 11: TestSlackOAuth (3 tests) # ============================================================================ class TestSlackOAuth: """Test OAuth token exchange flow""" @pytest.mark.asyncio async def test_exchange_code_for_tokens_success(self, slack_service): """Test successful OAuth code exchange""" code = 'test_auth_code' state = 'test_state' # Mock Redis for workspace save slack_service.redis_client = Mock() slack_service.redis_client.setex = Mock() with patch('httpx.AsyncClient') as mock_http_client: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { 'ok': True, 'access_token': 'xoxb-test-access-token', 'bot_user_id': 'B123456', 'team': { 'id': 'T123456', 'name': 'Test Workspace', 'domain': 'test-workspace' }, 'authed_user': { 'id': 'U123456' }, 'scope': 'channels:read,chat:write' } mock_post = AsyncMock(return_value=mock_response) mock_http_client.return_value.__aenter__.return_value.post = mock_post result = await slack_service.exchange_code_for_tokens(code, state) assert result['ok'] is True assert 'workspace' in result @pytest.mark.asyncio async def test_exchange_code_for_tokens_api_error(self, slack_service): """Test OAuth code exchange with API error""" code = 'invalid_code' with patch('httpx.AsyncClient') as mock_http_client: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { 'ok': False, 'error': 'invalid_code' } mock_post = AsyncMock(return_value=mock_response) mock_http_client.return_value.__aenter__.return_value.post = mock_post result = await slack_service.exchange_code_for_tokens(code, 'state') assert result['ok'] is False assert 'error' in result @pytest.mark.asyncio async def test_exchange_code_for_tokens_http_error(self, slack_service): """Test OAuth code exchange with HTTP error""" code = 'test_code' with patch('httpx.AsyncClient') as mock_http_client: mock_response = Mock() mock_response.status_code = 500 mock_response.text = 'Internal Server Error' mock_post = AsyncMock(return_value=mock_response) mock_http_client.return_value.__aenter__.return_value.post = mock_post result = await slack_service.exchange_code_for_tokens(code, 'state') assert result['ok'] is False # ============================================================================ # Test Class 12: TestSlackClientCreation (2 tests) # ============================================================================ class TestSlackClientCreation: """Test Slack client creation and caching""" @pytest.mark.asyncio async def test_get_client_returns_none_on_error(self, slack_service): """Test client creation returns None on error""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=None): client = slack_service._get_client(workspace_id) assert client is None @pytest.mark.asyncio async def test_get_sync_client_returns_none_on_error(self, slack_service): """Test sync client creation returns None on error""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=None): client = slack_service._get_sync_client(workspace_id) assert client is None # ============================================================================ # Test Class 13: TestSlackTokenStorageFallback (2 tests) # ============================================================================ class TestSlackTokenStorageFallback: """Test token storage fallback mechanism""" @pytest.mark.asyncio async def test_workspace_from_token_storage(self, slack_service): """Test getting workspace from token storage fallback""" workspace_id = 'T123456' with patch('integrations.slack_enhanced_service.token_storage') as mock_token_storage: mock_token_storage.get_token.return_value = { 'team': {'id': workspace_id, 'name': 'Test Workspace', 'domain': 'test-workspace'}, 'access_token': 'xoxb-test-token', 'authed_user': {'id': 'U123456'}, 'scope': 'channels:read,chat:write' } # This tests the fallback path in _get_workspace # When db and redis both return None, it falls back to token_storage result = slack_service._get_workspace(workspace_id) # Should have found it via token storage assert result is not None assert result.team_id == workspace_id @pytest.mark.asyncio async def test_workspace_not_found_anywhere(self, slack_service): """Test workspace not found in any storage""" workspace_id = 'T999999' with patch('integrations.slack_enhanced_service.token_storage') as mock_token_storage: mock_token_storage.get_token.return_value = None result = slack_service._get_workspace(workspace_id) assert result is None # ============================================================================ # Test Class 14: TestSlackCaching (2 tests) # ============================================================================ class TestSlackCaching: """Test message and file caching""" @pytest.mark.asyncio async def test_cache_message(self, slack_service, mock_message): """Test caching individual message""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=Mock()): mock_redis = Mock() slack_service.redis_client = mock_redis await slack_service._cache_message(workspace_id, {'ts': '1234567890.123456'}) mock_redis.setex.assert_called_once() @pytest.mark.asyncio async def test_cache_messages_batch(self, slack_service): """Test batch caching of messages""" workspace_id = 'T123456' channel_id = 'C123456' messages = [ SlackMessage( message_id='1234567890.123456', text='Test 1', user_id='U123456', user_name='user1', channel_id=channel_id, channel_name='test-channel', workspace_id=workspace_id, timestamp='1234567890.123456' ) ] with patch.object(slack_service, '_get_workspace', return_value=Mock()): mock_redis = Mock() slack_service.redis_client = mock_redis await slack_service._cache_messages(workspace_id, channel_id, messages) mock_redis.setex.assert_called_once() # ============================================================================ # Test Class 15: TestSlackErrorPaths (3 tests) # ============================================================================ class TestSlackErrorPaths: """Test additional error paths and edge cases""" @pytest.mark.asyncio async def test_send_message_with_attachments(self, slack_service, mock_workspace): """Test sending message with attachments""" workspace_id = 'T123456' channel_id = 'C123456' text = 'Message with attachments' attachments = [ { 'title': 'Attachment 1', 'text': 'Attachment text', 'color': '#36a64f' } ] with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123456', 'channel': channel_id, 'message': {'text': text} }) mock_get_client.return_value = mock_client result = await slack_service.send_message( workspace_id, channel_id, text, attachments=attachments ) assert result['ok'] is True @pytest.mark.asyncio async def test_get_channels_api_error(self, slack_service, mock_workspace): """Test get channels with API error returns cached""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_list = AsyncMock( side_effect=SlackApiError('API Error', {'data': {'error': 'api_error'}}) ) mock_get_client.return_value = mock_client # Mock Redis cache mock_redis = Mock() mock_redis.get.return_value = json.dumps([ { 'channel_id': 'C123456', 'name': 'cached-channel', 'workspace_id': workspace_id } ]) slack_service.redis_client = mock_redis channels = await slack_service.get_channels(workspace_id) # Should return cached channels assert len(channels) == 1 assert channels[0].channel_id == 'C123456' @pytest.mark.asyncio async def test_get_channels_no_cache_no_api(self, slack_service, mock_workspace): """Test get channels with no cache and API error returns empty""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_list = AsyncMock( side_effect=SlackApiError('API Error', {'data': {'error': 'api_error'}}) ) mock_get_client.return_value = mock_client # Mock Redis with no cache mock_redis = Mock() mock_redis.get.return_value = None slack_service.redis_client = mock_redis channels = await slack_service.get_channels(workspace_id) # Should return empty list assert channels == [] # ============================================================================ # Test Class 16: TestSlackWebhookRegistration (2 tests) # ============================================================================ class TestSlackWebhookRegistration: """Test webhook handler registration""" def test_register_event_handler(self, slack_service): """Test registering custom event handler""" async def handler(event): pass slack_service.register_event_handler(SlackEventType.MESSAGE, handler) assert handler in slack_service.event_handlers[SlackEventType.MESSAGE] def test_register_webhook_handler(self, slack_service): """Test registering global webhook handler""" async def handler(event): pass slack_service.register_webhook_handler(handler) assert handler in slack_service.webhook_handlers # ============================================================================ # Test Class 17: TestSlackAdditionalCoverage (8 tests) # ============================================================================ class TestSlackAdditionalCoverage: """Additional tests to reach 80% coverage target""" @pytest.mark.asyncio async def test_send_message_unexpected_error(self, slack_service, mock_workspace): """Test sending message with unexpected error""" workspace_id = 'T123456' channel_id = 'C123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(side_effect=Exception("Unexpected error")) mock_get_client.return_value = mock_client result = await slack_service.send_message(workspace_id, channel_id, 'test') assert result['ok'] is False assert 'error' in result @pytest.mark.asyncio async def test_upload_file_unexpected_error(self, slack_service, mock_workspace, tmp_path): """Test file upload with unexpected error""" test_file = tmp_path / "test.txt" test_file.write_text("Test content") with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.files_upload_v2 = AsyncMock(side_effect=Exception("Upload failed")) mock_get_client.return_value = mock_client result = await slack_service.upload_file('T123', 'C123', str(test_file)) assert result['ok'] is False @pytest.mark.asyncio async def test_search_messages_unexpected_error(self, slack_service, mock_workspace): """Test search messages with unexpected error""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.search_messages = AsyncMock(side_effect=Exception("Search failed")) mock_get_client.return_value = mock_client result = await slack_service.search_messages('T123', 'test') assert result['ok'] is False @pytest.mark.asyncio async def test_add_reaction_unexpected_error(self, slack_service, mock_workspace): """Test add reaction with unexpected error""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.reactions_add = AsyncMock(side_effect=Exception("Reaction failed")) mock_get_client.return_value = mock_client result = await slack_service.add_reaction('T123', 'C123', '123', '+1') assert result['ok'] is False @pytest.mark.asyncio async def test_pin_message_unexpected_error(self, slack_service, mock_workspace): """Test pin message with unexpected error""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.pins_add = AsyncMock(side_effect=Exception("Pin failed")) mock_get_client.return_value = mock_client result = await slack_service.pin_message('T123', 'C123', '123') assert result['ok'] is False @pytest.mark.asyncio async def test_send_dm_unexpected_error(self, slack_service, mock_workspace): """Test send DM with unexpected error""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_open = AsyncMock(side_effect=Exception("DM failed")) mock_get_client.return_value = mock_client result = await slack_service.send_dm('T123', 'U123', 'test') assert result['ok'] is False @pytest.mark.asyncio async def test_create_channel_unexpected_error(self, slack_service, mock_workspace): """Test create channel with unexpected error""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_create = AsyncMock(side_effect=Exception("Create failed")) mock_get_client.return_value = mock_client result = await slack_service.create_channel('T123', 'test') assert result['ok'] is False @pytest.mark.asyncio async def test_close_service(self, slack_service): """Test closing service and cleanup""" # Create a client first slack_service.clients['T123'] = AsyncMock() slack_service.sync_clients['T123'] = Mock() await slack_service.close() # Clients should be cleared (close() was called) # We can't easily test this without mocking, but the call should not raise assert True @pytest.mark.asyncio async def test_get_channel_history_unexpected_error(self, slack_service, mock_workspace): """Test get channel history with unexpected error""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_history = AsyncMock(side_effect=Exception("History failed")) mock_get_client.return_value = mock_client messages = await slack_service.get_channel_history('T123', 'C123') assert messages == [] @pytest.mark.asyncio async def test_handle_webhook_event_unexpected_error(self, slack_service): """Test webhook event handler with unexpected error""" event_data = {'event': {'type': 'message'}} # Register handler that raises error async def failing_handler(event): raise Exception("Handler failed") slack_service.register_event_handler(SlackEventType.MESSAGE, failing_handler) result = await slack_service.handle_webhook_event(event_data) # Should still return ok=True (error is logged but doesn't fail) assert result['ok'] is True @pytest.mark.asyncio async def test_save_workspace_to_database(self, slack_service, mock_workspace): """Test saving workspace to database path""" # Mock database connection mock_db = Mock() mock_db.execute = Mock(return_value=Mock()) mock_db.commit = Mock() slack_service.db = mock_db result = slack_service._save_workspace(mock_workspace) assert result is True # Verify database execute was called mock_db.execute.assert_called() mock_db.commit.assert_called() @pytest.mark.asyncio async def test_get_channel_history_slack_api_error(self, slack_service, mock_workspace): """Test get channel history with SlackApiError""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_history = AsyncMock( side_effect=SlackApiError('API Error', {'data': {'error': 'api_error'}}) ) mock_get_client.return_value = mock_client messages = await slack_service.get_channel_history('T123', 'C123') # Should return empty list on error assert messages == [] @pytest.mark.asyncio async def test_search_messages_slack_api_error(self, slack_service, mock_workspace): """Test search messages with SlackApiError""" with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.search_messages = AsyncMock( side_effect=SlackApiError('Search error', {'data': {'error': 'search_error'}}) ) mock_get_client.return_value = mock_client result = await slack_service.search_messages('T123', 'test') assert result['ok'] is False assert 'error' in result @pytest.mark.asyncio async def test_send_message_attachment_format(self, slack_service, mock_workspace): """Test sending message with attachment formatting""" workspace_id = 'T123456' channel_id = 'C123456' text = 'Message' attachments = [{'text': 'Attachment'}] with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123456', 'channel': channel_id, 'message': {'text': text} }) mock_get_client.return_value = mock_client result = await slack_service.send_message( workspace_id, channel_id, text, attachments=attachments ) assert result['ok'] is True # Verify attachments were passed call_kwargs = mock_client.chat_postMessage.call_args[1] assert 'attachments' in call_kwargs assert call_kwargs['attachments'] == attachments @pytest.mark.asyncio async def test_send_dm_with_unfurl_options(self, slack_service, mock_workspace): """Test sending DM with unfurl options""" workspace_id = 'T123456' user_id = 'U123456' text = 'DM with links' blocks = [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Test'}}] with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_open = AsyncMock(return_value={ 'ok': True, 'channel': {'id': 'D123456'} }) mock_client.chat_postMessage = AsyncMock(return_value={ 'ok': True, 'ts': '1234567890.123456', 'message': {'ts': '1234567890.123456'} }) mock_get_client.return_value = mock_client result = await slack_service.send_dm( workspace_id, user_id, text, blocks=blocks, unfurl_links=False, unfurl_media=False ) assert result['ok'] is True # Verify unfurl options call_kwargs = mock_client.chat_postMessage.call_args[1] assert call_kwargs['unfurl_links'] is False assert call_kwargs['unfurl_media'] is False @pytest.mark.asyncio async def test_get_channels_api_error_returns_empty(self, slack_service, mock_workspace): """Test get channels with API error and no cache returns empty list""" workspace_id = 'T123456' with patch.object(slack_service, '_get_workspace', return_value=mock_workspace): with patch.object(slack_service, '_get_client') as mock_get_client: mock_client = AsyncMock() mock_client.conversations_list = AsyncMock( side_effect=SlackApiError('API Error', {'data': {'error': 'api_error'}}) ) mock_get_client.return_value = mock_client # Mock Redis with no cache mock_redis = Mock() mock_redis.get.return_value = None slack_service.redis_client = mock_redis channels = await slack_service.get_channels(workspace_id) # Should return empty list when no cache available assert channels == []