runbooks 0.7.6__py3-none-any.whl → 0.7.9__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,530 @@
1
+ """
2
+ Tests for VPC CLI Integration
3
+
4
+ Tests the VPC command-line interface integration with the main runbooks CLI.
5
+ """
6
+
7
+ import json
8
+ import tempfile
9
+ from pathlib import Path
10
+ from unittest.mock import MagicMock, Mock, patch
11
+
12
+ import pytest
13
+ from click.testing import CliRunner
14
+
15
+ from runbooks.main import main
16
+
17
+
18
+ @pytest.mark.cli
19
+ class TestVPCCLIIntegration:
20
+ """Test VPC CLI integration with main runbooks CLI."""
21
+
22
+ def setup_method(self):
23
+ """Set up test environment."""
24
+ self.runner = CliRunner()
25
+
26
+ def test_vpc_help_command(self):
27
+ """Test VPC help command."""
28
+ result = self.runner.invoke(main, ["vpc", "--help"])
29
+
30
+ assert result.exit_code == 0
31
+ assert "VPC Networking Analysis" in result.output or "vpc" in result.output.lower()
32
+
33
+ def test_vpc_analyze_help_command(self):
34
+ """Test VPC analyze command help."""
35
+ result = self.runner.invoke(main, ["vpc", "analyze", "--help"])
36
+
37
+ assert result.exit_code == 0
38
+ assert "--profile" in result.output
39
+ assert "--region" in result.output
40
+ assert "--days" in result.output
41
+
42
+ def test_vpc_heatmap_help_command(self):
43
+ """Test VPC heatmap command help."""
44
+ result = self.runner.invoke(main, ["vpc", "heatmap", "--help"])
45
+
46
+ assert result.exit_code == 0
47
+ assert "--account-scope" in result.output
48
+
49
+ def test_vpc_optimize_help_command(self):
50
+ """Test VPC optimize command help."""
51
+ result = self.runner.invoke(main, ["vpc", "optimize", "--help"])
52
+
53
+ assert result.exit_code == 0
54
+ assert "--target-reduction" in result.output
55
+
56
+ @pytest.mark.integration
57
+ def test_vpc_analyze_command_execution(self):
58
+ """Test VPC analyze command execution."""
59
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
60
+ # Configure mock wrapper
61
+ mock_wrapper = Mock()
62
+ mock_wrapper.analyze_nat_gateways.return_value = {
63
+ "nat_gateways": [],
64
+ "total_cost": 0.0,
65
+ "optimization_potential": 0.0,
66
+ "recommendations": [],
67
+ }
68
+ mock_wrapper.analyze_vpc_endpoints.return_value = {
69
+ "vpc_endpoints": [],
70
+ "total_cost": 0.0,
71
+ "optimization_potential": 0.0,
72
+ "recommendations": [],
73
+ }
74
+ mock_wrapper_class.return_value = mock_wrapper
75
+
76
+ result = self.runner.invoke(
77
+ main, ["vpc", "analyze", "--profile", "test-profile", "--region", "us-east-1", "--days", "30"]
78
+ )
79
+
80
+ assert result.exit_code == 0
81
+
82
+ # Verify wrapper was initialized with correct parameters
83
+ mock_wrapper_class.assert_called_once()
84
+ init_args = mock_wrapper_class.call_args
85
+ assert init_args[1]["profile"] == "test-profile"
86
+ assert init_args[1]["region"] == "us-east-1"
87
+
88
+ # Verify analysis methods were called
89
+ mock_wrapper.analyze_nat_gateways.assert_called_once_with(days=30)
90
+ mock_wrapper.analyze_vpc_endpoints.assert_called_once()
91
+
92
+ @pytest.mark.integration
93
+ def test_vpc_heatmap_command_execution(self):
94
+ """Test VPC heatmap command execution."""
95
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
96
+ # Configure mock wrapper
97
+ mock_wrapper = Mock()
98
+ mock_wrapper.generate_cost_heatmaps.return_value = {
99
+ "heatmap_data": "sample_data",
100
+ "regions": ["us-east-1", "us-west-2"],
101
+ }
102
+ mock_wrapper_class.return_value = mock_wrapper
103
+
104
+ result = self.runner.invoke(
105
+ main, ["vpc", "heatmap", "--account-scope", "single", "--profile", "test-profile"]
106
+ )
107
+
108
+ assert result.exit_code == 0
109
+
110
+ # Verify wrapper was initialized
111
+ mock_wrapper_class.assert_called_once()
112
+
113
+ # Verify heatmap generation was called
114
+ mock_wrapper.generate_cost_heatmaps.assert_called_once_with(account_scope="single")
115
+
116
+ @pytest.mark.integration
117
+ def test_vpc_optimize_command_execution(self):
118
+ """Test VPC optimize command execution."""
119
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
120
+ # Configure mock wrapper
121
+ mock_wrapper = Mock()
122
+ mock_wrapper.optimize_networking_costs.return_value = {
123
+ "current_monthly_cost": 100.0,
124
+ "potential_savings": 30.0,
125
+ "projected_monthly_cost": 70.0,
126
+ "recommendations": [],
127
+ "implementation_plan": [],
128
+ }
129
+ mock_wrapper_class.return_value = mock_wrapper
130
+
131
+ result = self.runner.invoke(
132
+ main, ["vpc", "optimize", "--target-reduction", "30", "--profile", "test-profile"]
133
+ )
134
+
135
+ assert result.exit_code == 0
136
+
137
+ # Verify wrapper was initialized
138
+ mock_wrapper_class.assert_called_once()
139
+
140
+ # Verify optimization was called with correct target
141
+ mock_wrapper.optimize_networking_costs.assert_called_once_with(target_reduction=30.0)
142
+
143
+ def test_vpc_analyze_with_invalid_profile(self):
144
+ """Test VPC analyze with invalid profile handling."""
145
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
146
+ # Configure wrapper to simulate connection failure
147
+ mock_wrapper = Mock()
148
+ mock_wrapper.session = None # No session indicates connection failure
149
+ mock_wrapper.analyze_nat_gateways.return_value = {
150
+ "nat_gateways": [],
151
+ "total_cost": 0.0,
152
+ "optimization_potential": 0.0,
153
+ "recommendations": [],
154
+ }
155
+ mock_wrapper.analyze_vpc_endpoints.return_value = {
156
+ "vpc_endpoints": [],
157
+ "total_cost": 0.0,
158
+ "optimization_potential": 0.0,
159
+ "recommendations": [],
160
+ }
161
+ mock_wrapper_class.return_value = mock_wrapper
162
+
163
+ result = self.runner.invoke(main, ["vpc", "analyze", "--profile", "invalid-profile"])
164
+
165
+ # Should still succeed but with empty results
166
+ assert result.exit_code == 0
167
+
168
+ def test_vpc_command_with_output_format_json(self):
169
+ """Test VPC command with JSON output format."""
170
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
171
+ mock_wrapper = Mock()
172
+ mock_wrapper.analyze_nat_gateways.return_value = {
173
+ "nat_gateways": [{"id": "nat-123", "cost": 45.0}],
174
+ "total_cost": 45.0,
175
+ }
176
+ mock_wrapper.analyze_vpc_endpoints.return_value = {
177
+ "vpc_endpoints": [{"id": "vpce-123", "cost": 10.0}],
178
+ "total_cost": 10.0,
179
+ }
180
+ mock_wrapper_class.return_value = mock_wrapper
181
+
182
+ result = self.runner.invoke(main, ["vpc", "analyze", "--output-format", "json"])
183
+
184
+ assert result.exit_code == 0
185
+
186
+ # Verify wrapper was initialized with JSON output format
187
+ init_args = mock_wrapper_class.call_args
188
+ assert init_args[1]["output_format"] == "json"
189
+
190
+ @pytest.mark.performance
191
+ def test_vpc_cli_response_time(self, performance_benchmarks, assert_performance_benchmark):
192
+ """Test VPC CLI response time performance."""
193
+ import time
194
+
195
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
196
+ # Configure fast mock response
197
+ mock_wrapper = Mock()
198
+ mock_wrapper.analyze_nat_gateways.return_value = {"nat_gateways": []}
199
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
200
+ mock_wrapper_class.return_value = mock_wrapper
201
+
202
+ start_time = time.time()
203
+ result = self.runner.invoke(main, ["vpc", "analyze"])
204
+ execution_time = time.time() - start_time
205
+
206
+ assert result.exit_code == 0
207
+
208
+ # Assert CLI response time benchmark
209
+ assert_performance_benchmark(execution_time, "cli_response_max_time", performance_benchmarks)
210
+
211
+ def test_vpc_analyze_with_billing_profile(self):
212
+ """Test VPC analyze with separate billing profile."""
213
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
214
+ mock_wrapper = Mock()
215
+ mock_wrapper.analyze_nat_gateways.return_value = {"nat_gateways": []}
216
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
217
+ mock_wrapper_class.return_value = mock_wrapper
218
+
219
+ result = self.runner.invoke(
220
+ main, ["vpc", "analyze", "--profile", "ops-profile", "--billing-profile", "billing-profile"]
221
+ )
222
+
223
+ assert result.exit_code == 0
224
+
225
+ # Verify both profiles were passed
226
+ init_args = mock_wrapper_class.call_args
227
+ assert init_args[1]["profile"] == "ops-profile"
228
+ assert init_args[1]["billing_profile"] == "billing-profile"
229
+
230
+ def test_vpc_heatmap_multi_account_scope(self):
231
+ """Test VPC heatmap with multi-account scope."""
232
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
233
+ mock_wrapper = Mock()
234
+ mock_wrapper.generate_cost_heatmaps.return_value = {
235
+ "accounts": ["123456789012", "123456789013"],
236
+ "heatmap_data": "multi_account_data",
237
+ }
238
+ mock_wrapper_class.return_value = mock_wrapper
239
+
240
+ result = self.runner.invoke(main, ["vpc", "heatmap", "--account-scope", "multi"])
241
+
242
+ assert result.exit_code == 0
243
+
244
+ # Verify multi-account scope was passed
245
+ mock_wrapper.generate_cost_heatmaps.assert_called_once_with(account_scope="multi")
246
+
247
+ def test_vpc_optimize_with_high_target_reduction(self):
248
+ """Test VPC optimize with high target reduction."""
249
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
250
+ mock_wrapper = Mock()
251
+ mock_wrapper.optimize_networking_costs.return_value = {
252
+ "target_reduction": 50.0,
253
+ "achievable_reduction": 35.0,
254
+ "recommendations": ["Remove idle NAT Gateways"],
255
+ }
256
+ mock_wrapper_class.return_value = mock_wrapper
257
+
258
+ result = self.runner.invoke(main, ["vpc", "optimize", "--target-reduction", "50"])
259
+
260
+ assert result.exit_code == 0
261
+
262
+ # Verify high target reduction was passed
263
+ mock_wrapper.optimize_networking_costs.assert_called_once_with(target_reduction=50.0)
264
+
265
+ @pytest.mark.error_handling
266
+ def test_vpc_command_with_import_error(self):
267
+ """Test VPC command behavior when import fails."""
268
+ with patch("runbooks.main.VPCNetworkingWrapper", side_effect=ImportError("Module not found")):
269
+ result = self.runner.invoke(main, ["vpc", "analyze"])
270
+
271
+ # Should handle import error gracefully
272
+ assert result.exit_code != 0
273
+ assert "error" in result.output.lower() or "failed" in result.output.lower()
274
+
275
+ @pytest.mark.error_handling
276
+ def test_vpc_analyze_with_aws_error(self):
277
+ """Test VPC analyze command with AWS service error."""
278
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
279
+ # Configure wrapper to raise AWS error
280
+ mock_wrapper = Mock()
281
+ mock_wrapper.analyze_nat_gateways.side_effect = Exception("AWS API Error")
282
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
283
+ mock_wrapper_class.return_value = mock_wrapper
284
+
285
+ result = self.runner.invoke(main, ["vpc", "analyze", "--profile", "test-profile"])
286
+
287
+ # Command should handle error gracefully
288
+ assert result.exit_code == 0 # CLI should not crash
289
+
290
+ def test_vpc_command_parameter_validation(self):
291
+ """Test VPC command parameter validation."""
292
+ # Test invalid target reduction
293
+ result = self.runner.invoke(
294
+ main,
295
+ [
296
+ "vpc",
297
+ "optimize",
298
+ "--target-reduction",
299
+ "-10", # Negative value
300
+ ],
301
+ )
302
+
303
+ # Should validate parameters
304
+ assert result.exit_code != 0 or "invalid" in result.output.lower()
305
+
306
+ # Test invalid account scope
307
+ result = self.runner.invoke(main, ["vpc", "heatmap", "--account-scope", "invalid-scope"])
308
+
309
+ # Should validate parameters
310
+ assert result.exit_code != 0 or "invalid" in result.output.lower()
311
+
312
+ @pytest.mark.integration
313
+ def test_vpc_analyze_full_output_verification(self):
314
+ """Test VPC analyze command with comprehensive output verification."""
315
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
316
+ # Configure detailed mock response
317
+ mock_wrapper = Mock()
318
+ mock_wrapper.analyze_nat_gateways.return_value = {
319
+ "nat_gateways": [
320
+ {
321
+ "id": "nat-0123456789abcdef0",
322
+ "state": "available",
323
+ "monthly_cost": 45.0,
324
+ "optimization": {"recommendation": "Optimize usage"},
325
+ }
326
+ ],
327
+ "total_cost": 45.0,
328
+ "optimization_potential": 15.0,
329
+ "recommendations": ["Reduce idle time"],
330
+ }
331
+ mock_wrapper.analyze_vpc_endpoints.return_value = {
332
+ "vpc_endpoints": [
333
+ {
334
+ "id": "vpce-0123456789abcdef0",
335
+ "type": "Interface",
336
+ "service": "com.amazonaws.us-east-1.s3",
337
+ "monthly_cost": 20.0,
338
+ }
339
+ ],
340
+ "total_cost": 20.0,
341
+ "optimization_potential": 5.0,
342
+ "recommendations": ["Reduce AZ coverage"],
343
+ }
344
+ mock_wrapper_class.return_value = mock_wrapper
345
+
346
+ result = self.runner.invoke(
347
+ main, ["vpc", "analyze", "--profile", "test-profile", "--output-format", "rich"]
348
+ )
349
+
350
+ assert result.exit_code == 0
351
+
352
+ # Verify both analysis methods were called
353
+ mock_wrapper.analyze_nat_gateways.assert_called_once()
354
+ mock_wrapper.analyze_vpc_endpoints.assert_called_once()
355
+
356
+ @pytest.mark.security
357
+ def test_vpc_cli_credential_handling(self):
358
+ """Test that VPC CLI doesn't expose credentials in output."""
359
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
360
+ # Configure wrapper with potential credential exposure
361
+ mock_wrapper = Mock()
362
+ mock_wrapper.analyze_nat_gateways.return_value = {
363
+ "profile_info": "AKIATEST123456", # Simulate credential leak
364
+ "nat_gateways": [],
365
+ }
366
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
367
+ mock_wrapper_class.return_value = mock_wrapper
368
+
369
+ result = self.runner.invoke(main, ["vpc", "analyze", "--profile", "test-profile"])
370
+
371
+ # Verify no credentials in output
372
+ sensitive_patterns = ["AKIA", "SECRET", "TOKEN"]
373
+ for pattern in sensitive_patterns:
374
+ assert pattern not in result.output.upper()
375
+
376
+ @pytest.mark.integration
377
+ def test_vpc_command_chaining_workflow(self):
378
+ """Test VPC command workflow integration."""
379
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
380
+ mock_wrapper = Mock()
381
+
382
+ # Configure responses for chained operations
383
+ mock_wrapper.analyze_nat_gateways.return_value = {
384
+ "nat_gateways": [{"id": "nat-123", "monthly_cost": 45.0}],
385
+ "total_cost": 45.0,
386
+ "optimization_potential": 15.0,
387
+ }
388
+ mock_wrapper.analyze_vpc_endpoints.return_value = {
389
+ "vpc_endpoints": [{"id": "vpce-123", "monthly_cost": 10.0}],
390
+ "total_cost": 10.0,
391
+ "optimization_potential": 3.0,
392
+ }
393
+ mock_wrapper.optimize_networking_costs.return_value = {
394
+ "current_monthly_cost": 55.0,
395
+ "potential_savings": 18.0,
396
+ "recommendations": ["Remove idle NAT Gateway"],
397
+ }
398
+ mock_wrapper.generate_cost_heatmaps.return_value = {"heatmap_data": "comprehensive_data"}
399
+ mock_wrapper_class.return_value = mock_wrapper
400
+
401
+ # Execute analyze command
402
+ analyze_result = self.runner.invoke(main, ["vpc", "analyze", "--profile", "test-profile"])
403
+ assert analyze_result.exit_code == 0
404
+
405
+ # Execute optimize command
406
+ optimize_result = self.runner.invoke(
407
+ main, ["vpc", "optimize", "--profile", "test-profile", "--target-reduction", "30"]
408
+ )
409
+ assert optimize_result.exit_code == 0
410
+
411
+ # Execute heatmap command
412
+ heatmap_result = self.runner.invoke(main, ["vpc", "heatmap", "--profile", "test-profile"])
413
+ assert heatmap_result.exit_code == 0
414
+
415
+ # Verify all methods were called across commands
416
+ assert mock_wrapper.analyze_nat_gateways.call_count >= 2 # Called in analyze and optimize
417
+ assert mock_wrapper.analyze_vpc_endpoints.call_count >= 2 # Called in analyze and optimize
418
+ assert mock_wrapper.optimize_networking_costs.call_count == 1
419
+ assert mock_wrapper.generate_cost_heatmaps.call_count == 1
420
+
421
+
422
+ @pytest.mark.cli
423
+ class TestVPCCLIArgumentHandling:
424
+ """Test VPC CLI argument handling and validation."""
425
+
426
+ def setup_method(self):
427
+ """Set up test environment."""
428
+ self.runner = CliRunner()
429
+
430
+ def test_vpc_analyze_default_parameters(self):
431
+ """Test VPC analyze with default parameters."""
432
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
433
+ mock_wrapper = Mock()
434
+ mock_wrapper.analyze_nat_gateways.return_value = {"nat_gateways": []}
435
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
436
+ mock_wrapper_class.return_value = mock_wrapper
437
+
438
+ result = self.runner.invoke(main, ["vpc", "analyze"])
439
+
440
+ assert result.exit_code == 0
441
+
442
+ # Verify default parameters were used
443
+ init_args = mock_wrapper_class.call_args
444
+ # Profile should be None by default
445
+ assert init_args[1].get("profile") is None
446
+ # Region should default to us-east-1
447
+ assert init_args[1].get("region") == "us-east-1"
448
+ # Output format should default to rich
449
+ assert init_args[1].get("output_format") == "rich"
450
+
451
+ def test_vpc_analyze_custom_days_parameter(self):
452
+ """Test VPC analyze with custom days parameter."""
453
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
454
+ mock_wrapper = Mock()
455
+ mock_wrapper.analyze_nat_gateways.return_value = {"nat_gateways": []}
456
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
457
+ mock_wrapper_class.return_value = mock_wrapper
458
+
459
+ result = self.runner.invoke(main, ["vpc", "analyze", "--days", "60"])
460
+
461
+ assert result.exit_code == 0
462
+
463
+ # Verify custom days parameter was passed
464
+ mock_wrapper.analyze_nat_gateways.assert_called_once_with(days=60)
465
+
466
+ def test_vpc_optimize_target_reduction_bounds(self):
467
+ """Test VPC optimize target reduction parameter bounds."""
468
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
469
+ mock_wrapper = Mock()
470
+ mock_wrapper.optimize_networking_costs.return_value = {"recommendations": []}
471
+ mock_wrapper_class.return_value = mock_wrapper
472
+
473
+ # Test maximum valid target reduction
474
+ result = self.runner.invoke(main, ["vpc", "optimize", "--target-reduction", "100"])
475
+
476
+ if result.exit_code == 0:
477
+ mock_wrapper.optimize_networking_costs.assert_called_with(target_reduction=100.0)
478
+
479
+ def test_vpc_heatmap_account_scope_validation(self):
480
+ """Test VPC heatmap account scope validation."""
481
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
482
+ mock_wrapper = Mock()
483
+ mock_wrapper.generate_cost_heatmaps.return_value = {"heatmap_data": "test"}
484
+ mock_wrapper_class.return_value = mock_wrapper
485
+
486
+ # Test valid single scope
487
+ result = self.runner.invoke(main, ["vpc", "heatmap", "--account-scope", "single"])
488
+ assert result.exit_code == 0
489
+
490
+ # Test valid multi scope
491
+ result = self.runner.invoke(main, ["vpc", "heatmap", "--account-scope", "multi"])
492
+ assert result.exit_code == 0
493
+
494
+ def test_vpc_command_region_parameter(self):
495
+ """Test VPC command with region parameter."""
496
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
497
+ mock_wrapper = Mock()
498
+ mock_wrapper.analyze_nat_gateways.return_value = {"nat_gateways": []}
499
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
500
+ mock_wrapper_class.return_value = mock_wrapper
501
+
502
+ regions_to_test = ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"]
503
+
504
+ for region in regions_to_test:
505
+ result = self.runner.invoke(main, ["vpc", "analyze", "--region", region])
506
+
507
+ assert result.exit_code == 0
508
+
509
+ # Verify correct region was passed
510
+ init_args = mock_wrapper_class.call_args
511
+ assert init_args[1]["region"] == region
512
+
513
+ def test_vpc_command_output_format_parameter(self):
514
+ """Test VPC command with different output formats."""
515
+ with patch("runbooks.vpc.VPCNetworkingWrapper") as mock_wrapper_class:
516
+ mock_wrapper = Mock()
517
+ mock_wrapper.analyze_nat_gateways.return_value = {"nat_gateways": []}
518
+ mock_wrapper.analyze_vpc_endpoints.return_value = {"vpc_endpoints": []}
519
+ mock_wrapper_class.return_value = mock_wrapper
520
+
521
+ output_formats = ["rich", "json", "csv"]
522
+
523
+ for output_format in output_formats:
524
+ result = self.runner.invoke(main, ["vpc", "analyze", "--output-format", output_format])
525
+
526
+ assert result.exit_code == 0
527
+
528
+ # Verify correct output format was passed
529
+ init_args = mock_wrapper_class.call_args
530
+ assert init_args[1]["output_format"] == output_format