Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/tools/BitbucketSearchCodeTool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ def _resolve_config(


def _bb_available(sources: dict[str, dict]) -> bool:
return bool(sources.get("bitbucket", {}).get("connection_verified"))
bb = sources.get("bitbucket", {})
return bool(
bb.get("connection_verified")
and bb.get("workspace")
and bb.get("username")
and bb.get("app_password")
)


def _bb_creds(bb: dict[str, Any]) -> dict[str, Any]:
Expand Down
123 changes: 123 additions & 0 deletions tests/tools/test_bitbucket_commits_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Tests for BitbucketCommitsTool (function-based, @tool decorated)."""

from __future__ import annotations

from typing import Any, cast
from unittest.mock import patch

import pytest

from app.tools.BitbucketCommitsTool import list_bitbucket_commits
from tests.tools.conftest import BaseToolContract


def _registered_tool() -> Any:
return cast(Any, list_bitbucket_commits).__opensre_registered_tool__


class TestBitbucketCommitsToolContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool()


@pytest.mark.parametrize(
"sources,expected",
[
(
{
"bitbucket": {
"connection_verified": True,
"workspace": "acme",
"username": "bb-user",
"app_password": "bb-pass",
"repo_slug": "backend-service",
}
},
True,
),
({"bitbucket": {"connection_verified": True, "workspace": "acme"}}, False),
({"bitbucket": {"connection_verified": True, "username": "bb-user"}}, False),
({"bitbucket": {"connection_verified": True, "app_password": "bb-pass"}}, False),
(
{"bitbucket": {"workspace": "acme", "username": "bb-user", "app_password": "bb-pass"}},
False,
),
({}, False),
],
)
def test_is_available_requires_repo_and_credentials(sources: dict, expected: bool) -> None:
rt = _registered_tool()
assert rt.is_available(sources) is expected


def test_extract_params_maps_fields() -> None:
Comment on lines +23 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing is_available negative case for absent repo_slug

_list_bitbucket_commits_available requires both the four credentials and a repo_slug (or repo) key. The parametrized matrix has no case where all four credentials are fully supplied but repo_slug is omitted — so the per-field credential tests don't prove that the repo_slug guard works independently of the credential guards. Adding a case like {"bitbucket": {"connection_verified": True, "workspace": "acme", "username": "bb-user", "app_password": "bb-pass"}}False would complete the coverage.

rt = _registered_tool()
params = rt.extract_params(
{
"bitbucket": {
"repo_slug": "backend-service",
"path": "src/main.py",
"workspace": "acme",
"username": "bb-user",
"app_password": "bb-pass",
}
}
)

assert params["repo_slug"] == "backend-service"
assert params["path"] == "src/main.py"
assert params["workspace"] == "acme"
assert params["username"] == "bb-user"
assert params["app_password"] == "bb-pass"
assert params["limit"] == 20


def test_run_happy_path() -> None:
mock_result: dict[str, Any] = {
"source": "bitbucket",
"available": True,
"repo": "acme/backend-service",
"total_returned": 1,
"commits": [
{
"hash": "abc123def456",
"message": "Fix flaky test",
"author": "Jane Doe",
"date": "2026-04-28T10:00:00Z",
}
],
}

with patch(
"app.tools.BitbucketCommitsTool.list_commits", return_value=mock_result
) as mocked_list_commits:
result = list_bitbucket_commits(
repo_slug="backend-service",
workspace="acme",
username="bb-user",
app_password="bb-pass",
path="src/main.py",
limit=5,
)

assert result == mock_result
mocked_list_commits.assert_called_once()
config = mocked_list_commits.call_args.args[0]
assert config.workspace == "acme"
assert config.username == "bb-user"
assert config.app_password == "bb-pass"
assert mocked_list_commits.call_args.kwargs == {
"repo_slug": "backend-service",
"path": "src/main.py",
"limit": 5,
}


def test_run_returns_unavailable_without_credentials() -> None:
# Ensure env-based config doesn't make this test flaky
with patch("app.tools.BitbucketSearchCodeTool.bitbucket_config_from_env", return_value=None):
result = list_bitbucket_commits(repo_slug="backend-service")

assert result["available"] is False
assert result["commits"] == []
assert result["error"] == "Bitbucket integration is not configured."
Comment on lines +116 to +123
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This missing-credentials test assumes there is no Bitbucket env configuration. If BITBUCKET_WORKSPACE/credentials are set, _resolve_config() can return a config and the tool may call the real Bitbucket API (or return a different error payload). Patch app.tools.BitbucketSearchCodeTool.bitbucket_config_from_env to return None in this test to make it deterministic and side-effect free.

Copilot uses AI. Check for mistakes.
140 changes: 140 additions & 0 deletions tests/tools/test_bitbucket_file_contents_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Tests for BitbucketFileContentsTool (function-based, @tool decorated)."""

from __future__ import annotations

from typing import Any, cast
from unittest.mock import patch

import pytest

from app.tools.BitbucketFileContentsTool import get_bitbucket_file_contents
from tests.tools.conftest import BaseToolContract


def _registered_tool() -> Any:
return cast(Any, get_bitbucket_file_contents).__opensre_registered_tool__


class TestBitbucketFileContentsToolContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool()


@pytest.mark.parametrize(
"sources,expected",
[
(
{
"bitbucket": {
"connection_verified": True,
"workspace": "acme",
"username": "bb-user",
"app_password": "bb-pass",
"repo_slug": "backend-service",
"path": "src/main.py",
}
},
True,
),
(
{
"bitbucket": {
"connection_verified": True,
"workspace": "acme",
"username": "bb-user",
"app_password": "bb-pass",
}
},
False,
),
(
{
"bitbucket": {
"connection_verified": True,
"workspace": "acme",
"username": "bb-user",
"app_password": "bb-pass",
"path": "src/main.py",
}
},
False,
),
({"bitbucket": {"connection_verified": True, "workspace": "acme"}}, False),
({"bitbucket": {"connection_verified": True, "repo_slug": "backend-service"}}, False),
({"bitbucket": {"connection_verified": True, "path": "src/main.py"}}, False),
({}, False),
],
)
def test_is_available_requires_file_and_credentials(sources: dict, expected: bool) -> None:
rt = _registered_tool()
assert rt.is_available(sources) is expected
Comment on lines +23 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing is_available negative cases for credential-present-but-field-absent combinations

_get_bitbucket_file_contents_available requires credentials and both repo_slug and path. The current matrix only has single-field cases (workspace alone, repo_slug alone, path alone), so there's no case proving the function returns False when credentials are fully satisfied but path is absent, or credentials + path are present but repo_slug is absent. Adding those two cases would fully verify the compound guard.



def test_extract_params_maps_fields() -> None:
rt = _registered_tool()
params = rt.extract_params(
{
"bitbucket": {
"repo_slug": "backend-service",
"path": "src/main.py",
"ref": "main",
"workspace": "acme",
"username": "bb-user",
"app_password": "bb-pass",
}
}
)

assert params["repo_slug"] == "backend-service"
assert params["path"] == "src/main.py"
assert params["ref"] == "main"
assert params["workspace"] == "acme"
assert params["username"] == "bb-user"
assert params["app_password"] == "bb-pass"


def test_run_happy_path() -> None:
mock_result: dict[str, Any] = {
"source": "bitbucket",
"available": True,
"repo": "acme/backend-service",
"path": "src/main.py",
"ref": "main",
"content": "print('hello')",
"truncated": False,
}

with patch(
"app.tools.BitbucketFileContentsTool.get_file_contents",
return_value=mock_result,
) as mocked_get_file_contents:
result = get_bitbucket_file_contents(
repo_slug="backend-service",
path="src/main.py",
workspace="acme",
username="bb-user",
app_password="bb-pass",
ref="main",
)

assert result == mock_result
mocked_get_file_contents.assert_called_once()
config = mocked_get_file_contents.call_args.args[0]
assert config.workspace == "acme"
assert config.username == "bb-user"
assert config.app_password == "bb-pass"
assert mocked_get_file_contents.call_args.kwargs == {
"repo_slug": "backend-service",
"path": "src/main.py",
"ref": "main",
}


def test_run_returns_unavailable_without_credentials() -> None:
# Prevent loading real env config in CI/local runs
with patch("app.tools.BitbucketSearchCodeTool.bitbucket_config_from_env", return_value=None):
result = get_bitbucket_file_contents(repo_slug="backend-service", path="src/main.py")

assert result["available"] is False
assert result["file"] == {}
assert result["error"] == "Bitbucket integration is not configured."
Comment on lines +133 to +140
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This missing-credentials test can become flaky if BITBUCKET_* env vars are present, because _resolve_config() may load env config and the tool may call the real Bitbucket integration. Patch app.tools.BitbucketSearchCodeTool.bitbucket_config_from_env to return None here so the test always exercises the unavailable path without side effects.

Copilot uses AI. Check for mistakes.
Loading
Loading