Skip to main content
Glama
test_bitbucket_client.py14.1 kB
"""Tests for src/bitbucket_client.py.""" import pytest import respx from httpx import Response from src.bitbucket_client import BitbucketClient, BitbucketError @pytest.fixture def client(): """Create a BitbucketClient with test credentials.""" # Pass credentials directly to avoid .env interference return BitbucketClient( workspace="test-workspace", email="test@example.com", api_token="test-token", ) class TestListRepositories: """Tests for list_repositories method.""" @respx.mock def test_returns_list_of_repos(self, client): """Should return list of repository dicts.""" respx.get("https://api.bitbucket.org/2.0/repositories/test-workspace").mock( return_value=Response(200, json={ "values": [ {"name": "repo1", "full_name": "test-workspace/repo1"}, {"name": "repo2", "full_name": "test-workspace/repo2"}, ], "pagelen": 50, }) ) result = client.list_repositories(limit=50) assert len(result) == 2 assert result[0]["name"] == "repo1" assert result[1]["name"] == "repo2" @respx.mock def test_returns_empty_list_on_no_results(self, client): """Should return empty list when no repos found.""" respx.get("https://api.bitbucket.org/2.0/repositories/test-workspace").mock( return_value=Response(200, json={"values": [], "pagelen": 50}) ) result = client.list_repositories() assert result == [] @respx.mock def test_handles_404(self, client): """Should return empty list on 404.""" respx.get("https://api.bitbucket.org/2.0/repositories/test-workspace").mock( return_value=Response(404) ) result = client.list_repositories() assert result == [] class TestListBranches: """Tests for list_branches method.""" @respx.mock def test_returns_list_of_branches(self, client): """Should return list of branch dicts.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/refs/branches" ).mock( return_value=Response(200, json={ "values": [ {"name": "main", "target": {"hash": "abc123"}}, {"name": "develop", "target": {"hash": "def456"}}, ], }) ) result = client.list_branches("test-repo") assert len(result) == 2 assert result[0]["name"] == "main" class TestListPipelines: """Tests for list_pipelines method.""" @respx.mock def test_returns_sorted_pipelines(self, client): """Should return pipelines sorted by created_on.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pipelines/" ).mock( return_value=Response(200, json={ "values": [ {"uuid": "{uuid1}", "build_number": 2}, {"uuid": "{uuid2}", "build_number": 1}, ], }) ) result = client.list_pipelines("test-repo", limit=10) assert len(result) == 2 assert result[0]["build_number"] == 2 class TestGetPipeline: """Tests for get_pipeline method - verifies UUID normalization.""" @respx.mock def test_normalizes_uuid_without_braces(self, client): """Should add braces to UUID if missing.""" route = respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pipelines/{12345678-1234-1234-1234-123456789012}" ).mock( return_value=Response(200, json={"uuid": "{12345678-1234-1234-1234-123456789012}"}) ) # Call with UUID without braces result = client.get_pipeline("test-repo", "12345678-1234-1234-1234-123456789012") assert route.called assert result["uuid"] == "{12345678-1234-1234-1234-123456789012}" @respx.mock def test_preserves_uuid_with_braces(self, client): """Should preserve braces if already present.""" route = respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pipelines/{12345678-1234-1234-1234-123456789012}" ).mock( return_value=Response(200, json={"uuid": "{12345678-1234-1234-1234-123456789012}"}) ) # Call with UUID with braces result = client.get_pipeline("test-repo", "{12345678-1234-1234-1234-123456789012}") assert route.called assert result is not None class TestGetWebhook: """Tests for get_webhook method - verifies UUID normalization.""" @respx.mock def test_normalizes_webhook_uid(self, client): """Should add braces to webhook UID if missing.""" route = respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/hooks/{webhook-uid}" ).mock( return_value=Response(200, json={"uuid": "{webhook-uid}", "url": "https://example.com"}) ) result = client.get_webhook("test-repo", "webhook-uid") assert route.called assert result["uuid"] == "{webhook-uid}" class TestRequestText: """Tests for _request_text helper method.""" @respx.mock def test_returns_text_on_success(self, client): """Should return text content on 200 response.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/src/main/README.md" ).mock( return_value=Response(200, text="# Hello World\n\nThis is a README.") ) result = client._request_text("repositories/test-workspace/test-repo/src/main/README.md") assert result == "# Hello World\n\nThis is a README." @respx.mock def test_returns_none_on_404(self, client): """Should return None on 404 response.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/src/main/missing.txt" ).mock( return_value=Response(404) ) result = client._request_text("repositories/test-workspace/test-repo/src/main/missing.txt") assert result is None @respx.mock def test_raises_on_error(self, client): """Should raise BitbucketError on error responses.""" from src.bitbucket_client import BitbucketError respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/src/main/file.txt" ).mock( return_value=Response(500) ) with pytest.raises(BitbucketError, match="Request failed: 500"): client._request_text("repositories/test-workspace/test-repo/src/main/file.txt") class TestGetFileContent: """Tests for get_file_content method.""" @respx.mock def test_returns_file_content(self, client): """Should return file content as string.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/src/main/src/main.py" ).mock( return_value=Response(200, text="print('hello')") ) result = client.get_file_content("test-repo", "src/main.py") assert result == "print('hello')" @respx.mock def test_returns_none_when_not_found(self, client): """Should return None for non-existent files.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/src/main/missing.py" ).mock( return_value=Response(404) ) result = client.get_file_content("test-repo", "missing.py") assert result is None class TestGetPrDiff: """Tests for get_pr_diff method.""" @respx.mock def test_returns_diff_content(self, client): """Should return diff as string.""" diff_content = "diff --git a/file.py b/file.py\n+new line" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pullrequests/123/diff" ).mock( return_value=Response(200, text=diff_content) ) result = client.get_pr_diff("test-repo", 123) assert result == diff_content @respx.mock def test_returns_empty_on_404(self, client): """Should return empty string on 404.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pullrequests/999/diff" ).mock( return_value=Response(404) ) result = client.get_pr_diff("test-repo", 999) assert result == "" class TestGetPipelineLogs: """Tests for get_pipeline_logs method.""" @respx.mock def test_returns_logs(self, client): """Should return log content as string.""" log_content = "Step 1: Building...\nStep 2: Testing...\nDone!" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pipelines/" "{pipeline-uuid}/steps/{step-uuid}/log" ).mock( return_value=Response(200, text=log_content) ) result = client.get_pipeline_logs("test-repo", "pipeline-uuid", "step-uuid") assert result == log_content @respx.mock def test_returns_empty_on_404(self, client): """Should return empty string on 404.""" respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/pipelines/" "{missing-uuid}/steps/{step-uuid}/log" ).mock( return_value=Response(404) ) result = client.get_pipeline_logs("test-repo", "missing-uuid", "step-uuid") assert result == "" class TestRequireResult: """Tests for _require_result helper method.""" def test_returns_result_when_valid(self, client): """Should return the result when it's valid.""" result = {"id": 1, "name": "test"} returned = client._require_result(result, "create something", "test-id") assert returned == result def test_raises_on_none(self, client): """Should raise BitbucketError when result is None.""" from src.bitbucket_client import BitbucketError with pytest.raises(BitbucketError, match="Failed to create something: test-id"): client._require_result(None, "create something", "test-id") def test_raises_on_empty_dict(self, client): """Should raise BitbucketError when result is empty dict.""" from src.bitbucket_client import BitbucketError with pytest.raises(BitbucketError, match="Failed to do action"): client._require_result({}, "do action") def test_error_message_without_identifier(self, client): """Should format error message correctly without identifier.""" from src.bitbucket_client import BitbucketError with pytest.raises(BitbucketError) as exc_info: client._require_result(None, "create webhook") assert str(exc_info.value) == "Failed to create webhook" assert ": " not in str(exc_info.value) # No colon when no identifier class TestRepoPath: """Tests for _repo_path helper method.""" def test_base_path(self, client): """Should return base repository path.""" path = client._repo_path("my-repo") assert path == "repositories/test-workspace/my-repo" def test_with_single_part(self, client): """Should append single path part.""" path = client._repo_path("my-repo", "pullrequests") assert path == "repositories/test-workspace/my-repo/pullrequests" def test_with_multiple_parts(self, client): """Should append multiple path parts.""" path = client._repo_path("my-repo", "pullrequests", "123", "merge") assert path == "repositories/test-workspace/my-repo/pullrequests/123/merge" def test_with_pipelines_path(self, client): """Should work with pipeline paths.""" path = client._repo_path("my-repo", "pipelines", "{uuid}", "steps", "{step-uuid}", "log") assert path == "repositories/test-workspace/my-repo/pipelines/{uuid}/steps/{step-uuid}/log" class TestRateLimiting: """Tests for rate limiting and retry behavior.""" @respx.mock def test_retries_on_429(self, client, mocker): """Should retry on 429 with exponential backoff.""" # Mock time.sleep to avoid actual delays mocker.patch("src.bitbucket_client.time.sleep") # First two calls return 429, third returns 200 route = respx.get("https://api.bitbucket.org/2.0/repositories/test-workspace").mock( side_effect=[ Response(429, headers={"Retry-After": "1"}), Response(429), Response(200, json={"values": [{"name": "repo1"}]}), ] ) result = client.list_repositories() assert len(result) == 1 assert route.call_count == 3 @respx.mock def test_raises_after_max_retries(self, client, mocker): """Should raise BitbucketError after max retries exhausted.""" mocker.patch("src.bitbucket_client.time.sleep") # All calls return 429 respx.get("https://api.bitbucket.org/2.0/repositories/test-workspace").mock( return_value=Response(429) ) with pytest.raises(BitbucketError, match="Rate limited after"): client.list_repositories() @respx.mock def test_request_text_retries_on_429(self, client, mocker): """Should retry text requests on 429.""" mocker.patch("src.bitbucket_client.time.sleep") route = respx.get( "https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo/src/main/README.md" ).mock( side_effect=[ Response(429), Response(200, text="# README"), ] ) result = client._request_text("repositories/test-workspace/test-repo/src/main/README.md") assert result == "# README" assert route.call_count == 2

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JaviMaligno/mcp-server-bitbucket'

If you have feedback or need assistance with the MCP directory API, please join our Discord server