fix(resource): align guard enforcement with service layer
ISSUES CLOSED: #6329
This commit is contained in:
@@ -50,18 +50,21 @@ Feature: Resource CLI coverage boost for remaining uncovered lines
|
||||
Then the CliRunner exit code should be non-zero
|
||||
And the CliRunner output should contain "Error:"
|
||||
|
||||
# ---- resource_remove edge_count > 0 (lines 653-658) ----
|
||||
# ---- resource_remove ValidationError bubble-up (service guard) ----
|
||||
|
||||
Scenario: resource_remove aborts when resource has edges
|
||||
Given a mock resource service whose session reports edges on the resource
|
||||
@resource_cli_guard
|
||||
Scenario: resource_remove aborts when service rejects removal with ValidationError
|
||||
Given a mock resource service whose remove_resource raises ValidationError for the resource
|
||||
When I invoke resource remove with --yes via CliRunner for the edged resource
|
||||
Then the CliRunner exit code should be non-zero
|
||||
And the CliRunner output should contain "edge(s) still reference it"
|
||||
|
||||
# ---- resource_remove generic Exception rollback (lines 668-672) ----
|
||||
# ---- resource_remove unexpected exception surfaced to CLI ----
|
||||
|
||||
Scenario: resource_remove rolls back session on unexpected exception
|
||||
Given a mock resource service whose session delete raises a generic exception for resource
|
||||
@resource_cli_guard
|
||||
Scenario: resource_remove aborts when service remove_resource raises an unexpected exception
|
||||
Given a mock resource service whose remove_resource raises a generic exception for the resource
|
||||
When I invoke resource remove with --yes via CliRunner for the failing resource
|
||||
Then the CliRunner exit code should be non-zero
|
||||
And the mock session rollback should have been called for resource remove
|
||||
And the CliRunner output should contain "Unexpected error:"
|
||||
And the mock service should have attempted to remove the resource
|
||||
|
||||
@@ -15,7 +15,6 @@ Covers the remaining uncovered lines and partial branches in
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when # type: ignore[import-untyped]
|
||||
@@ -72,20 +71,6 @@ def _make_mock_resource() -> MagicMock:
|
||||
return res
|
||||
|
||||
|
||||
def _smart_query_side_effect(model_map: dict[str, MagicMock]) -> Any:
|
||||
"""Return a side_effect for session.query() that dispatches by model class name."""
|
||||
|
||||
def _side_effect(model_cls: Any) -> MagicMock:
|
||||
name = getattr(model_cls, "__name__", None) or str(model_cls)
|
||||
if name in model_map:
|
||||
return model_map[name]
|
||||
# Fallback: return a generic mock chain
|
||||
m = MagicMock()
|
||||
return m
|
||||
|
||||
return _side_effect
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: _get_registry_service delegates to the DI container
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -279,28 +264,29 @@ def step_invoke_type_remove_validation_error(context: Context) -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: resource_remove aborts when resource has edges
|
||||
# Scenario: resource_remove aborts when service rejects removal with ValidationError
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a mock resource service whose session reports edges on the resource")
|
||||
@given(
|
||||
"a mock resource service whose remove_resource raises ValidationError for the resource"
|
||||
)
|
||||
def step_mock_service_resource_edges(context: Context) -> None:
|
||||
svc = MagicMock()
|
||||
mock_res = _make_mock_resource()
|
||||
svc.show_resource.return_value = mock_res
|
||||
context.rcb_mock_resource = mock_res
|
||||
|
||||
mock_session = MagicMock()
|
||||
|
||||
# ResourceEdgeModel query → count returns 3 (edges exist)
|
||||
edge_query = MagicMock()
|
||||
edge_query.filter.return_value.count.return_value = 3
|
||||
|
||||
mock_session.query.side_effect = _smart_query_side_effect(
|
||||
{
|
||||
"ResourceEdgeModel": edge_query,
|
||||
}
|
||||
svc.remove_resource.side_effect = ValidationError(
|
||||
message=(
|
||||
"Cannot remove resource 'local/mock-res': 3 edge(s) still reference it."
|
||||
),
|
||||
details={
|
||||
"resource_id": mock_res.resource_id,
|
||||
"link_count": 0,
|
||||
"edge_count": 3,
|
||||
},
|
||||
)
|
||||
svc._session.return_value = mock_session
|
||||
|
||||
context.rcb_mock_service = svc
|
||||
|
||||
@@ -321,42 +307,22 @@ def step_invoke_resource_remove_edges(context: Context) -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario: resource_remove rolls back session on unexpected exception
|
||||
# Scenario: resource_remove aborts when service remove_resource raises an unexpected exception
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(
|
||||
"a mock resource service whose session delete raises a generic exception for resource"
|
||||
"a mock resource service whose remove_resource raises a generic exception for the resource"
|
||||
)
|
||||
def step_mock_service_resource_delete_exception(context: Context) -> None:
|
||||
svc = MagicMock()
|
||||
mock_res = _make_mock_resource()
|
||||
svc.show_resource.return_value = mock_res
|
||||
context.rcb_mock_resource = mock_res
|
||||
|
||||
mock_session = MagicMock()
|
||||
|
||||
# ResourceEdgeModel query → count returns 0 (no edges)
|
||||
edge_query = MagicMock()
|
||||
edge_query.filter.return_value.count.return_value = 0
|
||||
|
||||
# ResourceModel query → first returns a mock row
|
||||
mock_row = MagicMock()
|
||||
resource_query = MagicMock()
|
||||
resource_query.filter_by.return_value.first.return_value = mock_row
|
||||
|
||||
mock_session.query.side_effect = _smart_query_side_effect(
|
||||
{
|
||||
"ResourceEdgeModel": edge_query,
|
||||
"ResourceModel": resource_query,
|
||||
}
|
||||
)
|
||||
# session.delete raises a generic exception
|
||||
mock_session.delete.side_effect = RuntimeError("unexpected DB write failure")
|
||||
|
||||
svc._session.return_value = mock_session
|
||||
svc.remove_resource.side_effect = RuntimeError("unexpected DB write failure")
|
||||
|
||||
context.rcb_mock_service = svc
|
||||
context.rcb_mock_session = mock_session
|
||||
|
||||
|
||||
@when("I invoke resource remove with --yes via CliRunner for the failing resource")
|
||||
@@ -374,9 +340,10 @@ def step_invoke_resource_remove_exception(context: Context) -> None:
|
||||
)
|
||||
|
||||
|
||||
@then("the mock session rollback should have been called for resource remove")
|
||||
def step_verify_resource_rollback(context: Context) -> None:
|
||||
context.rcb_mock_session.rollback.assert_called()
|
||||
@then("the mock service should have attempted to remove the resource")
|
||||
def step_verify_service_remove_called(context: Context) -> None:
|
||||
expected_id = getattr(context.rcb_mock_resource, "resource_id", None)
|
||||
context.rcb_mock_service.remove_resource.assert_called_once_with(expected_id)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -53,6 +53,7 @@ Based on ``implementation_plan.md`` -- Tasks B0.cli.resources, B1.cli.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -713,11 +714,8 @@ def resource_add(
|
||||
|
||||
# --update: remove existing resource before re-registering
|
||||
if update:
|
||||
try:
|
||||
with contextlib.suppress(NotFoundError):
|
||||
service.remove_resource(name, force=True)
|
||||
except NotFoundError:
|
||||
# Resource doesn't exist yet — proceed with normal add
|
||||
pass
|
||||
|
||||
# Build properties from type-specific flags
|
||||
properties: dict[str, Any] = {}
|
||||
|
||||
Reference in New Issue
Block a user