"""Step definitions for AWS session building scenarios. Tests _build_aws_session and CloudResourceHandler.resolve() with mocked boto3. """ from __future__ import annotations import os from typing import Any from unittest.mock import MagicMock, patch from behave import given, then, when # type: ignore[attr-defined] from cleveragents.resource.handlers.cloud import CloudResourceHandler from cleveragents.resource.handlers.cloud_aws import ( _BOTO3_AVAILABLE, _build_aws_session, ) from features.steps.cloud_aws_helpers import ( _clear_cloud_env, _make_mock_session, _make_resource, _restore_cloud_env, ) @given("awssdk the cloud handler module is imported") def step_module_imported(context: Any) -> None: """Ensure the cloud handler module is importable.""" import cleveragents.resource.handlers.cloud as _mod context.cloud_module = _mod # type: ignore[attr-defined] @given("awssdk boto3 is not available") def step_boto3_not_available(context: Any) -> None: """Simulate boto3 not being installed.""" context.boto3_available = False # type: ignore[attr-defined] context.saved_env = _clear_cloud_env() # type: ignore[attr-defined] @given("awssdk boto3 is available via mock") def step_boto3_available_mock(context: Any) -> None: """Mark boto3 as available (mocked).""" context.boto3_available = True # type: ignore[attr-defined] 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('awssdk AWS STS returns account id "{account_id}"') def step_sts_account_id(context: Any, account_id: str) -> None: """Configure mock STS to return a specific account ID.""" context.mock_account_id = account_id # type: ignore[attr-defined] @given("awssdk AWS STS raises a ClientError") def step_sts_client_error(context: Any) -> None: """Configure mock STS to raise a ClientError.""" context.sts_error = RuntimeError("AWS ClientError: InvalidClientTokenId") # type: ignore[attr-defined] @given('awssdk a valid AWS cloud resource of type "{type_name}"') def step_aws_resource_type(context: Any, type_name: str) -> None: """Create an AWS resource of the given type.""" context.cloud_resource = _make_resource( # type: ignore[attr-defined] type_name, properties={ "access-key-id": "AKIAIOSFODNN7EXAMPLE", "secret-access-key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "region": "us-east-1", }, ) @given("awssdk a valid GCP cloud resource with credentials") def step_gcp_resource_with_creds(context: Any) -> None: """Create a GCP resource with credentials.""" context.cloud_resource = _make_resource( # type: ignore[attr-defined] "gcp", properties={ "service-account-json-path": "/path/to/sa.json", "project-id": "my-project", }, ) @given("awssdk a valid Azure cloud resource with credentials") def step_azure_resource_with_creds(context: Any) -> None: """Create an Azure resource with credentials.""" context.cloud_resource = _make_resource( # type: ignore[attr-defined] "azure", properties={ "subscription-id": "sub-123", "tenant-id": "tenant-456", "client-id": "client-789", "client-secret": "secret-abc", }, ) @when("awssdk I try to build an AWS session with valid credentials") def step_try_build_session_no_boto3(context: Any) -> None: """Try to build an AWS session when boto3 is unavailable.""" context.raised_error = None # type: ignore[attr-defined] context.raised_error_type = None # type: ignore[attr-defined] with ( patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", False), patch("cleveragents.resource.handlers.cloud_aws.boto3", None), ): try: _build_aws_session( { "access-key-id": "AKIAIOSFODNN7EXAMPLE", "secret-access-key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", } ) except ImportError as exc: context.raised_error = exc # type: ignore[attr-defined] context.raised_error_type = "ImportError" # type: ignore[attr-defined] @when("awssdk I build an AWS session with explicit credentials") def step_build_session_explicit(context: Any) -> None: """Build an AWS session with explicit credentials using mocked boto3.""" context.raised_error = None # type: ignore[attr-defined] mock_boto3 = MagicMock() mock_session = MagicMock() mock_boto3.Session.return_value = mock_session with ( patch("cleveragents.resource.handlers.cloud_aws.boto3", mock_boto3), patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", True), ): result = _build_aws_session( { "access-key-id": "AKIAIOSFODNN7EXAMPLE", "secret-access-key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "region": "us-east-1", } ) context.session_result = result # type: ignore[attr-defined] context.mock_boto3 = mock_boto3 # type: ignore[attr-defined] @when('awssdk I build an AWS session with a profile name "{profile}"') def step_build_session_profile(context: Any, profile: str) -> None: """Build an AWS session with a profile name.""" context.raised_error = None # type: ignore[attr-defined] mock_boto3 = MagicMock() mock_session = MagicMock() mock_boto3.Session.return_value = mock_session with ( patch("cleveragents.resource.handlers.cloud_aws.boto3", mock_boto3), patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", True), ): result = _build_aws_session({"profile": profile}) context.session_result = result # type: ignore[attr-defined] context.mock_boto3 = mock_boto3 # type: ignore[attr-defined] context.expected_profile = profile # type: ignore[attr-defined] @when("awssdk I call resolve on the cloud handler with mocked boto3") def step_resolve_with_mock(context: Any) -> None: """Call resolve on CloudResourceHandler with mocked boto3.""" from cleveragents.domain.models.core.resource import Resource handler = CloudResourceHandler() resource: Resource = context.cloud_resource # type: ignore[attr-defined] mock_manager = MagicMock() account_id = getattr(context, "mock_account_id", "123456789012") sts_error = getattr(context, "sts_error", None) mock_session = _make_mock_session(account_id=account_id, sts_error=sts_error) context.raised_error = None # type: ignore[attr-defined] context.raised_error_type = None # type: ignore[attr-defined] context.bound_resource = None # type: ignore[attr-defined] mock_boto3 = MagicMock() mock_boto3.Session.return_value = mock_session with ( patch("cleveragents.resource.handlers.cloud_aws.boto3", mock_boto3), patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", True), patch("cleveragents.resource.handlers.cloud._BOTO3_AVAILABLE", True), ): try: result = handler.resolve( resource=resource, plan_id="PLAN-TEST-001", slot_name="cloud-slot", sandbox_manager=mock_manager, ) context.bound_resource = result # type: ignore[attr-defined] except (ValueError, NotImplementedError, ImportError) as exc: context.raised_error = exc # type: ignore[attr-defined] context.raised_error_type = type(exc).__name__ # type: ignore[attr-defined] saved = getattr(context, "saved_env", {}) _restore_cloud_env(saved) @when("awssdk I call resolve on the cloud handler without boto3") def step_resolve_without_boto3(context: Any) -> None: """Call resolve on CloudResourceHandler without boto3.""" from cleveragents.domain.models.core.resource import Resource handler = CloudResourceHandler() resource: Resource = context.cloud_resource # type: ignore[attr-defined] mock_manager = MagicMock() context.raised_error = None # type: ignore[attr-defined] context.raised_error_type = None # type: ignore[attr-defined] context.bound_resource = None # type: ignore[attr-defined] with ( patch("cleveragents.resource.handlers.cloud_aws._BOTO3_AVAILABLE", False), patch("cleveragents.resource.handlers.cloud_aws.boto3", None), patch("cleveragents.resource.handlers.cloud._BOTO3_AVAILABLE", False), ): try: result = handler.resolve( resource=resource, plan_id="PLAN-TEST-001", slot_name="cloud-slot", sandbox_manager=mock_manager, ) context.bound_resource = result # type: ignore[attr-defined] except (ValueError, NotImplementedError, ImportError) as exc: context.raised_error = exc # type: ignore[attr-defined] context.raised_error_type = type(exc).__name__ # type: ignore[attr-defined] saved = getattr(context, "saved_env", {}) _restore_cloud_env(saved) @then("awssdk the boto3 availability flag should be a boolean") def step_boto3_flag_bool(context: Any) -> None: """Check that _BOTO3_AVAILABLE is a boolean.""" assert isinstance(_BOTO3_AVAILABLE, bool), ( f"Expected bool, got {type(_BOTO3_AVAILABLE)}" ) @then('awssdk an ImportError should be raised mentioning "{text}"') def step_import_error_raised(context: Any, text: str) -> None: """Check that an ImportError was raised with the expected text.""" assert context.raised_error_type == "ImportError", ( # type: ignore[attr-defined] f"Expected ImportError, got {context.raised_error_type}: " # type: ignore[attr-defined] f"{context.raised_error}" # type: ignore[attr-defined] ) msg = str(context.raised_error) # type: ignore[attr-defined] assert text in msg, f"'{text}' not found in ImportError: {msg}" @then("awssdk the session should be created successfully") def step_session_created(context: Any) -> None: """Check that a session was created.""" assert context.session_result is not None # type: ignore[attr-defined] mock_boto3: MagicMock = context.mock_boto3 # type: ignore[attr-defined] assert mock_boto3.Session.called, "boto3.Session was not called" @then('awssdk the session should be created with profile "{profile}"') def step_session_created_with_profile(context: Any, profile: str) -> None: """Check that the session was created with the expected profile.""" assert context.session_result is not None # type: ignore[attr-defined] mock_boto3: MagicMock = context.mock_boto3 # type: ignore[attr-defined] call_kwargs = mock_boto3.Session.call_args[1] assert call_kwargs.get("profile_name") == profile, ( f"Expected profile_name='{profile}', got {call_kwargs}" ) @then("awssdk a BoundResource should be returned") def step_bound_resource_returned(context: Any) -> None: """Check that a BoundResource was returned.""" from cleveragents.tool.context import BoundResource assert context.raised_error is None, ( # type: ignore[attr-defined] f"Expected BoundResource but got error: " f"{context.raised_error_type}: {context.raised_error}" # type: ignore[attr-defined] ) assert isinstance(context.bound_resource, BoundResource), ( # type: ignore[attr-defined] f"Expected BoundResource, got {type(context.bound_resource)}" # type: ignore[attr-defined] ) @then('awssdk the BoundResource slot_name should be "{slot_name}"') def step_bound_resource_slot(context: Any, slot_name: str) -> None: """Check BoundResource slot_name.""" from cleveragents.tool.context import BoundResource br: BoundResource = context.bound_resource # type: ignore[attr-defined] assert br.slot_name == slot_name, ( f"Expected slot_name='{slot_name}', got '{br.slot_name}'" ) @then('awssdk the BoundResource resource_type should be "{resource_type}"') def step_bound_resource_type(context: Any, resource_type: str) -> None: """Check BoundResource resource_type.""" from cleveragents.tool.context import BoundResource br: BoundResource = context.bound_resource # type: ignore[attr-defined] assert br.resource_type == resource_type, ( f"Expected resource_type='{resource_type}', got '{br.resource_type}'" ) @then('awssdk the BoundResource sandbox_path should contain "{text}"') def step_bound_resource_sandbox_path(context: Any, text: str) -> None: """Check BoundResource sandbox_path contains expected text.""" from cleveragents.tool.context import BoundResource br: BoundResource = context.bound_resource # type: ignore[attr-defined] assert br.sandbox_path is not None, "Expected sandbox_path to be set" assert text in br.sandbox_path, ( f"Expected '{text}' in sandbox_path='{br.sandbox_path}'" ) @then('awssdk a ValueError should be raised mentioning "{text}"') def step_value_error_raised(context: Any, text: str) -> None: """Check that a ValueError was raised with the expected text.""" assert context.raised_error_type == "ValueError", ( # type: ignore[attr-defined] f"Expected ValueError, got {context.raised_error_type}: " # type: ignore[attr-defined] f"{context.raised_error}" # type: ignore[attr-defined] ) msg = str(context.raised_error) # type: ignore[attr-defined] assert text in msg, f"'{text}' not found in ValueError: {msg}" @then('awssdk a NotImplementedError should be raised mentioning "{text}"') def step_not_implemented_raised(context: Any, text: str) -> None: """Check that a NotImplementedError was raised with the expected text.""" assert context.raised_error_type == "NotImplementedError", ( # type: ignore[attr-defined] f"Expected NotImplementedError, got {context.raised_error_type}: " # type: ignore[attr-defined] f"{context.raised_error}" # type: ignore[attr-defined] ) msg = str(context.raised_error) # type: ignore[attr-defined] assert text in msg, f"'{text}' not found in NotImplementedError: {msg}" @then("awssdk a NotImplementedError should be raised") def step_not_implemented_raised_any(context: Any) -> None: """Check that a NotImplementedError was raised.""" assert context.raised_error_type == "NotImplementedError", ( # type: ignore[attr-defined] f"Expected NotImplementedError, got {context.raised_error_type}: " # type: ignore[attr-defined] f"{context.raised_error}" # type: ignore[attr-defined] ) @then("awssdk no exception should be raised") def step_no_exception(context: Any) -> None: """Check that no exception was raised.""" assert context.raised_error is None, ( # type: ignore[attr-defined] f"Expected no exception, got {context.raised_error_type}: " # type: ignore[attr-defined] f"{context.raised_error}" # type: ignore[attr-defined] ) @then("awssdk no raw credential values should appear in log output") def step_no_raw_creds_in_log(context: Any) -> None: """Check that no raw credential values appear in log output.""" assert ( context.bound_resource is not None or context.raised_error is not None # type: ignore[attr-defined] ), "Handler did not run" if context.raised_error is not None: # type: ignore[attr-defined] msg = str(context.raised_error) # type: ignore[attr-defined] assert "wJalrXUtnFEMI" not in msg, f"Secret found in error: {msg}" assert "AKIAIOSFODNN7EXAMPLE" not in msg, f"Secret found in error: {msg}"