forked from HAL9000/cleveragents-core
374 lines
14 KiB
Python
374 lines
14 KiB
Python
"""Step definitions for project_cli_coverage_boost.feature.
|
|
|
|
Exercises uncovered error-handling branches in
|
|
``cleveragents.cli.commands.project`` using ``typer.testing.CliRunner``
|
|
with ``unittest.mock.patch`` to mock DI container helpers.
|
|
|
|
Targets lines: 81, 84, 89, 91-92, 565-566, 574-575, 643-645,
|
|
722, 725-726, 730-732, 772-774, 908-910, 914-916, 919-920.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands.project import app
|
|
from cleveragents.core.exceptions import DatabaseError, NotFoundError
|
|
|
|
# mix_stderr defaults to True in typer CliRunner, which merges stderr into
|
|
# result.output so we can assert on err_console output.
|
|
runner = CliRunner()
|
|
|
|
# Patch targets - these replace the module-level helper functions
|
|
_PATCH_PROJECT_REPO = "cleveragents.cli.commands.project._get_namespaced_project_repo"
|
|
_PATCH_LINK_REPO = "cleveragents.cli.commands.project._get_resource_link_repo"
|
|
_PATCH_REGISTRY_SVC = "cleveragents.cli.commands.project._get_resource_registry_service"
|
|
_PATCH_STORE_EXTRAS = "cleveragents.cli.commands.project._store_project_extras"
|
|
# For patching get_container inside the lazy-import helpers
|
|
_PATCH_CONTAINER = "cleveragents.application.container.get_container"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock factory helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_mock_project(
|
|
namespaced_name: str = "local/test-proj",
|
|
namespace: str = "local",
|
|
name: str = "test-proj",
|
|
description: str | None = "A test project",
|
|
linked_resources: list | None = None,
|
|
) -> MagicMock:
|
|
"""Create a mock project with the required attributes."""
|
|
proj = MagicMock()
|
|
proj.namespaced_name = namespaced_name
|
|
proj.namespace = namespace
|
|
proj.name = name
|
|
proj.description = description
|
|
proj.linked_resources = linked_resources or []
|
|
proj.created_at = datetime(2025, 1, 1, tzinfo=UTC)
|
|
proj.updated_at = datetime(2025, 1, 2, tzinfo=UTC)
|
|
return proj
|
|
|
|
|
|
def _make_mock_resource(
|
|
resource_id: str = "res-001", name: str = "local/some-res"
|
|
) -> MagicMock:
|
|
"""Create a mock resource with required attributes."""
|
|
res = MagicMock()
|
|
res.resource_id = resource_id
|
|
res.name = name
|
|
return res
|
|
|
|
|
|
def _make_mock_link(
|
|
link_id: str = "link-001", resource_id: str = "res-001"
|
|
) -> MagicMock:
|
|
"""Create a mock link with required attributes."""
|
|
link = MagicMock()
|
|
link.link_id = link_id
|
|
link.resource_id = resource_id
|
|
link.project_read_only = False
|
|
link.alias = None
|
|
link.linked_at = datetime(2025, 1, 3, tzinfo=UTC)
|
|
return link
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project CLI coverage mocks are prepared")
|
|
def step_prepare_coverage_mocks(context: Any) -> None:
|
|
"""Initialize mock holders on context for each scenario."""
|
|
context.cov_project_repo = MagicMock()
|
|
context.cov_link_repo = MagicMock()
|
|
context.cov_registry_svc = MagicMock()
|
|
context.cov_result = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper function coverage steps (lines 79-84, 89, 91-92)
|
|
#
|
|
# These call the actual _get_resource_link_repo / _get_resource_registry_service
|
|
# functions which do a lazy ``from cleveragents.application.container import
|
|
# get_container`` inside their body, so we patch get_container at its source.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I call the real _get_resource_link_repo helper")
|
|
def step_call_real_get_link_repo(context: Any) -> None:
|
|
"""Call _get_resource_link_repo with the container patched."""
|
|
mock_container = MagicMock()
|
|
mock_container.project_resource_link_repo.return_value = MagicMock()
|
|
with patch(_PATCH_CONTAINER, return_value=mock_container):
|
|
from cleveragents.cli.commands.project import _get_resource_link_repo
|
|
|
|
context.cov_helper_result = _get_resource_link_repo()
|
|
|
|
|
|
@then("the coverage link repo helper should return successfully")
|
|
def step_check_link_repo_result(context: Any) -> None:
|
|
assert context.cov_helper_result is not None
|
|
|
|
|
|
@when("I call the real _get_resource_registry_service helper")
|
|
def step_call_real_get_registry_service(context: Any) -> None:
|
|
"""Call _get_resource_registry_service with the container patched."""
|
|
mock_container = MagicMock()
|
|
mock_container.resource_registry_service.return_value = MagicMock()
|
|
with patch(_PATCH_CONTAINER, return_value=mock_container):
|
|
from cleveragents.cli.commands.project import _get_resource_registry_service
|
|
|
|
context.cov_helper_result = _get_resource_registry_service()
|
|
|
|
|
|
@then("the coverage registry service helper should return successfully")
|
|
def step_check_registry_service_result(context: Any) -> None:
|
|
assert context.cov_helper_result is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create command: resource linking raises NotFoundError (lines 565-566)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock is configured for successful create")
|
|
def step_project_repo_successful_create(context: Any) -> None:
|
|
"""Configure project repo so create succeeds and get returns a project."""
|
|
mock_proj = _make_mock_project()
|
|
context.cov_project_repo.create.return_value = None
|
|
context.cov_project_repo.get.return_value = mock_proj
|
|
|
|
|
|
@given("the link repo mock raises NotFoundError on create_link")
|
|
def step_link_repo_raises_not_found(context: Any) -> None:
|
|
context.cov_registry_svc.show_resource.side_effect = NotFoundError(
|
|
"Resource not found"
|
|
)
|
|
|
|
|
|
@when('I invoke the CLI create command with name "{name}" and resource "{res}"')
|
|
def step_invoke_create_with_resource(context: Any, name: str, res: str) -> None:
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo),
|
|
patch(_PATCH_LINK_REPO, return_value=context.cov_link_repo),
|
|
patch(_PATCH_REGISTRY_SVC, return_value=context.cov_registry_svc),
|
|
patch(_PATCH_STORE_EXTRAS),
|
|
):
|
|
context.cov_result = runner.invoke(app, ["create", name, "--resource", res])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create command: resource linking raises DatabaseError (lines 565-566)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the link repo mock raises DatabaseError on create_link")
|
|
def step_link_repo_raises_db_error_on_create_link(context: Any) -> None:
|
|
mock_res = _make_mock_resource()
|
|
context.cov_registry_svc.show_resource.return_value = mock_res
|
|
context.cov_link_repo.create_link.side_effect = DatabaseError("DB link failure")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create command: re-fetch raises generic Exception (lines 574-575)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock is configured for create but get raises Exception")
|
|
def step_project_repo_create_ok_get_fails(context: Any) -> None:
|
|
context.cov_project_repo.create.return_value = None
|
|
context.cov_project_repo.get.side_effect = Exception("re-fetch failed")
|
|
|
|
|
|
@when('I invoke the CLI create command with name "{name}" without resources')
|
|
def step_invoke_create_no_resources(context: Any, name: str) -> None:
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo),
|
|
patch(_PATCH_LINK_REPO, return_value=context.cov_link_repo),
|
|
patch(_PATCH_REGISTRY_SVC, return_value=context.cov_registry_svc),
|
|
patch(_PATCH_STORE_EXTRAS),
|
|
):
|
|
context.cov_result = runner.invoke(app, ["create", name])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# link-resource command: create_link raises DatabaseError (lines 643-645)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock returns a valid project for get")
|
|
def step_project_repo_get_returns_project(context: Any) -> None:
|
|
mock_proj = _make_mock_project()
|
|
context.cov_project_repo.get.return_value = mock_proj
|
|
|
|
|
|
@given("the registry mock returns a valid resource")
|
|
def step_registry_returns_valid_resource(context: Any) -> None:
|
|
mock_res = _make_mock_resource()
|
|
context.cov_registry_svc.show_resource.return_value = mock_res
|
|
|
|
|
|
@given("the link repo mock raises DatabaseError on create_link for link-resource")
|
|
def step_link_repo_raises_db_error_for_link_cmd(context: Any) -> None:
|
|
context.cov_link_repo.create_link.side_effect = DatabaseError(
|
|
"DB create_link failure"
|
|
)
|
|
|
|
|
|
@when('I invoke the CLI link-resource command for project "{proj}" resource "{res}"')
|
|
def step_invoke_link_resource_cmd(context: Any, proj: str, res: str) -> None:
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo),
|
|
patch(_PATCH_LINK_REPO, return_value=context.cov_link_repo),
|
|
patch(_PATCH_REGISTRY_SVC, return_value=context.cov_registry_svc),
|
|
):
|
|
context.cov_result = runner.invoke(app, ["link-resource", proj, res])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# unlink-resource command: user declines (lines 722, 725-726)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the link repo mock returns a matching link for unlink")
|
|
def step_link_repo_returns_matching_link(context: Any) -> None:
|
|
mock_link = _make_mock_link(resource_id="res-001")
|
|
context.cov_link_repo.list_links.return_value = [mock_link]
|
|
|
|
|
|
@when("I invoke the CLI unlink-resource command without yes and user declines")
|
|
def step_invoke_unlink_user_declines(context: Any) -> None:
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo),
|
|
patch(_PATCH_LINK_REPO, return_value=context.cov_link_repo),
|
|
patch(_PATCH_REGISTRY_SVC, return_value=context.cov_registry_svc),
|
|
):
|
|
context.cov_result = runner.invoke(
|
|
app,
|
|
["unlink-resource", "local/test-proj", "local/some-res"],
|
|
input="n\n",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# unlink-resource command: remove_link raises DatabaseError (lines 730-732)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the link repo mock raises DatabaseError on remove_link")
|
|
def step_link_repo_raises_db_error_on_remove(context: Any) -> None:
|
|
context.cov_link_repo.remove_link.side_effect = DatabaseError(
|
|
"DB remove_link failure"
|
|
)
|
|
|
|
|
|
@when("I invoke the CLI unlink-resource command with yes")
|
|
def step_invoke_unlink_with_yes(context: Any) -> None:
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo),
|
|
patch(_PATCH_LINK_REPO, return_value=context.cov_link_repo),
|
|
patch(_PATCH_REGISTRY_SVC, return_value=context.cov_registry_svc),
|
|
):
|
|
context.cov_result = runner.invoke(
|
|
app,
|
|
["unlink-resource", "local/test-proj", "local/some-res", "--yes"],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list command: list_projects raises DatabaseError (lines 772-774)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock raises DatabaseError on list_projects")
|
|
def step_project_repo_raises_db_error_on_list(context: Any) -> None:
|
|
context.cov_project_repo.list_projects.side_effect = DatabaseError(
|
|
"DB list failure"
|
|
)
|
|
|
|
|
|
@when("I invoke the CLI list command")
|
|
def step_invoke_list_cmd(context: Any) -> None:
|
|
with patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo):
|
|
context.cov_result = runner.invoke(app, ["list"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# delete command: user declines confirmation (lines 908-910)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock returns a project with no linked resources")
|
|
def step_project_repo_returns_no_linked(context: Any) -> None:
|
|
mock_proj = _make_mock_project(linked_resources=[])
|
|
context.cov_project_repo.get.return_value = mock_proj
|
|
|
|
|
|
@when("I invoke the CLI delete command without yes and user declines")
|
|
def step_invoke_delete_user_declines(context: Any) -> None:
|
|
with patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo):
|
|
context.cov_result = runner.invoke(
|
|
app,
|
|
["delete", "local/test-proj"],
|
|
input="n\n",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# delete command: repo.delete raises DatabaseError (lines 914-916)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock raises DatabaseError on delete")
|
|
def step_project_repo_raises_db_error_on_delete(context: Any) -> None:
|
|
context.cov_project_repo.delete.side_effect = DatabaseError("DB delete failure")
|
|
|
|
|
|
@when("I invoke the CLI delete command with yes")
|
|
def step_invoke_delete_with_yes(context: Any) -> None:
|
|
with patch(_PATCH_PROJECT_REPO, return_value=context.cov_project_repo):
|
|
context.cov_result = runner.invoke(
|
|
app,
|
|
["delete", "local/test-proj", "--yes"],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# delete command: repo.delete returns False (lines 919-920)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the project repo mock returns False on delete")
|
|
def step_project_repo_delete_returns_false(context: Any) -> None:
|
|
context.cov_project_repo.delete.return_value = False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Common then assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the coverage CLI result should contain "{text}"')
|
|
def step_cov_cli_result_contains(context: Any, text: str) -> None:
|
|
output = context.cov_result.output
|
|
assert text.lower() in output.lower(), (
|
|
f"Expected CLI output to contain '{text}', got:\n{output}"
|
|
)
|
|
|
|
|
|
@then("the coverage CLI exit code should be {code:d}")
|
|
def step_cov_cli_exit_code(context: Any, code: int) -> None:
|
|
actual = context.cov_result.exit_code
|
|
assert actual == code, (
|
|
f"Expected exit code {code}, got {actual}. Output:\n{context.cov_result.output}"
|
|
)
|