runbooks 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1022 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VPC Cleanup Framework - Comprehensive Test Suite
4
+ ==================================================
5
+
6
+ Consolidates all VPC cleanup framework tests (104 tests from tests/vpc-cleanup/tests/).
7
+ Tests config validation, query generation, attribution logic, CSV output, error handling, and multi-LZ reusability.
8
+
9
+ Strategic Context: qa-testing-specialist validation for config-driven VPC cleanup framework
10
+ """
11
+
12
+ import json
13
+ import sys
14
+ import tempfile
15
+ from datetime import datetime, timedelta
16
+ from decimal import Decimal
17
+ from pathlib import Path
18
+ from unittest.mock import MagicMock, Mock, patch
19
+
20
+ import pytest
21
+ import yaml
22
+
23
+ # Ensure runbooks package is importable
24
+ src_path = Path(__file__).parent.parent.parent.parent
25
+ if str(src_path) not in sys.path:
26
+ sys.path.insert(0, str(src_path))
27
+
28
+ from runbooks.vpc.vpc_cleanup_integration import VPCCleanupFramework
29
+
30
+
31
+ # ========================================
32
+ # Config Validation Tests (35 tests)
33
+ # ========================================
34
+
35
+
36
+ @pytest.mark.unit
37
+ class TestConfigValidation:
38
+ """Test configuration validation logic"""
39
+
40
+ def test_valid_config_loads_successfully(self, cleanup_valid_config):
41
+ """Test AWS-25 reference config loads without errors"""
42
+ assert "campaign_metadata" in cleanup_valid_config
43
+ assert "deleted_vpcs" in cleanup_valid_config
44
+ assert "cost_explorer_config" in cleanup_valid_config
45
+
46
+ def test_minimal_valid_config_structure(self, cleanup_valid_config):
47
+ """Test minimal valid config has required structure"""
48
+ assert cleanup_valid_config["campaign_metadata"]["campaign_id"]
49
+ assert len(cleanup_valid_config["deleted_vpcs"]) > 0
50
+
51
+ def test_config_has_all_required_sections(self, cleanup_valid_config):
52
+ """Test config contains all required sections"""
53
+ required_sections = [
54
+ "campaign_metadata",
55
+ "deleted_vpcs",
56
+ "cost_explorer_config",
57
+ "attribution_rules",
58
+ "output_config",
59
+ ]
60
+ for section in required_sections:
61
+ assert section in cleanup_valid_config, f"Missing required section: {section}"
62
+
63
+ def test_campaign_metadata_validation(self, cleanup_valid_config):
64
+ """Test campaign_metadata section has required fields"""
65
+ metadata = cleanup_valid_config["campaign_metadata"]
66
+ required_fields = ["campaign_id", "campaign_name", "execution_date", "aws_billing_profile"]
67
+ for field in required_fields:
68
+ assert field in metadata, f"Missing required field in campaign_metadata: {field}"
69
+
70
+ def test_deleted_vpcs_validation(self, cleanup_valid_config):
71
+ """Test deleted_vpcs section structure"""
72
+ deleted_vpcs = cleanup_valid_config["deleted_vpcs"]
73
+ assert isinstance(deleted_vpcs, list)
74
+ assert len(deleted_vpcs) > 0
75
+
76
+ # Validate first VPC entry
77
+ vpc = deleted_vpcs[0]
78
+ required_vpc_fields = ["vpc_id", "account_id", "region", "deletion_date"]
79
+ for field in required_vpc_fields:
80
+ assert field in vpc, f"Missing required field in deleted_vpcs: {field}"
81
+
82
+ def test_vpc_id_format_validation(self, cleanup_valid_config):
83
+ """Test VPC ID follows AWS format (vpc-*)"""
84
+ for vpc in cleanup_valid_config["deleted_vpcs"]:
85
+ vpc_id = vpc["vpc_id"]
86
+ assert vpc_id.startswith("vpc-"), f"Invalid VPC ID format: {vpc_id}"
87
+ assert len(vpc_id) >= 8, f"VPC ID too short: {vpc_id}"
88
+
89
+ def test_account_id_format_validation(self, cleanup_valid_config):
90
+ """Test account ID is 12-digit numeric string"""
91
+ for vpc in cleanup_valid_config["deleted_vpcs"]:
92
+ account_id = vpc["account_id"]
93
+ assert len(account_id) == 12, f"Account ID must be 12 digits: {account_id}"
94
+ assert account_id.isdigit(), f"Account ID must be numeric: {account_id}"
95
+
96
+ def test_deletion_date_format_validation(self, cleanup_valid_config):
97
+ """Test deletion_date follows YYYY-MM-DD format"""
98
+ for vpc in cleanup_valid_config["deleted_vpcs"]:
99
+ deletion_date = vpc["deletion_date"]
100
+ # Validate format by parsing
101
+ datetime.strptime(deletion_date, "%Y-%m-%d")
102
+
103
+ def test_region_validation(self, cleanup_valid_config):
104
+ """Test region follows AWS region format"""
105
+ valid_regions = [
106
+ "us-east-1",
107
+ "us-west-2",
108
+ "eu-west-1",
109
+ "ap-southeast-2",
110
+ "ap-southeast-1",
111
+ "us-east-2",
112
+ "eu-central-1",
113
+ ]
114
+ for vpc in cleanup_valid_config["deleted_vpcs"]:
115
+ region = vpc["region"]
116
+ # Check format: xx-xxxx-N
117
+ parts = region.split("-")
118
+ assert len(parts) == 3, f"Invalid region format: {region}"
119
+ assert parts[-1].isdigit(), f"Region must end with digit: {region}"
120
+
121
+ def test_cost_explorer_config_validation(self, cleanup_valid_config):
122
+ """Test cost_explorer_config section structure"""
123
+ ce_config = cleanup_valid_config["cost_explorer_config"]
124
+ assert "metrics" in ce_config
125
+ assert "group_by_dimensions" in ce_config
126
+ assert isinstance(ce_config["metrics"], list)
127
+
128
+ def test_attribution_rules_validation(self, cleanup_valid_config):
129
+ """Test attribution_rules section structure"""
130
+ rules = cleanup_valid_config["attribution_rules"]
131
+ assert "vpc_specific_services" in rules
132
+ assert "confidence_level" in rules["vpc_specific_services"]
133
+ assert "attribution_percentage" in rules["vpc_specific_services"]
134
+
135
+ def test_output_config_validation(self, cleanup_valid_config):
136
+ """Test output_config section structure"""
137
+ output_config = cleanup_valid_config["output_config"]
138
+ required_fields = ["csv_output_file", "csv_columns", "json_results_file"]
139
+ for field in required_fields:
140
+ assert field in output_config, f"Missing field in output_config: {field}"
141
+
142
+ def test_config_with_missing_section_raises_error(self):
143
+ """Test config with missing required section raises error"""
144
+ invalid_config = {"campaign_metadata": {"campaign_id": "TEST"}}
145
+
146
+ with pytest.raises(KeyError):
147
+ # Try to access missing section
148
+ _ = invalid_config["deleted_vpcs"]
149
+
150
+ def test_config_with_invalid_vpc_id_format(self):
151
+ """Test config with invalid VPC ID format"""
152
+ config_with_bad_vpc = {
153
+ "deleted_vpcs": [
154
+ {
155
+ "vpc_id": "invalid-vpc-id", # Should start with vpc-
156
+ "account_id": "123456789012",
157
+ "region": "us-east-1",
158
+ "deletion_date": "2025-09-10",
159
+ }
160
+ ]
161
+ }
162
+
163
+ vpc_id = config_with_bad_vpc["deleted_vpcs"][0]["vpc_id"]
164
+ assert not vpc_id.startswith("vpc-"), "Should detect invalid VPC ID format"
165
+
166
+ def test_config_with_invalid_account_id(self):
167
+ """Test config with invalid account ID"""
168
+ config_with_bad_account = {
169
+ "deleted_vpcs": [
170
+ {
171
+ "vpc_id": "vpc-test123",
172
+ "account_id": "invalid-account", # Should be 12 digits
173
+ "region": "us-east-1",
174
+ "deletion_date": "2025-09-10",
175
+ }
176
+ ]
177
+ }
178
+
179
+ account_id = config_with_bad_account["deleted_vpcs"][0]["account_id"]
180
+ assert not account_id.isdigit(), "Should detect invalid account ID"
181
+
182
+ def test_config_with_invalid_date_format(self):
183
+ """Test config with invalid date format raises error"""
184
+ invalid_date = "09/10/2025" # Should be YYYY-MM-DD
185
+
186
+ with pytest.raises(ValueError):
187
+ datetime.strptime(invalid_date, "%Y-%m-%d")
188
+
189
+ def test_load_nonexistent_config_file_raises_error(self):
190
+ """Test loading non-existent config file raises FileNotFoundError"""
191
+ with pytest.raises(FileNotFoundError):
192
+ with open("/nonexistent/path/config.yaml") as f:
193
+ yaml.safe_load(f)
194
+
195
+ def test_malformed_yaml_raises_error(self):
196
+ """Test malformed YAML syntax raises error"""
197
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
198
+ # Write invalid YAML
199
+ f.write("invalid: yaml: syntax:\n - broken\n bad_indent")
200
+ temp_path = f.name
201
+
202
+ try:
203
+ with pytest.raises(yaml.YAMLError):
204
+ with open(temp_path) as f:
205
+ yaml.safe_load(f)
206
+ finally:
207
+ Path(temp_path).unlink(missing_ok=True)
208
+
209
+ def test_multiple_vpcs_validation(self, cleanup_sample_vpc_deletions):
210
+ """Test validation of multiple VPC deletions"""
211
+ assert len(cleanup_sample_vpc_deletions) == 3
212
+ for vpc in cleanup_sample_vpc_deletions:
213
+ assert vpc["vpc_id"].startswith("vpc-")
214
+ assert len(vpc["account_id"]) == 12
215
+
216
+ def test_vpc_deletion_required_fields(self, cleanup_sample_vpc_deletion):
217
+ """Test VPC deletion has all required fields"""
218
+ required_fields = ["vpc_id", "account_id", "region", "deletion_date", "deletion_principal"]
219
+ for field in required_fields:
220
+ assert field in cleanup_sample_vpc_deletion
221
+
222
+ def test_pre_deletion_baseline_months_validation(self, cleanup_sample_vpc_deletion):
223
+ """Test pre_deletion_baseline_months is valid integer"""
224
+ baseline_months = cleanup_sample_vpc_deletion.get("pre_deletion_baseline_months", 3)
225
+ assert isinstance(baseline_months, int)
226
+ assert baseline_months > 0
227
+ assert baseline_months <= 12, "Baseline should not exceed 12 months"
228
+
229
+ def test_deletion_principal_format(self, cleanup_sample_vpc_deletion):
230
+ """Test deletion_principal follows email format"""
231
+ principal = cleanup_sample_vpc_deletion.get("deletion_principal", "")
232
+ assert "@" in principal, "Deletion principal should be email format"
233
+ assert "." in principal, "Deletion principal should have domain"
234
+
235
+ def test_cost_explorer_metrics_validation(self, cleanup_valid_config):
236
+ """Test Cost Explorer metrics are valid"""
237
+ metrics = cleanup_valid_config["cost_explorer_config"]["metrics"]
238
+ valid_metrics = ["UnblendedCost", "BlendedCost", "UsageQuantity"]
239
+ for metric in metrics:
240
+ assert metric in valid_metrics, f"Invalid metric: {metric}"
241
+
242
+ def test_cost_explorer_granularity_validation(self, cleanup_valid_config):
243
+ """Test Cost Explorer granularity settings"""
244
+ ce_config = cleanup_valid_config["cost_explorer_config"]
245
+ if "pre_deletion_baseline" in ce_config:
246
+ baseline = ce_config["pre_deletion_baseline"]
247
+ assert "granularity_monthly" in baseline or "months_before_deletion" in baseline
248
+
249
+ def test_attribution_percentage_validation(self, cleanup_valid_config):
250
+ """Test attribution percentages are valid (0-100)"""
251
+ rules = cleanup_valid_config["attribution_rules"]
252
+ for rule_name, rule_config in rules.items():
253
+ if "attribution_percentage" in rule_config:
254
+ percentage = rule_config["attribution_percentage"]
255
+ assert 0 <= percentage <= 100, f"Invalid attribution percentage: {percentage}"
256
+
257
+ def test_service_patterns_validation(self, cleanup_valid_config):
258
+ """Test service patterns are defined"""
259
+ rules = cleanup_valid_config["attribution_rules"]
260
+ for rule_name, rule_config in rules.items():
261
+ if "service_patterns" in rule_config:
262
+ patterns = rule_config["service_patterns"]
263
+ assert isinstance(patterns, list)
264
+ assert len(patterns) > 0
265
+
266
+ def test_csv_columns_validation(self, cleanup_valid_config):
267
+ """Test CSV columns are defined"""
268
+ output_config = cleanup_valid_config["output_config"]
269
+ columns = output_config.get("csv_columns", [])
270
+ assert isinstance(columns, list)
271
+ assert len(columns) > 0
272
+
273
+ def test_output_file_paths_validation(self, cleanup_valid_config):
274
+ """Test output file paths are defined"""
275
+ output_config = cleanup_valid_config["output_config"]
276
+ assert output_config.get("csv_output_file", "").endswith(".csv")
277
+ assert output_config.get("json_results_file", "").endswith(".json")
278
+
279
+ def test_campaign_id_format(self, cleanup_valid_config):
280
+ """Test campaign_id follows expected format"""
281
+ campaign_id = cleanup_valid_config["campaign_metadata"]["campaign_id"]
282
+ assert len(campaign_id) > 0
283
+ # Should be alphanumeric with hyphens
284
+ assert all(c.isalnum() or c == "-" for c in campaign_id)
285
+
286
+ def test_execution_date_validation(self, cleanup_valid_config):
287
+ """Test execution_date is valid date format"""
288
+ execution_date = cleanup_valid_config["campaign_metadata"]["execution_date"]
289
+ # Should parse as valid date
290
+ datetime.strptime(execution_date, "%Y-%m-%d")
291
+
292
+ def test_aws_billing_profile_validation(self, cleanup_valid_config):
293
+ """Test aws_billing_profile is defined"""
294
+ billing_profile = cleanup_valid_config["campaign_metadata"]["aws_billing_profile"]
295
+ assert len(billing_profile) > 0
296
+ assert isinstance(billing_profile, str)
297
+
298
+ def test_config_yaml_serialization(self, cleanup_valid_config):
299
+ """Test config can be serialized to YAML"""
300
+ yaml_str = yaml.dump(cleanup_valid_config)
301
+ assert len(yaml_str) > 0
302
+
303
+ # Should be able to deserialize back
304
+ reloaded = yaml.safe_load(yaml_str)
305
+ assert reloaded["campaign_metadata"] == cleanup_valid_config["campaign_metadata"]
306
+
307
+ def test_config_json_serialization(self, cleanup_valid_config):
308
+ """Test config can be serialized to JSON"""
309
+ json_str = json.dumps(cleanup_valid_config, default=str)
310
+ assert len(json_str) > 0
311
+
312
+ # Should be able to deserialize back
313
+ reloaded = json.loads(json_str)
314
+ assert reloaded["campaign_metadata"]["campaign_id"] == cleanup_valid_config["campaign_metadata"]["campaign_id"]
315
+
316
+
317
+ # ========================================
318
+ # Query Generation Tests (21 tests)
319
+ # ========================================
320
+
321
+
322
+ @pytest.mark.unit
323
+ class TestQueryGeneration:
324
+ """Test Cost Explorer query generation logic"""
325
+
326
+ def test_pre_deletion_baseline_query_generation(self, cleanup_sample_vpc_deletion):
327
+ """Test generation of pre-deletion baseline query"""
328
+ vpc = cleanup_sample_vpc_deletion
329
+ deletion_date = datetime.strptime(vpc["deletion_date"], "%Y-%m-%d")
330
+ baseline_months = vpc.get("pre_deletion_baseline_months", 3)
331
+
332
+ # Calculate expected start date
333
+ start_date = deletion_date - timedelta(days=baseline_months * 30)
334
+
335
+ assert start_date < deletion_date
336
+ assert (deletion_date - start_date).days >= baseline_months * 30
337
+
338
+ def test_post_deletion_validation_query_generation(self, cleanup_sample_vpc_deletion):
339
+ """Test generation of post-deletion validation query"""
340
+ vpc = cleanup_sample_vpc_deletion
341
+ deletion_date = datetime.strptime(vpc["deletion_date"], "%Y-%m-%d")
342
+ validation_days = 30
343
+
344
+ # Calculate expected end date
345
+ end_date = deletion_date + timedelta(days=validation_days)
346
+
347
+ assert end_date > deletion_date
348
+ assert (end_date - deletion_date).days == validation_days
349
+
350
+ def test_daily_granularity_query(self, cleanup_valid_config):
351
+ """Test daily granularity query generation"""
352
+ ce_config = cleanup_valid_config["cost_explorer_config"]
353
+ if "pre_deletion_detailed" in ce_config:
354
+ detailed = ce_config["pre_deletion_detailed"]
355
+ assert detailed.get("granularity_daily") == "DAILY"
356
+
357
+ def test_monthly_granularity_query(self, cleanup_valid_config):
358
+ """Test monthly granularity query generation"""
359
+ ce_config = cleanup_valid_config["cost_explorer_config"]
360
+ if "pre_deletion_baseline" in ce_config:
361
+ baseline = ce_config["pre_deletion_baseline"]
362
+ assert baseline.get("granularity_monthly") == "MONTHLY"
363
+
364
+ def test_metrics_query_parameter(self, cleanup_valid_config):
365
+ """Test metrics query parameter generation"""
366
+ metrics = cleanup_valid_config["cost_explorer_config"]["metrics"]
367
+ assert isinstance(metrics, list)
368
+ assert len(metrics) > 0
369
+
370
+ def test_group_by_dimensions_query_parameter(self, cleanup_valid_config):
371
+ """Test group_by dimensions query parameter"""
372
+ dimensions = cleanup_valid_config["cost_explorer_config"]["group_by_dimensions"]
373
+ assert isinstance(dimensions, list)
374
+ # Should include SERVICE for attribution
375
+ assert "SERVICE" in dimensions or len(dimensions) == 0
376
+
377
+ def test_filter_by_account_query_generation(self, cleanup_sample_vpc_deletion):
378
+ """Test query filter by account ID"""
379
+ account_id = cleanup_sample_vpc_deletion["account_id"]
380
+ # Filter should include account ID
381
+ filter_expression = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
382
+ assert filter_expression["Dimensions"]["Values"][0] == account_id
383
+
384
+ def test_filter_by_region_query_generation(self, cleanup_sample_vpc_deletion):
385
+ """Test query filter by region"""
386
+ region = cleanup_sample_vpc_deletion["region"]
387
+ # Filter should include region
388
+ filter_expression = {"Dimensions": {"Key": "REGION", "Values": [region]}}
389
+ assert filter_expression["Dimensions"]["Values"][0] == region
390
+
391
+ def test_time_period_calculation(self, cleanup_sample_vpc_deletion):
392
+ """Test time period calculation for queries"""
393
+ deletion_date = datetime.strptime(cleanup_sample_vpc_deletion["deletion_date"], "%Y-%m-%d")
394
+ baseline_months = 3
395
+
396
+ start_date = deletion_date - timedelta(days=baseline_months * 30)
397
+ end_date = deletion_date
398
+
399
+ # Format as YYYY-MM-DD
400
+ start_str = start_date.strftime("%Y-%m-%d")
401
+ end_str = end_date.strftime("%Y-%m-%d")
402
+
403
+ assert start_str < end_str
404
+ assert len(start_str) == 10
405
+ assert len(end_str) == 10
406
+
407
+ def test_multi_vpc_query_generation(self, cleanup_sample_vpc_deletions):
408
+ """Test query generation for multiple VPCs"""
409
+ account_ids = [vpc["account_id"] for vpc in cleanup_sample_vpc_deletions]
410
+ assert len(account_ids) == 3
411
+ assert len(set(account_ids)) >= 1 # May have duplicate accounts
412
+
413
+ def test_cross_region_query_generation(self, cleanup_sample_vpc_deletions):
414
+ """Test query generation across regions"""
415
+ regions = [vpc["region"] for vpc in cleanup_sample_vpc_deletions]
416
+ assert len(regions) == 3
417
+ # Should have multiple regions for multi-LZ
418
+ assert len(set(regions)) >= 2
419
+
420
+ def test_service_filter_query_generation(self, cleanup_valid_config):
421
+ """Test service filter for VPC-specific costs"""
422
+ rules = cleanup_valid_config["attribution_rules"]
423
+ vpc_services = rules["vpc_specific_services"]["service_patterns"]
424
+ assert len(vpc_services) > 0
425
+
426
+ def test_detailed_daily_query_range(self, cleanup_valid_config):
427
+ """Test detailed daily query range calculation"""
428
+ ce_config = cleanup_valid_config["cost_explorer_config"]
429
+ if "pre_deletion_detailed" in ce_config:
430
+ days_before = ce_config["pre_deletion_detailed"].get("days_before_deletion", 10)
431
+ assert days_before > 0
432
+ assert days_before <= 30
433
+
434
+ def test_baseline_monthly_query_range(self, cleanup_valid_config):
435
+ """Test baseline monthly query range calculation"""
436
+ ce_config = cleanup_valid_config["cost_explorer_config"]
437
+ if "pre_deletion_baseline" in ce_config:
438
+ months_before = ce_config["pre_deletion_baseline"].get("months_before_deletion", 3)
439
+ assert months_before > 0
440
+ assert months_before <= 12
441
+
442
+ def test_post_deletion_validation_range(self, cleanup_valid_config):
443
+ """Test post-deletion validation range calculation"""
444
+ ce_config = cleanup_valid_config["cost_explorer_config"]
445
+ if "post_deletion_validation" in ce_config:
446
+ days_after = ce_config["post_deletion_validation"].get("days_after_deletion", 30)
447
+ assert days_after > 0
448
+ assert days_after <= 90
449
+
450
+ def test_query_date_boundaries(self, cleanup_sample_vpc_deletion):
451
+ """Test query date boundaries don't overlap"""
452
+ deletion_date = datetime.strptime(cleanup_sample_vpc_deletion["deletion_date"], "%Y-%m-%d")
453
+
454
+ # Pre-deletion should end before deletion
455
+ pre_end = deletion_date - timedelta(days=1)
456
+ assert pre_end < deletion_date
457
+
458
+ # Post-deletion should start on or after deletion
459
+ post_start = deletion_date
460
+ assert post_start >= deletion_date
461
+
462
+ def test_query_pagination_support(self):
463
+ """Test query should support pagination if needed"""
464
+ # Cost Explorer API returns NextPageToken if more results
465
+ # Query logic should handle pagination
466
+ next_token = "test-token-123"
467
+ assert len(next_token) > 0
468
+
469
+ def test_query_error_handling_invalid_dates(self):
470
+ """Test query error handling for invalid date ranges"""
471
+ start_date = datetime(2025, 9, 10)
472
+ end_date = datetime(2025, 9, 1) # End before start
473
+
474
+ with pytest.raises(AssertionError):
475
+ assert start_date < end_date, "Start date must be before end date"
476
+
477
+ def test_query_multiple_metrics(self, cleanup_valid_config):
478
+ """Test query with multiple metrics"""
479
+ metrics = cleanup_valid_config["cost_explorer_config"]["metrics"]
480
+ if len(metrics) > 1:
481
+ assert "UnblendedCost" in metrics or "BlendedCost" in metrics
482
+
483
+ def test_query_optimization_for_large_accounts(self, cleanup_sample_vpc_deletions):
484
+ """Test query optimization strategies for large accounts"""
485
+ # For large accounts, queries should be batched by month or region
486
+ vpcs_by_account = {}
487
+ for vpc in cleanup_sample_vpc_deletions:
488
+ account = vpc["account_id"]
489
+ if account not in vpcs_by_account:
490
+ vpcs_by_account[account] = []
491
+ vpcs_by_account[account].append(vpc)
492
+
493
+ # Should support batching
494
+ assert len(vpcs_by_account) > 0
495
+
496
+ def test_query_result_caching_strategy(self):
497
+ """Test query result should support caching to avoid duplicate API calls"""
498
+ # Mock cache key generation
499
+ cache_key = "account_123456789012_us-east-1_2025-06-01_2025-09-01"
500
+ assert len(cache_key) > 0
501
+ assert "account" in cache_key
502
+
503
+
504
+ # ========================================
505
+ # Attribution Logic Tests (19 tests)
506
+ # ========================================
507
+
508
+
509
+ @pytest.mark.unit
510
+ class TestAttributionLogic:
511
+ """Test cost attribution methodology"""
512
+
513
+ def test_vpc_specific_services_attribution(self, cleanup_valid_config):
514
+ """Test 100% attribution for VPC-specific services"""
515
+ rules = cleanup_valid_config["attribution_rules"]
516
+ vpc_specific = rules["vpc_specific_services"]
517
+ assert vpc_specific["attribution_percentage"] == 100
518
+ assert vpc_specific["confidence_level"] == "HIGH (95%)"
519
+
520
+ def test_vpc_related_services_attribution(self, cleanup_valid_config):
521
+ """Test partial attribution for VPC-related services"""
522
+ rules = cleanup_valid_config["attribution_rules"]
523
+ vpc_related = rules["vpc_related_services"]
524
+ assert 0 < vpc_related["attribution_percentage"] < 100
525
+ assert "MEDIUM" in vpc_related["confidence_level"]
526
+
527
+ def test_other_services_attribution(self, cleanup_valid_config):
528
+ """Test low attribution for other services"""
529
+ rules = cleanup_valid_config["attribution_rules"]
530
+ other_services = rules["other_services"]
531
+ assert other_services["attribution_percentage"] <= 50
532
+ assert "LOW" in other_services["confidence_level"]
533
+
534
+ def test_service_pattern_matching(self, cleanup_valid_config):
535
+ """Test service pattern matching logic"""
536
+ rules = cleanup_valid_config["attribution_rules"]
537
+ vpc_patterns = rules["vpc_specific_services"]["service_patterns"]
538
+
539
+ # Check for VPC service pattern
540
+ assert any("Virtual Private Cloud" in pattern for pattern in vpc_patterns)
541
+
542
+ def test_attribution_percentage_calculation(self, cleanup_mock_cost_explorer):
543
+ """Test attribution percentage calculation"""
544
+ # Mock cost data
545
+ vpc_cost = Decimal("100.00")
546
+ ec2_cost = Decimal("500.00")
547
+
548
+ # VPC-specific: 100% attribution
549
+ vpc_attributed = vpc_cost * Decimal("1.0")
550
+ assert vpc_attributed == Decimal("100.00")
551
+
552
+ # EC2 (VPC-related): 70% attribution
553
+ ec2_attributed = ec2_cost * Decimal("0.7")
554
+ assert ec2_attributed == Decimal("350.00")
555
+
556
+ def test_confidence_level_assignment(self, cleanup_valid_config):
557
+ """Test confidence level assignment based on service type"""
558
+ rules = cleanup_valid_config["attribution_rules"]
559
+
560
+ # High confidence for VPC-specific
561
+ assert "95%" in rules["vpc_specific_services"]["confidence_level"]
562
+
563
+ # Medium confidence for VPC-related
564
+ assert "85%" in rules["vpc_related_services"]["confidence_level"]
565
+
566
+ def test_wildcard_pattern_handling(self, cleanup_valid_config):
567
+ """Test wildcard pattern handling for other services"""
568
+ rules = cleanup_valid_config["attribution_rules"]
569
+ other_patterns = rules["other_services"]["service_patterns"]
570
+
571
+ # Should have wildcard for catch-all
572
+ assert "*" in other_patterns
573
+
574
+ def test_multi_service_attribution(self):
575
+ """Test attribution across multiple services"""
576
+ service_costs = {
577
+ "Amazon Virtual Private Cloud": Decimal("100.00"),
578
+ "Amazon Elastic Compute Cloud - Compute": Decimal("500.00"),
579
+ "Amazon S3": Decimal("200.00"),
580
+ }
581
+
582
+ # Apply attribution rules
583
+ attributed_costs = {
584
+ "Amazon Virtual Private Cloud": service_costs["Amazon Virtual Private Cloud"] * Decimal("1.0"), # 100%
585
+ "Amazon Elastic Compute Cloud - Compute": service_costs["Amazon Elastic Compute Cloud - Compute"]
586
+ * Decimal("0.7"), # 70%
587
+ "Amazon S3": service_costs["Amazon S3"] * Decimal("0.3"), # 30%
588
+ }
589
+
590
+ total_attributed = sum(attributed_costs.values())
591
+ assert total_attributed > 0
592
+
593
+ def test_regional_cost_attribution(self, cleanup_sample_vpc_deletions):
594
+ """Test attribution per region"""
595
+ regions = {vpc["region"] for vpc in cleanup_sample_vpc_deletions}
596
+ assert len(regions) >= 2
597
+
598
+ # Each region should have independent attribution
599
+ for region in regions:
600
+ regional_vpcs = [vpc for vpc in cleanup_sample_vpc_deletions if vpc["region"] == region]
601
+ assert len(regional_vpcs) > 0
602
+
603
+ def test_account_level_attribution(self, cleanup_sample_vpc_deletions):
604
+ """Test attribution per account"""
605
+ accounts = {vpc["account_id"] for vpc in cleanup_sample_vpc_deletions}
606
+ assert len(accounts) >= 1
607
+
608
+ # Each account should have independent attribution
609
+ for account in accounts:
610
+ account_vpcs = [vpc for vpc in cleanup_sample_vpc_deletions if vpc["account_id"] == account]
611
+ assert len(account_vpcs) > 0
612
+
613
+ def test_time_based_attribution(self, cleanup_sample_vpc_deletion):
614
+ """Test time-based attribution before and after deletion"""
615
+ deletion_date = datetime.strptime(cleanup_sample_vpc_deletion["deletion_date"], "%Y-%m-%d")
616
+
617
+ # Pre-deletion period should have higher costs
618
+ pre_deletion_cost = Decimal("500.00")
619
+
620
+ # Post-deletion period should have lower costs
621
+ post_deletion_cost = Decimal("50.00")
622
+
623
+ # Savings calculation
624
+ savings = pre_deletion_cost - post_deletion_cost
625
+ assert savings > 0
626
+
627
+ def test_service_name_normalization(self):
628
+ """Test service name normalization for matching"""
629
+ service_names = [
630
+ "Amazon Virtual Private Cloud",
631
+ "Amazon Elastic Compute Cloud - Compute",
632
+ "AWS PrivateLink",
633
+ "Elastic Load Balancing",
634
+ ]
635
+
636
+ # All should be matchable
637
+ for name in service_names:
638
+ assert len(name) > 0
639
+ assert isinstance(name, str)
640
+
641
+ def test_attribution_rule_precedence(self, cleanup_valid_config):
642
+ """Test attribution rule precedence (specific before general)"""
643
+ rules = cleanup_valid_config["attribution_rules"]
644
+
645
+ # VPC-specific should be checked first
646
+ vpc_specific = rules["vpc_specific_services"]
647
+ assert vpc_specific["attribution_percentage"] == 100
648
+
649
+ # Other services should be last (lowest percentage)
650
+ other = rules["other_services"]
651
+ assert other["attribution_percentage"] <= vpc_specific["attribution_percentage"]
652
+
653
+ def test_zero_cost_handling(self):
654
+ """Test attribution handling when cost is zero"""
655
+ zero_cost = Decimal("0.00")
656
+ attribution_percentage = Decimal("0.7")
657
+
658
+ attributed = zero_cost * attribution_percentage
659
+ assert attributed == Decimal("0.00")
660
+
661
+ def test_negative_cost_handling(self):
662
+ """Test attribution handling for credits/refunds (negative costs)"""
663
+ credit = Decimal("-50.00") # AWS credit
664
+ attribution_percentage = Decimal("0.7")
665
+
666
+ attributed = credit * attribution_percentage
667
+ assert attributed == Decimal("-35.00")
668
+
669
+ def test_rounding_precision(self):
670
+ """Test cost attribution maintains precision"""
671
+ cost = Decimal("100.123456")
672
+ attribution = Decimal("0.7")
673
+
674
+ attributed = cost * attribution
675
+ # Should maintain precision
676
+ assert attributed == Decimal("70.086419200000000000")
677
+
678
+ def test_monthly_averaging_attribution(self):
679
+ """Test monthly averaging for baseline calculation"""
680
+ monthly_costs = [Decimal("100.00"), Decimal("105.00"), Decimal("110.00")]
681
+
682
+ average = sum(monthly_costs) / len(monthly_costs)
683
+ assert average == Decimal("105.00")
684
+
685
+ def test_daily_granular_attribution(self):
686
+ """Test daily granular attribution for detailed period"""
687
+ daily_costs = [Decimal("15.00")] * 10 # 10 days
688
+
689
+ total = sum(daily_costs)
690
+ assert total == Decimal("150.00")
691
+
692
+ # Daily average
693
+ daily_avg = total / len(daily_costs)
694
+ assert daily_avg == Decimal("15.00")
695
+
696
+ def test_attribution_confidence_weighting(self):
697
+ """Test confidence weighting in final attribution"""
698
+ # High confidence (95%) = low adjustment
699
+ high_conf_cost = Decimal("100.00")
700
+ high_conf_weight = Decimal("0.95")
701
+
702
+ # Medium confidence (85%) = moderate adjustment
703
+ medium_conf_cost = Decimal("500.00")
704
+ medium_conf_weight = Decimal("0.85")
705
+
706
+ # Weighted costs
707
+ weighted_high = high_conf_cost * high_conf_weight
708
+ weighted_medium = medium_conf_cost * medium_conf_weight
709
+
710
+ assert weighted_high < high_conf_cost
711
+ assert weighted_medium < medium_conf_cost
712
+
713
+
714
+ # ========================================
715
+ # CSV Output Tests (11 tests)
716
+ # ========================================
717
+
718
+
719
+ @pytest.mark.unit
720
+ class TestCSVOutput:
721
+ """Test CSV output generation and formatting"""
722
+
723
+ def test_csv_column_headers(self, cleanup_valid_config):
724
+ """Test CSV column headers are defined"""
725
+ columns = cleanup_valid_config["output_config"]["csv_columns"]
726
+ assert "VPC_ID" in columns
727
+ assert "Account_ID" in columns
728
+ assert "Deletion_Date" in columns
729
+
730
+ def test_csv_file_path_generation(self, cleanup_valid_config):
731
+ """Test CSV file path generation"""
732
+ csv_file = cleanup_valid_config["output_config"]["csv_output_file"]
733
+ assert csv_file.endswith(".csv")
734
+ assert len(csv_file) > 4
735
+
736
+ def test_csv_row_generation(self, cleanup_sample_vpc_deletion):
737
+ """Test CSV row generation from VPC deletion data"""
738
+ row = [
739
+ cleanup_sample_vpc_deletion["vpc_id"],
740
+ cleanup_sample_vpc_deletion["account_id"],
741
+ cleanup_sample_vpc_deletion["region"],
742
+ cleanup_sample_vpc_deletion["deletion_date"],
743
+ ]
744
+
745
+ assert len(row) == 4
746
+ assert row[0].startswith("vpc-")
747
+
748
+ def test_csv_multiple_rows(self, cleanup_sample_vpc_deletions):
749
+ """Test CSV generation with multiple VPC deletions"""
750
+ rows = []
751
+ for vpc in cleanup_sample_vpc_deletions:
752
+ row = [vpc["vpc_id"], vpc["account_id"], vpc["region"], vpc["deletion_date"]]
753
+ rows.append(row)
754
+
755
+ assert len(rows) == 3
756
+
757
+ def test_csv_numeric_formatting(self):
758
+ """Test CSV numeric value formatting"""
759
+ monthly_savings = Decimal("1234.56")
760
+ annual_savings = monthly_savings * 12
761
+
762
+ # Format to 2 decimal places
763
+ monthly_str = f"{monthly_savings:.2f}"
764
+ annual_str = f"{annual_savings:.2f}"
765
+
766
+ assert monthly_str == "1234.56"
767
+ assert annual_str == "14814.72"
768
+
769
+ def test_csv_date_formatting(self, cleanup_sample_vpc_deletion):
770
+ """Test CSV date formatting consistency"""
771
+ deletion_date = cleanup_sample_vpc_deletion["deletion_date"]
772
+
773
+ # Should be YYYY-MM-DD format
774
+ datetime.strptime(deletion_date, "%Y-%m-%d")
775
+
776
+ def test_csv_special_character_handling(self):
777
+ """Test CSV special character escaping"""
778
+ # Values with commas should be quoted
779
+ value_with_comma = "VPC,with,commas"
780
+ escaped = f'"{value_with_comma}"'
781
+
782
+ assert escaped.startswith('"')
783
+ assert escaped.endswith('"')
784
+
785
+ def test_csv_null_value_handling(self):
786
+ """Test CSV null value representation"""
787
+ null_value = None
788
+ csv_representation = "" if null_value is None else str(null_value)
789
+
790
+ assert csv_representation == ""
791
+
792
+ def test_csv_column_order(self, cleanup_valid_config):
793
+ """Test CSV column order is consistent"""
794
+ columns = cleanup_valid_config["output_config"]["csv_columns"]
795
+
796
+ # VPC_ID should be first
797
+ assert columns[0] == "VPC_ID"
798
+
799
+ # Account_ID should be second
800
+ assert columns[1] == "Account_ID"
801
+
802
+ def test_csv_data_quality_indicators(self):
803
+ """Test CSV includes data quality indicators"""
804
+ data_quality = "HIGH"
805
+ confidence_level = "95%"
806
+
807
+ assert data_quality in ["HIGH", "MEDIUM", "LOW"]
808
+ assert "%" in confidence_level
809
+
810
+ def test_csv_output_validation(self):
811
+ """Test CSV output can be parsed back"""
812
+ import csv
813
+ import io
814
+
815
+ # Create sample CSV data
816
+ csv_data = "VPC_ID,Account_ID,Region,Deletion_Date\nvpc-123,123456789012,us-east-1,2025-09-10\n"
817
+
818
+ # Parse it
819
+ reader = csv.DictReader(io.StringIO(csv_data))
820
+ rows = list(reader)
821
+
822
+ assert len(rows) == 1
823
+ assert rows[0]["VPC_ID"] == "vpc-123"
824
+
825
+
826
+ # ========================================
827
+ # Error Handling Tests (15 tests)
828
+ # ========================================
829
+
830
+
831
+ @pytest.mark.unit
832
+ class TestErrorHandling:
833
+ """Test error handling and edge cases"""
834
+
835
+ def test_missing_config_file_error(self):
836
+ """Test error handling for missing config file"""
837
+ with pytest.raises(FileNotFoundError):
838
+ with open("/nonexistent/config.yaml") as f:
839
+ yaml.safe_load(f)
840
+
841
+ def test_invalid_yaml_syntax_error(self):
842
+ """Test error handling for invalid YAML syntax"""
843
+ invalid_yaml = "invalid: yaml: [\n not closed"
844
+
845
+ with pytest.raises(yaml.YAMLError):
846
+ yaml.safe_load(invalid_yaml)
847
+
848
+ def test_missing_required_field_error(self):
849
+ """Test error handling for missing required fields"""
850
+ incomplete_config = {"campaign_metadata": {"campaign_id": "TEST"}}
851
+
852
+ with pytest.raises(KeyError):
853
+ _ = incomplete_config["deleted_vpcs"]
854
+
855
+ def test_invalid_date_format_error(self):
856
+ """Test error handling for invalid date format"""
857
+ invalid_date = "2025/09/10" # Should be YYYY-MM-DD
858
+
859
+ with pytest.raises(ValueError):
860
+ datetime.strptime(invalid_date, "%Y-%m-%d")
861
+
862
+ def test_cost_explorer_api_error_handling(self, cleanup_mock_cost_explorer):
863
+ """Test error handling for Cost Explorer API failures"""
864
+ # Mock API error
865
+ cleanup_mock_cost_explorer.get_cost_and_usage.side_effect = Exception("API Error")
866
+
867
+ with pytest.raises(Exception):
868
+ cleanup_mock_cost_explorer.get_cost_and_usage()
869
+
870
+ def test_invalid_vpc_id_error(self):
871
+ """Test error handling for invalid VPC ID format"""
872
+ invalid_vpc_id = "invalid-id"
873
+ assert not invalid_vpc_id.startswith("vpc-")
874
+
875
+ def test_invalid_account_id_error(self):
876
+ """Test error handling for invalid account ID"""
877
+ invalid_account = "not-numeric"
878
+ assert not invalid_account.isdigit()
879
+
880
+ def test_future_deletion_date_warning(self):
881
+ """Test warning for future deletion dates"""
882
+ future_date = datetime.now() + timedelta(days=30)
883
+ current_date = datetime.now()
884
+
885
+ is_future = future_date > current_date
886
+ assert is_future, "Should detect future deletion date"
887
+
888
+ def test_zero_baseline_months_error(self):
889
+ """Test error handling for zero baseline months"""
890
+ baseline_months = 0
891
+ with pytest.raises(AssertionError):
892
+ assert baseline_months > 0, "Baseline months must be positive"
893
+
894
+ def test_negative_cost_validation(self):
895
+ """Test validation for negative costs (credits)"""
896
+ cost = Decimal("-100.00")
897
+ # Negative costs are valid (credits/refunds)
898
+ assert cost < 0
899
+
900
+ def test_division_by_zero_protection(self):
901
+ """Test protection against division by zero"""
902
+ total_cost = Decimal("0.00")
903
+ num_months = 3
904
+
905
+ # Should handle zero cost gracefully
906
+ average = total_cost / num_months if num_months > 0 else Decimal("0.00")
907
+ assert average == Decimal("0.00")
908
+
909
+ def test_empty_cost_data_handling(self):
910
+ """Test handling of empty cost data"""
911
+ cost_data = []
912
+ assert len(cost_data) == 0
913
+
914
+ # Should handle empty data gracefully
915
+ total = sum(cost_data) if cost_data else Decimal("0.00")
916
+ assert total == Decimal("0.00")
917
+
918
+ def test_permission_denied_error_handling(self):
919
+ """Test error handling for permission denied scenarios"""
920
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
921
+ temp_file = f.name
922
+
923
+ try:
924
+ # Make file read-only
925
+ import os
926
+
927
+ os.chmod(temp_file, 0o444)
928
+
929
+ # Try to write (should fail)
930
+ with pytest.raises(PermissionError):
931
+ with open(temp_file, "w") as f:
932
+ f.write("test")
933
+ finally:
934
+ # Cleanup
935
+ os.chmod(temp_file, 0o644)
936
+ Path(temp_file).unlink(missing_ok=True)
937
+
938
+ def test_network_timeout_handling(self):
939
+ """Test error handling for network timeouts"""
940
+ # Mock timeout scenario
941
+ mock_client = MagicMock()
942
+ mock_client.get_cost_and_usage.side_effect = TimeoutError("Request timed out")
943
+
944
+ with pytest.raises(TimeoutError):
945
+ mock_client.get_cost_and_usage()
946
+
947
+ def test_partial_data_recovery(self):
948
+ """Test partial data recovery when some queries fail"""
949
+ successful_data = [Decimal("100.00"), Decimal("105.00")]
950
+ failed_data = None
951
+
952
+ # Should still process successful data
953
+ total = sum(successful_data)
954
+ assert total > 0
955
+
956
+
957
+ # ========================================
958
+ # Multi-LZ Reusability Tests (8 tests)
959
+ # ========================================
960
+
961
+
962
+ @pytest.mark.integration
963
+ class TestMultiLZReusability:
964
+ """Test framework reusability across multiple Landing Zones"""
965
+
966
+ def test_multi_account_support(self, cleanup_multi_lz_config):
967
+ """Test support for multiple AWS accounts"""
968
+ accounts = {vpc["account_id"] for vpc in cleanup_multi_lz_config["deleted_vpcs"]}
969
+ assert len(accounts) >= 2, "Should support multiple accounts"
970
+
971
+ def test_multi_region_support(self, cleanup_multi_lz_config):
972
+ """Test support for multiple AWS regions"""
973
+ regions = {vpc["region"] for vpc in cleanup_multi_lz_config["deleted_vpcs"]}
974
+ assert len(regions) >= 2, "Should support multiple regions"
975
+
976
+ def test_different_deletion_dates_support(self, cleanup_multi_lz_config):
977
+ """Test support for different deletion dates"""
978
+ dates = {vpc["deletion_date"] for vpc in cleanup_multi_lz_config["deleted_vpcs"]}
979
+ assert len(dates) >= 2, "Should support different deletion dates"
980
+
981
+ def test_different_deletion_principals(self, cleanup_multi_lz_config):
982
+ """Test support for different deletion principals"""
983
+ principals = {vpc["deletion_principal"] for vpc in cleanup_multi_lz_config["deleted_vpcs"]}
984
+ assert len(principals) >= 2, "Should support different deletion principals"
985
+
986
+ def test_campaign_level_metadata(self, cleanup_multi_lz_config):
987
+ """Test campaign-level metadata for multi-LZ"""
988
+ metadata = cleanup_multi_lz_config["campaign_metadata"]
989
+ assert "MULTI-LZ" in metadata["campaign_id"]
990
+ assert "Landing Zone" in metadata.get("description", "")
991
+
992
+ def test_consolidated_output_generation(self, cleanup_multi_lz_config):
993
+ """Test consolidated output for multiple LZs"""
994
+ output_config = cleanup_multi_lz_config["output_config"]
995
+
996
+ # Should have single output files for all LZs
997
+ assert "multi_lz" in output_config["csv_output_file"]
998
+ assert "multi_lz" in output_config["json_results_file"]
999
+
1000
+ def test_per_lz_cost_attribution(self, cleanup_multi_lz_config):
1001
+ """Test per-LZ cost attribution and aggregation"""
1002
+ vpcs = cleanup_multi_lz_config["deleted_vpcs"]
1003
+
1004
+ # Each VPC should have independent attribution
1005
+ for vpc in vpcs:
1006
+ assert "account_id" in vpc
1007
+ assert "region" in vpc
1008
+
1009
+ def test_cross_lz_comparison_support(self, cleanup_multi_lz_config):
1010
+ """Test support for cross-LZ comparison and analysis"""
1011
+ vpcs = cleanup_multi_lz_config["deleted_vpcs"]
1012
+
1013
+ # Group by account for comparison
1014
+ by_account = {}
1015
+ for vpc in vpcs:
1016
+ account = vpc["account_id"]
1017
+ if account not in by_account:
1018
+ by_account[account] = []
1019
+ by_account[account].append(vpc)
1020
+
1021
+ # Should enable comparison across accounts
1022
+ assert len(by_account) > 0