"""Property and unit tests for dry-run mode. Requirements: 6.1, 6.2, 6.3 """ from __future__ import annotations import string from hypothesis import given, settings, strategies as st from cfn_updater.models import DiscoveredStack from cfn_updater.updater import format_dry_run_output # --------------------------------------------------------------------------- # Strategies # --------------------------------------------------------------------------- _stack_name = st.text( alphabet=string.ascii_letters + string.digits + "-", min_size=1, max_size=40, ).map(lambda s: f"CL-AppPipe-{s}") _stack_status = st.sampled_from([ "CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "DELETE_IN_PROGRESS", "UPDATE_ROLLBACK_COMPLETE", "CREATE_IN_PROGRESS", ]) _discovered_stack = st.builds( DiscoveredStack, name=_stack_name, status=_stack_status, updatable=st.booleans(), ) # --------------------------------------------------------------------------- # Property-Based Test — Property 10 # --------------------------------------------------------------------------- # Feature: one-click-cfn-stack-updater, Property 10: Dry-run performs no updates and lists all discovered stacks class TestPropertyDryRunNoUpdates: """ **Validates: Requirements 6.2, 6.3** For any set of discovered stacks with dry-run enabled, zero UpdateStack calls are made and the output contains the name and status of every discovered stack. """ @settings(max_examples=100) @given(stacks=st.lists(_discovered_stack, min_size=0, max_size=30)) def test_dry_run_lists_all_stacks_without_updates(self, stacks: list[DiscoveredStack]) -> None: output = format_dry_run_output(stacks) # The function is pure formatting — it never receives a CFN client, # so zero UpdateStack calls are made by construction. # Every stack's name and status must appear in the output. for stack in stacks: assert stack.name in output, f"Missing stack name: {stack.name}" assert stack.status in output, f"Missing stack status: {stack.status}" # --------------------------------------------------------------------------- # Unit Tests # --------------------------------------------------------------------------- class TestDryRunOutputFormat: """Req 6.2, 6.3: Dry-run outputs all stack names and statuses.""" def test_single_stack(self) -> None: stacks = [ DiscoveredStack(name="CL-AppPipe-abc123", status="CREATE_COMPLETE", updatable=True), ] output = format_dry_run_output(stacks) assert "CL-AppPipe-abc123" in output assert "CREATE_COMPLETE" in output def test_multiple_stacks(self) -> None: stacks = [ DiscoveredStack(name="CL-AppPipe-aaa", status="CREATE_COMPLETE", updatable=True), DiscoveredStack(name="CL-AppPipe-bbb", status="UPDATE_COMPLETE", updatable=True), DiscoveredStack(name="CL-AppPipe-ccc", status="ROLLBACK_COMPLETE", updatable=False), ] output = format_dry_run_output(stacks) for stack in stacks: assert stack.name in output assert stack.status in output def test_empty_stack_list(self) -> None: output = format_dry_run_output([]) assert "No stacks found" in output def test_output_contains_count(self) -> None: stacks = [ DiscoveredStack(name=f"CL-AppPipe-s{i}", status="CREATE_COMPLETE", updatable=True) for i in range(5) ] output = format_dry_run_output(stacks) assert "5" in output class TestDryRunNoApiCalls: """Req 6.1, 6.2: Dry-run makes no update API calls. format_dry_run_output is a pure function that takes a list of DiscoveredStack and returns a string. It has no CFN client parameter, so it is impossible for it to make any API calls by design. """ def test_function_signature_has_no_client_parameter(self) -> None: import inspect sig = inspect.signature(format_dry_run_output) param_names = list(sig.parameters.keys()) # Only parameter is 'stacks' assert param_names == ["stacks"]