import json from unittest.mock import MagicMock, patch import pytest import litellm from litellm.integrations.bitbucket import BitBucketPromptManager from litellm.integrations.bitbucket.bitbucket_client import _sanitize_file_path @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_integration_with_litellm(mock_client_class): """Test BitBucket prompt integration with LiteLLM completion.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 temperature: 0.7 max_tokens: 150 --- System: You are a helpful assistant. User: {{user_message}}""" mock_client_class.return_value = mock_client # Configure BitBucket bitbucket_config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } # Set global BitBucket configuration litellm.set_global_bitbucket_config(bitbucket_config) # Test that the configuration was set assert litellm.global_bitbucket_config == bitbucket_config @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_initialization(mock_client_class): """Test BitBucketPromptManager initialization.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 temperature: 0.7 --- Hello {{name}}!""" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="test_prompt") # Should have loaded the prompt assert "test_prompt" in manager.prompt_manager.prompts template = manager.prompt_manager.prompts["test_prompt"] assert template.model == "gpt-4" assert template.temperature == 0.7 # Test rendering rendered = manager.prompt_manager.render_template("test_prompt", {"name": "World"}) assert rendered == "Hello World!" @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_error_handling(mock_client_class): """Test BitBucketPromptManager error handling.""" # Mock the BitBucket client to raise an error mock_client = MagicMock() mock_client.get_file_content.side_effect = Exception("BitBucket API error") mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="test_prompt") with pytest.raises( Exception, match="Failed to load prompt 'test_prompt' from BitBucket" ): _ = manager.prompt_manager def test_bitbucket_prompt_manager_config_validation(): """Test BitBucketPromptManager configuration validation.""" # Test missing required fields - validation happens when prompt_manager is accessed manager = BitBucketPromptManager({}) with pytest.raises( ValueError, match="workspace, repository, and access_token are required" ): _ = manager.prompt_manager manager = BitBucketPromptManager({"workspace": "test"}) with pytest.raises( ValueError, match="workspace, repository, and access_token are required" ): _ = manager.prompt_manager manager = BitBucketPromptManager({"repository": "test"}) with pytest.raises( ValueError, match="workspace, repository, and access_token are required" ): _ = manager.prompt_manager manager = BitBucketPromptManager({"access_token": "test"}) with pytest.raises( ValueError, match="workspace, repository, and access_token are required" ): _ = manager.prompt_manager @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_complex_prompt(mock_client_class): """Test BitBucketPromptManager with complex prompt structure.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 temperature: 0.3 max_tokens: 500 input: schema: user_question: string context?: string language: string --- System: You are a helpful {{language}} programming assistant. {% if context %}Context: {{context}} {% endif %}User: {{user_question}} Please provide a detailed response in {{language}}.""" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="complex_prompt") # Should have loaded the prompt assert "complex_prompt" in manager.prompt_manager.prompts template = manager.prompt_manager.prompts["complex_prompt"] assert template.model == "gpt-4" assert template.temperature == 0.3 assert template.max_tokens == 500 assert template.input_schema == { "user_question": "string", "context?": "string", "language": "string", } # Test rendering with all variables rendered = manager.prompt_manager.render_template( "complex_prompt", { "user_question": "How do I create a class?", "context": "Python programming", "language": "Python", }, ) assert "You are a helpful Python programming assistant." in rendered assert "Context: Python programming" in rendered assert "How do I create a class?" in rendered assert "Please provide a detailed response in Python." in rendered # Test rendering without optional context rendered_no_context = manager.prompt_manager.render_template( "complex_prompt", {"user_question": "What is inheritance?", "language": "Java"} ) assert "You are a helpful Java programming assistant." in rendered_no_context assert "Context:" not in rendered_no_context assert "What is inheritance?" in rendered_no_context @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_message_parsing(mock_client_class): """Test BitBucketPromptManager message parsing for different prompt formats.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 --- System: You are a helpful assistant. User: {{user_message}} Assistant: I'll help you with that.""" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="conversation_prompt") # Test message parsing messages = manager._parse_prompt_to_messages( "System: You are a helpful assistant.\n\nUser: Hello!\n\nAssistant: Hi there!" ) assert len(messages) == 3 assert messages[0]["role"] == "system" assert messages[0]["content"] == "You are a helpful assistant." assert messages[1]["role"] == "user" assert messages[1]["content"] == "Hello!" assert messages[2]["role"] == "assistant" assert messages[2]["content"] == "Hi there!" @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_pre_call_hook_integration(mock_client_class): """Test BitBucketPromptManager pre_call_hook integration.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = """--- model: gpt-4 temperature: 0.8 max_tokens: 200 --- System: You are a helpful assistant. User: {{user_message}}""" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="test_prompt") # Test pre_call_hook original_messages = [{"role": "user", "content": "This will be ignored"}] litellm_params = {"api_key": "test-key"} result_messages, result_params = manager.pre_call_hook( user_id="test_user", messages=original_messages, litellm_params=litellm_params, prompt_id="test_prompt", prompt_variables={"user_message": "What is AI?"}, ) # Should have parsed the prompt into messages assert len(result_messages) == 2 assert result_messages[0]["role"] == "system" assert result_messages[0]["content"] == "You are a helpful assistant." assert result_messages[1]["role"] == "user" assert result_messages[1]["content"] == "What is AI?" # Should have updated litellm_params assert result_params["model"] == "gpt-4" assert result_params["temperature"] == 0.8 assert result_params["max_tokens"] == 200 assert result_params["api_key"] == "test-key" # Original params preserved @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_post_call_hook(mock_client_class): """Test BitBucketPromptManager post_call_hook.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = "Simple prompt: {{message}}" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="test_prompt") # Mock response mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message = MagicMock() mock_response.choices[0].message.content = "Test response" # Test post_call_hook result = manager.post_call_hook( user_id="test_user", response=mock_response, input_messages=[{"role": "user", "content": "test"}], litellm_params={}, prompt_id="test_prompt", ) # Should return the response unchanged assert result == mock_response def test_bitbucket_prompt_manager_integration_name(): """Test BitBucketPromptManager integration name.""" config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config) assert manager.integration_name == "bitbucket" @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_get_template(mock_client_class): """Test BitBucketPromptManager get_template method.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = "Test content" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="test_prompt") # Test getting existing template template = manager.prompt_manager.get_template("test_prompt") assert template is not None assert template.template_id == "test_prompt" # Test getting non-existing template template = manager.prompt_manager.get_template("nonexistent") assert template is None @patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") def test_bitbucket_prompt_manager_list_templates(mock_client_class): """Test BitBucketPromptManager list_templates method.""" # Mock the BitBucket client mock_client = MagicMock() mock_client.get_file_content.return_value = "Test content" mock_client_class.return_value = mock_client config = { "workspace": "test-workspace", "repository": "test-repo", "access_token": "test-token", } manager = BitBucketPromptManager(config, prompt_id="test_prompt") # Test listing templates templates = manager.prompt_manager.list_templates() assert isinstance(templates, list) assert "test_prompt" in templates # --- Security: path traversal / SSRF --- def test_sanitize_file_path_rejects_traversal(): with pytest.raises(ValueError, match="path traversal"): _sanitize_file_path("../../etc/passwd") def test_sanitize_file_path_rejects_fragment(): with pytest.raises(ValueError, match="URL special characters"): _sanitize_file_path("secret#.prompt") def test_sanitize_file_path_rejects_query(): with pytest.raises(ValueError, match="URL special characters"): _sanitize_file_path("secret?.prompt") def test_sanitize_file_path_encodes_special_chars(): result = _sanitize_file_path("prompts/my prompt.prompt") assert result == "prompts/my%20prompt.prompt" def test_sanitize_file_path_allows_normal_paths(): assert _sanitize_file_path("prompts/my-prompt") == "prompts/my-prompt" assert _sanitize_file_path("simple") == "simple" def test_bitbucket_client_rejects_traversal_in_get_file_content(): from litellm.integrations.bitbucket.bitbucket_client import BitBucketClient client = BitBucketClient( { "workspace": "ws", "repository": "repo", "access_token": "tok", } ) with pytest.raises(ValueError, match="path traversal"): client.get_file_content("../../admin/credentials")