| """ |
| External service mocking integration tests (INTG-04). |
| |
| Tests cover: |
| - LLM provider mocking (OpenAI, Anthropic) |
| - Slack integration mocking |
| - GitHub integration mocking |
| - Google OAuth mocking |
| - Error handling for external service failures |
| """ |
|
|
| import pytest |
| import responses |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
| from unittest.mock import Mock, AsyncMock, patch |
| import json |
|
|
|
|
| class TestOpenAIMocking: |
| """Test OpenAI API mocking.""" |
|
|
| @responses.activate |
| def test_openai_chat_completion_mock(self, client: TestClient, admin_token: str): |
| """Test OpenAI chat completion with mocked response.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={ |
| "id": "chatcmpl-test123", |
| "object": "chat.completion", |
| "created": 1234567890, |
| "model": "gpt-4", |
| "choices": [{ |
| "index": 0, |
| "message": { |
| "role": "assistant", |
| "content": "Mocked response from OpenAI" |
| }, |
| "finish_reason": "stop" |
| }], |
| "usage": { |
| "prompt_tokens": 10, |
| "completion_tokens": 5, |
| "total_tokens": 15 |
| } |
| }, |
| status=200 |
| ) |
|
|
| |
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 404, 405] |
| if response.status_code == 200: |
| data = response.json() |
| assert "content" in data or "choices" in data |
|
|
| @responses.activate |
| def test_openai_error_handling(self, client: TestClient, admin_token: str): |
| """Test OpenAI API error handling.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={ |
| "error": { |
| "message": "Invalid API key", |
| "type": "invalid_request_error", |
| "code": "invalid_api_key" |
| } |
| }, |
| status=401 |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [400, 401, 404, 405, 500, 502, 503] |
|
|
| @responses.activate |
| def test_openai_rate_limiting(self, client: TestClient, admin_token: str): |
| """Test OpenAI rate limit handling.""" |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={ |
| "error": { |
| "message": "Rate limit exceeded", |
| "type": "rate_limit_error" |
| } |
| }, |
| status=429 |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [404, 405, 429, 503] |
|
|
| @responses.activate |
| def test_openai_streaming_response_mock(self, client: TestClient, admin_token: str): |
| """Test OpenAI streaming response with mocked data.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| body="data: {\"choices\": [{\"delta\": {\"content\": \"Hello\"}}]}\n\n", |
| status=200, |
| content_type="text/event-stream" |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}], |
| "stream": True |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
|
|
| @responses.activate |
| def test_openai_timeout_handling(self, client: TestClient, admin_token: str): |
| """Test OpenAI timeout handling.""" |
| |
| import time |
|
|
| def request_callback(request): |
| time.sleep(0.1) |
| return (200, {}, json.dumps({"choices": [{"message": {"content": "Response"}}]})) |
|
|
| responses.add_callback( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| callback=request_callback, |
| content_type="application/json" |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"}, |
| timeout=1.0 |
| ) |
|
|
| assert response.status_code in [200, 404, 405, 504] |
|
|
|
|
| class TestAnthropicMocking: |
| """Test Anthropic API mocking.""" |
|
|
| @responses.activate |
| def test_anthropic_message_mock(self, client: TestClient, admin_token: str): |
| """Test Anthropic message API with mocked response.""" |
| responses.add( |
| responses.POST, |
| "https://api.anthropic.com/v1/messages", |
| json={ |
| "id": "msg_test123", |
| "type": "message", |
| "role": "assistant", |
| "content": [{"type": "text", "text": "Mocked Anthropic response"}], |
| "model": "claude-3-sonnet", |
| "stop_reason": "end_turn" |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/llm/anthropic/message", |
| json={ |
| "model": "claude-3-sonnet", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
| if response.status_code == 200: |
| data = response.json() |
| assert "content" in data or "completion" in data |
|
|
| @responses.activate |
| def test_anthropic_error_handling(self, client: TestClient, admin_token: str): |
| """Test Anthropic error handling.""" |
| responses.add( |
| responses.POST, |
| "https://api.anthropic.com/v1/messages", |
| json={ |
| "error": { |
| "message": "Invalid API key", |
| "type": "invalid_request_error" |
| } |
| }, |
| status=401 |
| ) |
|
|
| response = client.post( |
| "/api/llm/anthropic/message", |
| json={ |
| "model": "claude-3-sonnet", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [400, 401, 404, 405, 500] |
|
|
| @responses.activate |
| def test_anthropic_rate_limit(self, client: TestClient, admin_token: str): |
| """Test Anthropic rate limit handling.""" |
| responses.add( |
| responses.POST, |
| "https://api.anthropic.com/v1/messages", |
| json={ |
| "error": { |
| "message": "Rate limit exceeded", |
| "type": "rate_limit_error" |
| } |
| }, |
| status=429 |
| ) |
|
|
| response = client.post( |
| "/api/llm/anthropic/message", |
| json={ |
| "model": "claude-3-sonnet", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [404, 405, 429, 503] |
|
|
| @responses.activate |
| def test_anthropic_streaming_mock(self, client: TestClient, admin_token: str): |
| """Test Anthropic streaming response.""" |
| responses.add( |
| responses.POST, |
| "https://api.anthropic.com/v1/messages", |
| body='data: {"delta": {"type": "text_delta", "text": "Hello"}}\n\n', |
| status=200, |
| content_type="text/event-stream" |
| ) |
|
|
| response = client.post( |
| "/api/llm/anthropic/message", |
| json={ |
| "model": "claude-3-sonnet", |
| "messages": [{"role": "user", "content": "Hello"}], |
| "stream": True |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
|
|
|
|
| class TestSlackIntegrationMocking: |
| """Test Slack integration with mocking.""" |
|
|
| @responses.activate |
| def test_slack_message_send_mock(self, client: TestClient, admin_token: str): |
| """Test Slack message sending with mocked response.""" |
| responses.add( |
| responses.POST, |
| "https://slack.com/api/chat.postMessage", |
| json={ |
| "ok": True, |
| "channel": "C12345", |
| "ts": "1234567890.123456", |
| "message": { |
| "text": "Test message", |
| "username": "Test Bot" |
| } |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/slack/send", |
| json={ |
| "channel": "C12345", |
| "text": "Test message" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 201, 404, 405] |
|
|
| @responses.activate |
| def test_slack_error_handling(self, client: TestClient, admin_token: str): |
| """Test Slack API error handling.""" |
| responses.add( |
| responses.POST, |
| "https://slack.com/api/chat.postMessage", |
| json={ |
| "ok": False, |
| "error": "channel_not_found" |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/slack/send", |
| json={ |
| "channel": "invalid-channel", |
| "text": "Test message" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [400, 404, 405, 500] |
|
|
| @responses.activate |
| def test_slack_authentication_error(self, client: TestClient, admin_token: str): |
| """Test Slack authentication error.""" |
| responses.add( |
| responses.POST, |
| "https://slack.com/api/chat.postMessage", |
| json={ |
| "ok": False, |
| "error": "invalid_auth" |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/slack/send", |
| json={ |
| "channel": "C12345", |
| "text": "Test message" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [400, 401, 404, 405, 500] |
|
|
| @responses.activate |
| def test_slack_rate_limiting(self, client: TestClient, admin_token: str): |
| """Test Slack rate limiting.""" |
| responses.add( |
| responses.POST, |
| "https://slack.com/api/chat.postMessage", |
| json={ |
| "ok": False, |
| "error": "rate_limited" |
| }, |
| status=429 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/slack/send", |
| json={ |
| "channel": "C12345", |
| "text": "Test message" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [404, 405, 429, 503] |
|
|
| @responses.activate |
| def test_slack_channel_list_mock(self, client: TestClient, admin_token: str): |
| """Test Slack channel list with mocked response.""" |
| responses.add( |
| responses.GET, |
| "https://slack.com/api/conversations.list", |
| json={ |
| "ok": True, |
| "channels": [ |
| {"id": "C12345", "name": "general"}, |
| {"id": "C67890", "name": "random"} |
| ] |
| }, |
| status=200 |
| ) |
|
|
| response = client.get( |
| "/api/integrations/slack/channels", |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
|
|
|
|
| class TestGitHubIntegrationMocking: |
| """Test GitHub integration with mocking.""" |
|
|
| @responses.activate |
| def test_github_issue_creation_mock(self, client: TestClient, admin_token: str): |
| """Test GitHub issue creation with mocked response.""" |
| responses.add( |
| responses.POST, |
| "https://api.github.com/repos/test/repo/issues", |
| json={ |
| "id": 12345, |
| "number": 1, |
| "title": "Test Issue", |
| "state": "open", |
| "user": {"login": "testuser"} |
| }, |
| status=201 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/github/issues", |
| json={ |
| "repo": "test/repo", |
| "title": "Test Issue", |
| "body": "Issue description" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 201, 404, 405] |
|
|
| @responses.activate |
| def test_github_authentication_error(self, client: TestClient, admin_token: str): |
| """Test GitHub authentication error handling.""" |
| responses.add( |
| responses.POST, |
| "https://api.github.com/repos/test/repo/issues", |
| json={ |
| "message": "Bad credentials", |
| "documentation_url": "https://docs.github.com/rest" |
| }, |
| status=401 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/github/issues", |
| json={ |
| "repo": "test/repo", |
| "title": "Test Issue" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [401, 404, 405, 502] |
|
|
| @responses.activate |
| def test_github_rate_limiting(self, client: TestClient, admin_token: str): |
| """Test GitHub rate limit handling.""" |
| responses.add( |
| responses.POST, |
| "https://api.github.com/repos/test/repo/issues", |
| json={ |
| "message": "API rate limit exceeded", |
| "documentation_url": "https://docs.github.com/rest" |
| }, |
| status=403 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/github/issues", |
| json={ |
| "repo": "test/repo", |
| "title": "Test Issue" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [403, 404, 405, 503] |
|
|
| @responses.activate |
| def test_github_repository_list_mock(self, client: TestClient, admin_token: str): |
| """Test GitHub repository list with mocked response.""" |
| responses.add( |
| responses.GET, |
| "https://api.github.com/user/repos", |
| json=[ |
| {"id": 1, "name": "repo1", "full_name": "user/repo1"}, |
| {"id": 2, "name": "repo2", "full_name": "user/repo2"} |
| ], |
| status=200 |
| ) |
|
|
| response = client.get( |
| "/api/integrations/github/repos", |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
|
|
| @responses.activate |
| def test_github_webhook_verification(self, client: TestClient, admin_token: str): |
| """Test GitHub webhook signature verification.""" |
| responses.add( |
| responses.POST, |
| "https://api.github.com/repos/test/repo/hooks", |
| json={ |
| "id": 12345, |
| "name": "web", |
| "active": True |
| }, |
| status=201 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/github/webhooks", |
| json={ |
| "repo": "test/repo", |
| "url": "https://example.com/webhook" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 201, 404, 405] |
|
|
|
|
| class TestGoogleOAuthMocking: |
| """Test Google OAuth with mocking.""" |
|
|
| @responses.activate |
| def test_google_oauth_flow_mock(self, client: TestClient): |
| """Test Google OAuth flow with mocked responses.""" |
| |
| responses.add( |
| responses.POST, |
| "https://oauth2.googleapis.com/token", |
| json={ |
| "access_token": "mock_access_token", |
| "expires_in": 3600, |
| "refresh_token": "mock_refresh_token", |
| "token_type": "Bearer" |
| }, |
| status=200 |
| ) |
|
|
| |
| responses.add( |
| responses.GET, |
| "https://www.googleapis.com/oauth2/v2/userinfo", |
| json={ |
| "id": "123456789", |
| "email": "test@example.com", |
| "verified_email": True, |
| "name": "Test User" |
| }, |
| status=200 |
| ) |
|
|
| response = client.get( |
| "/api/auth/google/callback?code=test_code&state=test_state" |
| ) |
|
|
| |
| assert response.status_code in [200, 302, 404, 405] |
|
|
| @responses.activate |
| def test_google_oauth_error_handling(self, client: TestClient): |
| """Test Google OAuth error handling.""" |
| responses.add( |
| responses.POST, |
| "https://oauth2.googleapis.com/token", |
| json={ |
| "error": "invalid_grant", |
| "error_description": "The code has expired." |
| }, |
| status=400 |
| ) |
|
|
| response = client.get( |
| "/api/auth/google/callback?code=expired_code&state=test_state" |
| ) |
|
|
| assert response.status_code in [400, 404, 405, 500] |
|
|
| @responses.activate |
| def test_google_oauth_invalid_state(self, client: TestClient): |
| """Test Google OAuth with invalid state parameter.""" |
| response = client.get( |
| "/api/auth/google/callback?code=test_code&state=invalid_state" |
| ) |
|
|
| |
| assert response.status_code in [400, 401, 403, 404, 405] |
|
|
| @responses.activate |
| def test_google_drive_api_mock(self, client: TestClient, admin_token: str): |
| """Test Google Drive API with mocked response.""" |
| responses.add( |
| responses.GET, |
| "https://www.googleapis.com/drive/v3/files", |
| json={ |
| "files": [ |
| {"id": "1", "name": "file1.txt"}, |
| {"id": "2", "name": "file2.pdf"} |
| ] |
| }, |
| status=200 |
| ) |
|
|
| response = client.get( |
| "/api/integrations/google/files", |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
|
|
|
|
| class TestExternalServiceTimeout: |
| """Test timeout handling for external services.""" |
|
|
| @responses.activate |
| def test_slow_llm_response_timeout(self, client: TestClient, admin_token: str): |
| """Test timeout handling for slow LLM responses.""" |
| |
| import time |
|
|
| def request_callback(request): |
| time.sleep(0.1) |
| return (200, {}, json.dumps({"choices": [{"message": {"content": "Response"}}]})) |
|
|
| responses.add_callback( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| callback=request_callback, |
| content_type="application/json" |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 404, 405, 504] |
|
|
| @responses.activate |
| def test_external_service_unavailable(self, client: TestClient, admin_token: str): |
| """Test handling when external service is unavailable.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| body=Exception("Connection refused"), |
| status=503 |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [404, 405, 503, 504] |
|
|
| @responses.activate |
| def test_external_service_dns_failure(self, client: TestClient, admin_token: str): |
| """Test DNS failure handling.""" |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={"error": {"message": "DNS resolution failed"}}, |
| status=502 |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [404, 405, 502, 503] |
|
|
|
|
| class TestLLMProviderFailover: |
| """Test LLM provider failover logic.""" |
|
|
| @responses.activate |
| def test_failover_to_backup_provider(self, client: TestClient, admin_token: str): |
| """Test failover from primary to backup LLM provider.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={"error": {"message": "Service unavailable"}}, |
| status=503 |
| ) |
|
|
| |
| responses.add( |
| responses.POST, |
| "https://api.anthropic.com/v1/messages", |
| json={ |
| "id": "msg_backup", |
| "content": [{"type": "text", "text": "Backup response"}] |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/llm/chat", |
| json={ |
| "messages": [{"role": "user", "content": "Hello"}], |
| "enable_failover": True |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 404, 405, 503] |
|
|
| @responses.activate |
| def test_all_providers_fail(self, client: TestClient, admin_token: str): |
| """Test when all LLM providers fail.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={"error": {"message": "Service unavailable"}}, |
| status=503 |
| ) |
| responses.add( |
| responses.POST, |
| "https://api.anthropic.com/v1/messages", |
| json={"error": {"message": "Service unavailable"}}, |
| status=503 |
| ) |
|
|
| response = client.post( |
| "/api/llm/chat", |
| json={ |
| "messages": [{"role": "user", "content": "Hello"}], |
| "enable_failover": True |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [404, 405, 500, 503] |
|
|
| @responses.activate |
| def test_provider_selection_by_cost(self, client: TestClient, admin_token: str): |
| """Test provider selection based on query complexity/cost.""" |
| responses.add( |
| responses.POST, |
| "https://api.deepseek.com/v1/chat/completions", |
| json={ |
| "choices": [{"message": {"content": "Budget response"}}] |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/llm/chat", |
| json={ |
| "messages": [{"role": "user", "content": "Simple question"}], |
| "cost_optimization": True |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 404, 405] |
|
|
|
|
| class TestMultiServiceIntegration: |
| """Test integration across multiple external services.""" |
|
|
| @responses.activate |
| def test_slack_and_github_integration(self, client: TestClient, admin_token: str): |
| """Test coordinated Slack and GitHub integration.""" |
| |
| responses.add( |
| responses.POST, |
| "https://api.github.com/repos/test/repo/issues", |
| json={ |
| "id": 12345, |
| "number": 1, |
| "title": "Test Issue", |
| "state": "open" |
| }, |
| status=201 |
| ) |
|
|
| |
| responses.add( |
| responses.POST, |
| "https://slack.com/api/chat.postMessage", |
| json={ |
| "ok": True, |
| "channel": "C12345", |
| "ts": "1234567890.123456" |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/integrations/github-notify", |
| json={ |
| "repo": "test/repo", |
| "title": "Test Issue", |
| "notify_slack": True, |
| "channel": "C12345" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 201, 404, 405] |
|
|
| @responses.activate |
| def test_oauth_with_multiple_providers(self, client: TestClient): |
| """Test OAuth flow with multiple providers.""" |
| |
| responses.add( |
| responses.POST, |
| "https://oauth2.googleapis.com/token", |
| json={ |
| "access_token": "google_token", |
| "expires_in": 3600 |
| }, |
| status=200 |
| ) |
|
|
| |
| responses.add( |
| responses.POST, |
| "https://github.com/login/oauth/access_token", |
| json={ |
| "access_token": "github_token", |
| "token_type": "bearer" |
| }, |
| status=200 |
| ) |
|
|
| |
| google_response = client.get( |
| "/api/auth/google/callback?code=test_code&state=test_state" |
| ) |
| assert google_response.status_code in [200, 302, 404, 405] |
|
|
| |
| github_response = client.get( |
| "/api/auth/github/callback?code=test_code&state=test_state" |
| ) |
| assert github_response.status_code in [200, 302, 404, 405] |
|
|
|
|
| class TestExternalServiceSecurity: |
| """Test security aspects of external service integration.""" |
|
|
| @responses.activate |
| def test_api_key_not_exposed(self, client: TestClient, admin_token: str): |
| """Test that API keys are not exposed in responses.""" |
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={ |
| "choices": [{"message": {"content": "Response"}}] |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| if response.status_code == 200: |
| response_text = response.text.lower() |
| assert "sk-" not in response_text |
| assert "api_key" not in response_text or "hidden" in response_text |
|
|
| @responses.activate |
| def test_insecure_url_rejected(self, client: TestClient, admin_token: str): |
| """Test that non-HTTPS URLs are rejected.""" |
| response = client.post( |
| "/api/integrations/custom", |
| json={ |
| "url": "http://example.com/api", |
| "data": {"test": "data"} |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [400, 404, 405] |
|
|
| @responses.activate |
| def test_malicious_payload_sanitized(self, client: TestClient, admin_token: str): |
| """Test that malicious payloads are sanitized.""" |
| malicious_payload = { |
| "message": "<script>alert('xss')</script>", |
| "query": "'; DROP TABLE users; --" |
| } |
|
|
| responses.add( |
| responses.POST, |
| "https://api.openai.com/v1/chat/completions", |
| json={ |
| "choices": [{"message": {"content": "Safe response"}}] |
| }, |
| status=200 |
| ) |
|
|
| response = client.post( |
| "/api/llm/openai/chat", |
| json={ |
| "model": "gpt-4", |
| "messages": [{"role": "user", "content": malicious_payload["message"]}] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [200, 400, 404, 405] |
|
|