feat(resource): add cloud infrastructure resources
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 29s
CI / typecheck (pull_request) Successful in 59s
CI / security (pull_request) Successful in 59s
CI / unit_tests (pull_request) Successful in 3m9s
CI / integration_tests (pull_request) Successful in 3m34s
CI / e2e_tests (pull_request) Successful in 3m54s
CI / docker (pull_request) Successful in 55s
CI / coverage (pull_request) Successful in 6m1s
CI / build (push) Successful in 15s
CI / lint (push) Successful in 16s
CI / quality (push) Successful in 27s
CI / typecheck (push) Successful in 38s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 48s
CI / unit_tests (push) Successful in 3m14s
CI / integration_tests (push) Successful in 3m30s
CI / docker (push) Successful in 56s
CI / e2e_tests (push) Successful in 4m43s
CI / coverage (push) Successful in 6m1s
CI / benchmark-publish (push) Successful in 19m50s
CI / benchmark-regression (pull_request) Successful in 37m17s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 29s
CI / typecheck (pull_request) Successful in 59s
CI / security (pull_request) Successful in 59s
CI / unit_tests (pull_request) Successful in 3m9s
CI / integration_tests (pull_request) Successful in 3m34s
CI / e2e_tests (pull_request) Successful in 3m54s
CI / docker (pull_request) Successful in 55s
CI / coverage (pull_request) Successful in 6m1s
CI / build (push) Successful in 15s
CI / lint (push) Successful in 16s
CI / quality (push) Successful in 27s
CI / typecheck (push) Successful in 38s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 48s
CI / unit_tests (push) Successful in 3m14s
CI / integration_tests (push) Successful in 3m30s
CI / docker (push) Successful in 56s
CI / e2e_tests (push) Successful in 4m43s
CI / coverage (push) Successful in 6m1s
CI / benchmark-publish (push) Successful in 19m50s
CI / benchmark-regression (pull_request) Successful in 37m17s
Implement cloud resource types (aws, gcp, azure) with credential fields, region/tenant metadata, and stubbed sandbox strategies. Credential resolution uses environment variables and profile names with no secrets logged. Key changes: - Add CloudResourceHandler with aws/gcp/azure type definitions - Add credential resolution from env vars and profile names - Add stubbed sandbox strategies (validate config, raise NotImplementedError) - Register cloud types in bootstrap_builtin_types - Credential masking via existing redaction patterns - Add Behave BDD tests, Robot integration tests, ASV benchmarks ISSUES CLOSED: #343
This commit was merged in pull request #669.
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
"""Step definitions for cloud_resources.feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from behave import given, then, when # type: ignore[attr-defined]
|
||||
|
||||
from cleveragents.application.services._resource_registry_data import (
|
||||
BUILTIN_TYPES as _BUILTIN_TYPES,
|
||||
)
|
||||
from cleveragents.domain.models.core.resource import (
|
||||
PhysVirt,
|
||||
Resource,
|
||||
ResourceCapabilities,
|
||||
)
|
||||
from cleveragents.resource.handlers.cloud import (
|
||||
CloudResourceHandler,
|
||||
CloudSandboxStrategy,
|
||||
resolve_credentials,
|
||||
validate_credentials,
|
||||
)
|
||||
from cleveragents.resource.handlers.protocol import ResourceHandler
|
||||
|
||||
# ── Environment variable management ─────────────────────────
|
||||
|
||||
_CLOUD_ENV_VARS = [
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN",
|
||||
"AWS_REGION",
|
||||
"AWS_PROFILE",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"GCLOUD_PROJECT",
|
||||
"GCP_REGION",
|
||||
"AZURE_SUBSCRIPTION_ID",
|
||||
"AZURE_TENANT_ID",
|
||||
"AZURE_CLIENT_ID",
|
||||
"AZURE_CLIENT_SECRET",
|
||||
"AZURE_REGION",
|
||||
]
|
||||
|
||||
|
||||
def _clear_cloud_env() -> dict[str, str]:
|
||||
"""Remove all cloud env vars and return saved values."""
|
||||
saved: dict[str, str] = {}
|
||||
for var in _CLOUD_ENV_VARS:
|
||||
val = os.environ.pop(var, None)
|
||||
if val is not None:
|
||||
saved[var] = val
|
||||
return saved
|
||||
|
||||
|
||||
def _restore_cloud_env(saved: dict[str, str]) -> None:
|
||||
"""Restore previously saved env vars."""
|
||||
for var in _CLOUD_ENV_VARS:
|
||||
if var in saved:
|
||||
os.environ[var] = saved[var]
|
||||
else:
|
||||
os.environ.pop(var, None)
|
||||
|
||||
|
||||
def _make_resource( # type: ignore[attr-defined]
|
||||
provider: str,
|
||||
properties: dict[str, Any] | None = None,
|
||||
) -> Resource:
|
||||
"""Create a test cloud resource."""
|
||||
return Resource(
|
||||
resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS",
|
||||
resource_type_name=provider,
|
||||
classification=PhysVirt.PHYSICAL,
|
||||
location=None,
|
||||
properties=properties or {},
|
||||
capabilities=ResourceCapabilities(
|
||||
readable=True,
|
||||
writable=True,
|
||||
sandboxable=False,
|
||||
checkpointable=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── Given steps ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@given('a cloud resource type definition for "{provider}"')
|
||||
def step_cloud_type_def(context: Any, provider: str) -> None:
|
||||
"""Load the built-in cloud type definition."""
|
||||
for builtin in _BUILTIN_TYPES:
|
||||
if builtin["name"] == provider:
|
||||
context.cloud_type_def = builtin # type: ignore[attr-defined]
|
||||
return
|
||||
raise AssertionError(f"Built-in type '{provider}' not found in _BUILTIN_TYPES")
|
||||
|
||||
|
||||
@given("AWS environment variables are set")
|
||||
def step_aws_env_set(context: Any) -> None:
|
||||
"""Set AWS environment variables for testing."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
|
||||
os.environ["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
os.environ["AWS_REGION"] = "us-east-1"
|
||||
|
||||
|
||||
@given("GCP environment variables are set")
|
||||
def step_gcp_env_set(context: Any) -> None:
|
||||
"""Set GCP environment variables for testing."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/sa-key.json"
|
||||
os.environ["GCLOUD_PROJECT"] = "my-gcp-project"
|
||||
|
||||
|
||||
@given("Azure environment variables are set")
|
||||
def step_azure_env_set(context: Any) -> None:
|
||||
"""Set Azure environment variables for testing."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["AZURE_SUBSCRIPTION_ID"] = "sub-12345"
|
||||
os.environ["AZURE_TENANT_ID"] = "tenant-67890"
|
||||
os.environ["AZURE_CLIENT_ID"] = "client-abcde"
|
||||
os.environ["AZURE_CLIENT_SECRET"] = "secret-fghij"
|
||||
|
||||
|
||||
@given("no cloud environment variables are set")
|
||||
def step_no_cloud_env(context: Any) -> None:
|
||||
"""Clear all cloud environment variables."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given('AWS profile environment variable is set to "{profile}"')
|
||||
def step_aws_profile_set(context: Any, profile: str) -> None:
|
||||
"""Set only the AWS_PROFILE env var."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["AWS_PROFILE"] = profile
|
||||
|
||||
|
||||
@given("a valid AWS cloud resource")
|
||||
def step_valid_aws_resource(context: Any) -> None:
|
||||
"""Create an AWS resource with credentials from env vars."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
|
||||
os.environ["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
context.cloud_resource = _make_resource("aws") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("a valid GCP cloud resource")
|
||||
def step_valid_gcp_resource(context: Any) -> None:
|
||||
"""Create a GCP resource with credentials from env vars."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/sa-key.json"
|
||||
os.environ["GCLOUD_PROJECT"] = "my-gcp-project"
|
||||
context.cloud_resource = _make_resource("gcp") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("a valid Azure cloud resource")
|
||||
def step_valid_azure_resource(context: Any) -> None:
|
||||
"""Create an Azure resource with credentials from env vars."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
os.environ["AZURE_SUBSCRIPTION_ID"] = "sub-12345"
|
||||
os.environ["AZURE_TENANT_ID"] = "tenant-67890"
|
||||
os.environ["AZURE_CLIENT_ID"] = "client-abcde"
|
||||
os.environ["AZURE_CLIENT_SECRET"] = "secret-fghij"
|
||||
context.cloud_resource = _make_resource("azure") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("an AWS cloud resource without credentials")
|
||||
def step_aws_no_creds(context: Any) -> None:
|
||||
"""Create an AWS resource with no credentials."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
context.cloud_resource = _make_resource("aws") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("a GCP cloud resource without credentials")
|
||||
def step_gcp_no_creds(context: Any) -> None:
|
||||
"""Create a GCP resource with no credentials."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
context.cloud_resource = _make_resource("gcp") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("an Azure cloud resource without credentials")
|
||||
def step_azure_no_creds(context: Any) -> None:
|
||||
"""Create an Azure resource with no credentials."""
|
||||
context.saved_env = _clear_cloud_env() # type: ignore[attr-defined]
|
||||
context.cloud_resource = _make_resource("azure") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given('a cloud sandbox strategy for "{provider}"')
|
||||
def step_cloud_sandbox_strategy(context: Any, provider: str) -> None:
|
||||
"""Create a cloud sandbox strategy instance."""
|
||||
context.cloud_sandbox = CloudSandboxStrategy(provider) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("a CloudResourceHandler instance")
|
||||
def step_cloud_handler_instance(context: Any) -> None:
|
||||
"""Create a CloudResourceHandler instance."""
|
||||
context.cloud_handler = CloudResourceHandler() # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# ── When steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@when('I resolve credentials for provider "{provider}"')
|
||||
def step_resolve_creds(context: Any, provider: str) -> None:
|
||||
"""Resolve credentials for the given provider."""
|
||||
try:
|
||||
context.resolved_creds = resolve_credentials(provider, {}) # type: ignore[attr-defined]
|
||||
context.resolve_error = None # type: ignore[attr-defined]
|
||||
except (ValueError, KeyError) as exc:
|
||||
context.resolved_creds = {} # type: ignore[attr-defined]
|
||||
context.resolve_error = exc # type: ignore[attr-defined]
|
||||
finally:
|
||||
saved = getattr(context, "saved_env", {})
|
||||
_restore_cloud_env(saved)
|
||||
|
||||
|
||||
@when('I validate credentials for provider "{provider}"')
|
||||
def step_validate_creds(context: Any, provider: str) -> None:
|
||||
"""Validate resolved credentials."""
|
||||
resolved: dict[str, str | None] = getattr(context, "resolved_creds", {})
|
||||
context.validation_errors = validate_credentials(provider, resolved) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call resolve on the cloud handler")
|
||||
def step_call_resolve(context: Any) -> None:
|
||||
"""Call resolve on CloudResourceHandler."""
|
||||
handler = CloudResourceHandler()
|
||||
resource: Resource = context.cloud_resource # type: ignore[attr-defined]
|
||||
mock_manager = MagicMock()
|
||||
try:
|
||||
handler.resolve(
|
||||
resource=resource,
|
||||
plan_id="PLAN_TEST",
|
||||
slot_name="cloud-slot",
|
||||
sandbox_manager=mock_manager,
|
||||
)
|
||||
context.handler_error = None # type: ignore[attr-defined]
|
||||
context.handler_error_type = None # type: ignore[attr-defined]
|
||||
except NotImplementedError as exc:
|
||||
context.handler_error = exc # type: ignore[attr-defined]
|
||||
context.handler_error_type = "NotImplementedError" # type: ignore[attr-defined]
|
||||
except ValueError as exc:
|
||||
context.handler_error = exc # type: ignore[attr-defined]
|
||||
context.handler_error_type = "ValueError" # type: ignore[attr-defined]
|
||||
finally:
|
||||
saved = getattr(context, "saved_env", {})
|
||||
_restore_cloud_env(saved)
|
||||
|
||||
|
||||
@when('I try to resolve credentials for provider "{provider}"')
|
||||
def step_try_resolve_unknown(context: Any, provider: str) -> None:
|
||||
"""Try to resolve credentials for an unknown provider."""
|
||||
try:
|
||||
resolve_credentials(provider, {})
|
||||
context.handler_error = None # type: ignore[attr-defined]
|
||||
context.handler_error_type = None # type: ignore[attr-defined]
|
||||
except ValueError as exc:
|
||||
context.handler_error = exc # type: ignore[attr-defined]
|
||||
context.handler_error_type = "ValueError" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call create on the sandbox strategy")
|
||||
def step_sandbox_create(context: Any) -> None:
|
||||
"""Call create on the cloud sandbox strategy."""
|
||||
strategy: CloudSandboxStrategy = context.cloud_sandbox # type: ignore[attr-defined]
|
||||
try:
|
||||
strategy.create("res-test", "plan-test")
|
||||
context.handler_error = None # type: ignore[attr-defined]
|
||||
context.handler_error_type = None # type: ignore[attr-defined]
|
||||
except NotImplementedError as exc:
|
||||
context.handler_error = exc # type: ignore[attr-defined]
|
||||
context.handler_error_type = "NotImplementedError" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call commit on the sandbox strategy")
|
||||
def step_sandbox_commit(context: Any) -> None:
|
||||
"""Call commit on the cloud sandbox strategy."""
|
||||
strategy: CloudSandboxStrategy = context.cloud_sandbox # type: ignore[attr-defined]
|
||||
try:
|
||||
strategy.commit("res-test", "plan-test")
|
||||
context.handler_error = None # type: ignore[attr-defined]
|
||||
context.handler_error_type = None # type: ignore[attr-defined]
|
||||
except NotImplementedError as exc:
|
||||
context.handler_error = exc # type: ignore[attr-defined]
|
||||
context.handler_error_type = "NotImplementedError" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call rollback on the sandbox strategy")
|
||||
def step_sandbox_rollback(context: Any) -> None:
|
||||
"""Call rollback on the cloud sandbox strategy."""
|
||||
strategy: CloudSandboxStrategy = context.cloud_sandbox # type: ignore[attr-defined]
|
||||
try:
|
||||
strategy.rollback("res-test", "plan-test")
|
||||
context.handler_error = None # type: ignore[attr-defined]
|
||||
context.handler_error_type = None # type: ignore[attr-defined]
|
||||
except NotImplementedError as exc:
|
||||
context.handler_error = exc # type: ignore[attr-defined]
|
||||
context.handler_error_type = "NotImplementedError" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I validate the cloud sandbox strategy")
|
||||
def step_validate_sandbox(context: Any) -> None:
|
||||
"""Validate the cloud sandbox strategy."""
|
||||
strategy: CloudSandboxStrategy = context.cloud_sandbox # type: ignore[attr-defined]
|
||||
context.validation_errors = strategy.validate({}) # type: ignore[attr-defined]
|
||||
saved = getattr(context, "saved_env", {})
|
||||
_restore_cloud_env(saved)
|
||||
|
||||
|
||||
# ── Then steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@then('the cloud type name should be "{name}"')
|
||||
def step_cloud_type_name(context: Any, name: str) -> None:
|
||||
"""Check cloud type name."""
|
||||
assert context.cloud_type_def["name"] == name # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then('the cloud type sandbox_strategy should be "{strategy}"')
|
||||
def step_cloud_type_strategy(context: Any, strategy: str) -> None:
|
||||
"""Check cloud type sandbox strategy."""
|
||||
assert context.cloud_type_def["sandbox_strategy"] == strategy # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then('the cloud type resource_kind should be "{kind}"')
|
||||
def step_cloud_type_kind(context: Any, kind: str) -> None:
|
||||
"""Check cloud type resource kind."""
|
||||
assert context.cloud_type_def["resource_kind"] == kind # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then("the cloud type should be built_in")
|
||||
def step_cloud_type_builtin(context: Any) -> None:
|
||||
"""Check cloud type is built-in."""
|
||||
assert context.cloud_type_def["built_in"] is True # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then("the cloud type should not be user_addable")
|
||||
def step_cloud_type_not_user_addable(context: Any) -> None:
|
||||
"""Check cloud type is not user-addable."""
|
||||
assert context.cloud_type_def["user_addable"] is False # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then("the cloud type should have {count:d} cli_args")
|
||||
def step_cloud_type_cli_args_count(context: Any, count: int) -> None:
|
||||
"""Check cloud type CLI args count."""
|
||||
assert len(context.cloud_type_def["cli_args"]) == count # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then('the cloud type cli_args should include "{arg_name}"')
|
||||
def step_cloud_type_cli_arg(context: Any, arg_name: str) -> None:
|
||||
"""Check cloud type has a specific CLI arg."""
|
||||
names = [a["name"] for a in context.cloud_type_def["cli_args"]] # type: ignore[attr-defined]
|
||||
assert arg_name in names, f"'{arg_name}' not in {names}"
|
||||
|
||||
|
||||
@then('the resolved credential "{field}" should not be None')
|
||||
def step_cred_not_none(context: Any, field: str) -> None:
|
||||
"""Check a resolved credential is not None."""
|
||||
assert context.resolved_creds.get(field) is not None, ( # type: ignore[attr-defined]
|
||||
f"Credential '{field}' is None"
|
||||
)
|
||||
|
||||
|
||||
@then("there should be no cloud validation errors")
|
||||
def step_no_cloud_validation_errors(context: Any) -> None:
|
||||
"""Check there are no cloud validation errors."""
|
||||
errors: list[str] = getattr(context, "validation_errors", [])
|
||||
assert len(errors) == 0, f"Expected no errors, got: {errors}"
|
||||
|
||||
|
||||
@then("there should be cloud validation errors")
|
||||
def step_has_cloud_validation_errors(context: Any) -> None:
|
||||
"""Check there are cloud validation errors."""
|
||||
errors: list[str] = getattr(context, "validation_errors", [])
|
||||
assert len(errors) > 0, "Expected validation errors but got none"
|
||||
|
||||
|
||||
@then('the cloud validation errors should mention "{field}"')
|
||||
def step_cloud_errors_mention(context: Any, field: str) -> None:
|
||||
"""Check cloud validation errors mention a specific field."""
|
||||
errors: list[str] = getattr(context, "validation_errors", [])
|
||||
combined = " ".join(errors)
|
||||
assert field in combined, f"'{field}' not found in errors: {combined}"
|
||||
|
||||
|
||||
@then("the cloud validation errors should not contain secrets")
|
||||
def step_no_secrets_in_cloud_errors(context: Any) -> None:
|
||||
"""Ensure no raw secret values appear in error messages."""
|
||||
errors: list[str] = getattr(context, "validation_errors", [])
|
||||
combined = " ".join(errors)
|
||||
# Secrets should never appear in error text
|
||||
assert "AKIAIOSFODNN7EXAMPLE" not in combined
|
||||
assert "wJalrXUtnFEMI" not in combined
|
||||
|
||||
|
||||
@then("a cloud NotImplementedError should be raised")
|
||||
def step_cloud_not_implemented(context: Any) -> None:
|
||||
"""Check that a NotImplementedError was raised."""
|
||||
assert context.handler_error_type == "NotImplementedError", ( # type: ignore[attr-defined]
|
||||
f"Expected NotImplementedError, got {context.handler_error_type}: " # type: ignore[attr-defined]
|
||||
f"{context.handler_error}" # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
@then("a cloud ValueError should be raised")
|
||||
def step_cloud_value_error(context: Any) -> None:
|
||||
"""Check that a ValueError was raised."""
|
||||
assert context.handler_error_type == "ValueError", ( # type: ignore[attr-defined]
|
||||
f"Expected ValueError, got {context.handler_error_type}: " # type: ignore[attr-defined]
|
||||
f"{context.handler_error}" # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
@then('the cloud error message should mention "{text}"')
|
||||
def step_cloud_error_mentions(context: Any, text: str) -> None:
|
||||
"""Check cloud error message contains expected text."""
|
||||
msg = str(context.handler_error) # type: ignore[attr-defined]
|
||||
assert text in msg, f"'{text}' not found in error: {msg}"
|
||||
|
||||
|
||||
@then('the cloud type should inherit from "{parent}"')
|
||||
def step_cloud_type_inherits(context: Any, parent: str) -> None:
|
||||
"""Check cloud type inherits from expected parent."""
|
||||
actual = context.cloud_type_def.get("inherits") # type: ignore[attr-defined]
|
||||
assert actual == parent, f"Expected inherits='{parent}', got '{actual}'"
|
||||
|
||||
|
||||
@then("the cloud handler should satisfy the ResourceHandler protocol")
|
||||
def step_cloud_satisfies_protocol(context: Any) -> None:
|
||||
"""Check CloudResourceHandler satisfies ResourceHandler protocol."""
|
||||
handler: CloudResourceHandler = context.cloud_handler # type: ignore[attr-defined]
|
||||
assert isinstance(handler, ResourceHandler), (
|
||||
"CloudResourceHandler does not satisfy ResourceHandler protocol"
|
||||
)
|
||||
Reference in New Issue
Block a user