cfn-stack-updater/tests/test_dry_run.py
Vijaya Manne 632ac9e328 Initial commit: One-Click CloudFormation Stack Updater
- Python CLI tool for rolling updates of CL-AppPipe-* and CL-SvcPipe-* stacks
- Async update engine with configurable concurrency (asyncio.Semaphore)
- Exponential backoff retry for API throttling
- Dry-run mode for safe preview
- IAM permission pre-validation
- Comprehensive test suite (80 tests: 11 property-based + 69 unit)
- Full spec documentation (requirements, design, tasks)
2026-05-29 14:56:59 -04:00

128 lines
4.1 KiB
Python

"""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"]