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.
- runbooks/__init__.py +1 -1
- runbooks/base.py +5 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +871 -0
- runbooks/cfat/assessment/runner.py +122 -11
- runbooks/cfat/models.py +6 -2
- runbooks/common/logger.py +14 -0
- runbooks/common/rich_utils.py +451 -0
- runbooks/enterprise/__init__.py +68 -0
- runbooks/enterprise/error_handling.py +411 -0
- runbooks/enterprise/logging.py +439 -0
- runbooks/enterprise/multi_tenant.py +583 -0
- runbooks/finops/README.md +468 -241
- runbooks/finops/__init__.py +39 -3
- runbooks/finops/cli.py +83 -18
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +812 -164
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +485 -51
- runbooks/finops/optimizer.py +823 -0
- runbooks/finops/tests/__init__.py +19 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
- runbooks/finops/tests/run_comprehensive_tests.py +421 -0
- runbooks/finops/tests/run_tests.py +305 -0
- runbooks/finops/tests/test_finops_dashboard.py +705 -0
- runbooks/finops/tests/test_integration.py +477 -0
- runbooks/finops/tests/test_performance.py +380 -0
- runbooks/finops/tests/test_performance_benchmarks.py +500 -0
- runbooks/finops/tests/test_reference_images_validation.py +867 -0
- runbooks/finops/tests/test_single_account_features.py +715 -0
- runbooks/finops/tests/validate_test_suite.py +220 -0
- runbooks/finops/types.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +725 -0
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +442 -0
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- runbooks/inventory/discovery.md +1 -1
- runbooks/inventory/list_ec2_instances.py +18 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1269 -0
- runbooks/inventory/rich_inventory_display.py +393 -0
- runbooks/inventory/run_on_multi_accounts.py +35 -19
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/vpc_flow_analyzer.py +1030 -0
- runbooks/main.py +2215 -119
- runbooks/metrics/dora_metrics_engine.py +599 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +122 -10
- runbooks/operate/deployment_framework.py +1032 -0
- runbooks/operate/deployment_validator.py +853 -0
- runbooks/operate/dynamodb_operations.py +10 -6
- runbooks/operate/ec2_operations.py +319 -11
- runbooks/operate/executive_dashboard.py +779 -0
- runbooks/operate/mcp_integration.py +750 -0
- runbooks/operate/nat_gateway_operations.py +1120 -0
- runbooks/operate/networking_cost_heatmap.py +685 -0
- runbooks/operate/privatelink_operations.py +940 -0
- runbooks/operate/s3_operations.py +10 -6
- runbooks/operate/vpc_endpoints.py +644 -0
- runbooks/operate/vpc_operations.py +1038 -0
- runbooks/remediation/__init__.py +2 -2
- runbooks/remediation/acm_remediation.py +1 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/cloudtrail_remediation.py +1 -1
- runbooks/remediation/cognito_remediation.py +1 -1
- runbooks/remediation/dynamodb_remediation.py +1 -1
- runbooks/remediation/ec2_remediation.py +1 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
- runbooks/remediation/kms_enable_key_rotation.py +1 -1
- runbooks/remediation/kms_remediation.py +1 -1
- runbooks/remediation/lambda_remediation.py +1 -1
- runbooks/remediation/multi_account.py +1 -1
- runbooks/remediation/rds_remediation.py +1 -1
- runbooks/remediation/s3_block_public_access.py +1 -1
- runbooks/remediation/s3_enable_access_logging.py +1 -1
- runbooks/remediation/s3_encryption.py +1 -1
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/remediation/vpc_remediation.py +475 -0
- runbooks/security/__init__.py +3 -1
- runbooks/security/compliance_automation.py +632 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +31 -5
- runbooks/security/security_baseline_tester.py +169 -30
- runbooks/security/security_export.py +477 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +484 -0
- runbooks/validation/cli.py +356 -0
- runbooks/validation/mcp_validator.py +768 -0
- runbooks/vpc/__init__.py +38 -0
- runbooks/vpc/config.py +212 -0
- runbooks/vpc/cost_engine.py +347 -0
- runbooks/vpc/heatmap_engine.py +605 -0
- runbooks/vpc/manager_interface.py +634 -0
- runbooks/vpc/networking_wrapper.py +1260 -0
- runbooks/vpc/rich_formatters.py +679 -0
- runbooks/vpc/tests/__init__.py +5 -0
- runbooks/vpc/tests/conftest.py +356 -0
- runbooks/vpc/tests/test_cli_integration.py +530 -0
- runbooks/vpc/tests/test_config.py +458 -0
- runbooks/vpc/tests/test_cost_engine.py +479 -0
- runbooks/vpc/tests/test_networking_wrapper.py +512 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,479 @@
|
|
1
|
+
"""
|
2
|
+
Tests for Networking Cost Engine
|
3
|
+
|
4
|
+
Tests the core cost calculation and analysis logic for VPC networking components.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import time
|
8
|
+
from datetime import datetime, timedelta
|
9
|
+
from unittest.mock import Mock, patch
|
10
|
+
|
11
|
+
import boto3
|
12
|
+
import pytest
|
13
|
+
|
14
|
+
from runbooks.vpc.config import VPCNetworkingConfig
|
15
|
+
from runbooks.vpc.cost_engine import NetworkingCostEngine
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.mark.unit
|
19
|
+
class TestNetworkingCostEngine:
|
20
|
+
"""Test Networking Cost Engine functionality."""
|
21
|
+
|
22
|
+
def test_initialization_default(self):
|
23
|
+
"""Test cost engine initialization with defaults."""
|
24
|
+
engine = NetworkingCostEngine()
|
25
|
+
|
26
|
+
assert engine.session is not None
|
27
|
+
assert isinstance(engine.config, VPCNetworkingConfig)
|
28
|
+
assert engine.cost_model is not None
|
29
|
+
assert engine._cost_explorer_client is None
|
30
|
+
assert engine._cloudwatch_client is None
|
31
|
+
|
32
|
+
def test_initialization_with_parameters(self, vpc_test_config):
|
33
|
+
"""Test cost engine initialization with parameters."""
|
34
|
+
mock_session = Mock(spec=boto3.Session)
|
35
|
+
|
36
|
+
engine = NetworkingCostEngine(session=mock_session, config=vpc_test_config)
|
37
|
+
|
38
|
+
assert engine.session == mock_session
|
39
|
+
assert engine.config == vpc_test_config
|
40
|
+
assert engine.cost_model == vpc_test_config.cost_model
|
41
|
+
|
42
|
+
def test_lazy_client_loading(self, networking_cost_engine):
|
43
|
+
"""Test lazy loading of AWS clients."""
|
44
|
+
# Initially, clients should be None
|
45
|
+
assert networking_cost_engine._cost_explorer_client is None
|
46
|
+
assert networking_cost_engine._cloudwatch_client is None
|
47
|
+
|
48
|
+
# Mock the session client method
|
49
|
+
mock_ce_client = Mock()
|
50
|
+
mock_cw_client = Mock()
|
51
|
+
|
52
|
+
networking_cost_engine.session.client.side_effect = lambda service, **kwargs: {
|
53
|
+
"ce": mock_ce_client,
|
54
|
+
"cloudwatch": mock_cw_client,
|
55
|
+
}.get(service)
|
56
|
+
|
57
|
+
# Access properties should create clients
|
58
|
+
ce_client = networking_cost_engine.cost_explorer
|
59
|
+
cw_client = networking_cost_engine.cloudwatch
|
60
|
+
|
61
|
+
assert ce_client == mock_ce_client
|
62
|
+
assert cw_client == mock_cw_client
|
63
|
+
assert networking_cost_engine._cost_explorer_client == mock_ce_client
|
64
|
+
assert networking_cost_engine._cloudwatch_client == mock_cw_client
|
65
|
+
|
66
|
+
@pytest.mark.performance
|
67
|
+
def test_nat_gateway_cost_calculation_performance(
|
68
|
+
self, networking_cost_engine, performance_benchmarks, assert_performance_benchmark
|
69
|
+
):
|
70
|
+
"""Test NAT Gateway cost calculation performance."""
|
71
|
+
# Mock CloudWatch response
|
72
|
+
mock_cloudwatch = Mock()
|
73
|
+
mock_cloudwatch.get_metric_statistics.return_value = {
|
74
|
+
"Datapoints": [
|
75
|
+
{
|
76
|
+
"Sum": 5368709120.0, # 5 GB in bytes
|
77
|
+
"Average": 100.0,
|
78
|
+
"Maximum": 150.0,
|
79
|
+
}
|
80
|
+
]
|
81
|
+
}
|
82
|
+
|
83
|
+
networking_cost_engine._cloudwatch_client = mock_cloudwatch
|
84
|
+
|
85
|
+
start_time = time.time()
|
86
|
+
result = networking_cost_engine.calculate_nat_gateway_cost("nat-0123456789abcdef0", days=30)
|
87
|
+
execution_time = time.time() - start_time
|
88
|
+
|
89
|
+
# Assert performance benchmark
|
90
|
+
assert_performance_benchmark(execution_time, "cost_calculation_max_time", performance_benchmarks)
|
91
|
+
|
92
|
+
# Validate result structure
|
93
|
+
assert "nat_gateway_id" in result
|
94
|
+
assert "base_cost" in result
|
95
|
+
assert "data_processing_cost" in result
|
96
|
+
assert "total_cost" in result
|
97
|
+
|
98
|
+
def test_nat_gateway_cost_calculation_basic(self, networking_cost_engine):
|
99
|
+
"""Test basic NAT Gateway cost calculation without data processing."""
|
100
|
+
result = networking_cost_engine.calculate_nat_gateway_cost(
|
101
|
+
"nat-0123456789abcdef0", days=30, include_data_processing=False
|
102
|
+
)
|
103
|
+
|
104
|
+
# Validate basic cost calculation
|
105
|
+
expected_base_cost = 0.045 * 24 * 30 # hourly rate * hours * days
|
106
|
+
|
107
|
+
assert result["nat_gateway_id"] == "nat-0123456789abcdef0"
|
108
|
+
assert result["period_days"] == 30
|
109
|
+
assert result["base_cost"] == expected_base_cost
|
110
|
+
assert result["data_processing_cost"] == 0.0
|
111
|
+
assert result["total_cost"] == expected_base_cost
|
112
|
+
assert result["daily_average"] == expected_base_cost / 30
|
113
|
+
assert result["monthly_projection"] == expected_base_cost
|
114
|
+
|
115
|
+
def test_nat_gateway_cost_calculation_with_data_processing(self, networking_cost_engine):
|
116
|
+
"""Test NAT Gateway cost calculation with data processing."""
|
117
|
+
# Mock CloudWatch response with data processing metrics
|
118
|
+
mock_cloudwatch = Mock()
|
119
|
+
mock_cloudwatch.get_metric_statistics.return_value = {
|
120
|
+
"Datapoints": [
|
121
|
+
{
|
122
|
+
"Sum": 10737418240.0, # 10 GB in bytes
|
123
|
+
"Average": 100.0,
|
124
|
+
"Maximum": 150.0,
|
125
|
+
}
|
126
|
+
]
|
127
|
+
}
|
128
|
+
|
129
|
+
networking_cost_engine._cloudwatch_client = mock_cloudwatch
|
130
|
+
|
131
|
+
result = networking_cost_engine.calculate_nat_gateway_cost(
|
132
|
+
"nat-0123456789abcdef0", days=30, include_data_processing=True
|
133
|
+
)
|
134
|
+
|
135
|
+
# Validate cost calculation with data processing
|
136
|
+
expected_base_cost = 0.045 * 24 * 30
|
137
|
+
expected_data_cost = 10.0 * 0.045 # 10 GB * $0.045/GB
|
138
|
+
expected_total = expected_base_cost + expected_data_cost
|
139
|
+
|
140
|
+
assert result["base_cost"] == expected_base_cost
|
141
|
+
assert result["data_processing_cost"] == expected_data_cost
|
142
|
+
assert result["total_cost"] == expected_total
|
143
|
+
assert result["monthly_projection"] == expected_total
|
144
|
+
|
145
|
+
def test_nat_gateway_cost_with_cloudwatch_error(self, networking_cost_engine):
|
146
|
+
"""Test NAT Gateway cost calculation when CloudWatch fails."""
|
147
|
+
# Mock CloudWatch to raise exception
|
148
|
+
mock_cloudwatch = Mock()
|
149
|
+
mock_cloudwatch.get_metric_statistics.side_effect = Exception("CloudWatch API Error")
|
150
|
+
|
151
|
+
networking_cost_engine._cloudwatch_client = mock_cloudwatch
|
152
|
+
|
153
|
+
result = networking_cost_engine.calculate_nat_gateway_cost(
|
154
|
+
"nat-0123456789abcdef0", days=30, include_data_processing=True
|
155
|
+
)
|
156
|
+
|
157
|
+
# Should still calculate base cost, but no data processing cost
|
158
|
+
expected_base_cost = 0.045 * 24 * 30
|
159
|
+
|
160
|
+
assert result["base_cost"] == expected_base_cost
|
161
|
+
assert result["data_processing_cost"] == 0.0
|
162
|
+
assert result["total_cost"] == expected_base_cost
|
163
|
+
|
164
|
+
def test_vpc_endpoint_cost_calculation_interface(self, networking_cost_engine):
|
165
|
+
"""Test VPC Endpoint cost calculation for Interface endpoints."""
|
166
|
+
result = networking_cost_engine.calculate_vpc_endpoint_cost(
|
167
|
+
endpoint_type="Interface", availability_zones=2, data_processed_gb=100.0
|
168
|
+
)
|
169
|
+
|
170
|
+
# Validate Interface endpoint cost calculation
|
171
|
+
expected_base_cost = 10.0 * 2 # $10/month * 2 AZs
|
172
|
+
expected_data_cost = 100.0 * 0.01 # 100 GB * $0.01/GB
|
173
|
+
expected_total = expected_base_cost + expected_data_cost
|
174
|
+
|
175
|
+
assert result["endpoint_type"] == "Interface"
|
176
|
+
assert result["availability_zones"] == 2
|
177
|
+
assert result["data_processed_gb"] == 100.0
|
178
|
+
assert result["base_cost"] == expected_base_cost
|
179
|
+
assert result["data_processing_cost"] == expected_data_cost
|
180
|
+
assert result["total_monthly_cost"] == expected_total
|
181
|
+
|
182
|
+
def test_vpc_endpoint_cost_calculation_gateway(self, networking_cost_engine):
|
183
|
+
"""Test VPC Endpoint cost calculation for Gateway endpoints."""
|
184
|
+
result = networking_cost_engine.calculate_vpc_endpoint_cost(
|
185
|
+
endpoint_type="Gateway", availability_zones=0, data_processed_gb=1000.0
|
186
|
+
)
|
187
|
+
|
188
|
+
# Gateway endpoints are always free
|
189
|
+
assert result["endpoint_type"] == "Gateway"
|
190
|
+
assert result["base_cost"] == 0.0
|
191
|
+
assert result["data_processing_cost"] == 0.0
|
192
|
+
assert result["total_monthly_cost"] == 0.0
|
193
|
+
|
194
|
+
def test_transit_gateway_cost_calculation(self, networking_cost_engine):
|
195
|
+
"""Test Transit Gateway cost calculation."""
|
196
|
+
result = networking_cost_engine.calculate_transit_gateway_cost(attachments=5, data_processed_gb=500.0, days=30)
|
197
|
+
|
198
|
+
# Validate Transit Gateway cost calculation
|
199
|
+
expected_base_cost = 0.05 * 24 * 30 # $0.05/hour * 24h * 30 days
|
200
|
+
expected_attachment_cost = 0.05 * 24 * 30 * 5 # $0.05/hour * 24h * 30 days * 5 attachments
|
201
|
+
expected_data_cost = 500.0 * 0.02 # 500 GB * $0.02/GB
|
202
|
+
expected_total = expected_base_cost + expected_attachment_cost + expected_data_cost
|
203
|
+
expected_monthly = expected_total # Already calculated for 30 days
|
204
|
+
|
205
|
+
assert result["attachments"] == 5
|
206
|
+
assert result["data_processed_gb"] == 500.0
|
207
|
+
assert result["base_cost"] == expected_base_cost
|
208
|
+
assert result["attachment_cost"] == expected_attachment_cost
|
209
|
+
assert result["data_processing_cost"] == expected_data_cost
|
210
|
+
assert result["total_cost"] == expected_total
|
211
|
+
assert result["monthly_projection"] == expected_monthly
|
212
|
+
|
213
|
+
def test_elastic_ip_cost_calculation(self, networking_cost_engine):
|
214
|
+
"""Test Elastic IP cost calculation."""
|
215
|
+
result = networking_cost_engine.calculate_elastic_ip_cost(
|
216
|
+
idle_hours=720, # 30 days
|
217
|
+
remaps=5,
|
218
|
+
)
|
219
|
+
|
220
|
+
# Validate Elastic IP cost calculation
|
221
|
+
expected_idle_cost = 720 * 0.005 # 720 hours * $0.005/hour
|
222
|
+
expected_remap_cost = 5 * 0.10 # 5 remaps * $0.10/remap
|
223
|
+
expected_total = expected_idle_cost + expected_remap_cost
|
224
|
+
expected_monthly = expected_total # 720 hours = 30 days
|
225
|
+
|
226
|
+
assert result["idle_hours"] == 720
|
227
|
+
assert result["remaps"] == 5
|
228
|
+
assert result["idle_cost"] == expected_idle_cost
|
229
|
+
assert result["remap_cost"] == expected_remap_cost
|
230
|
+
assert result["total_cost"] == expected_total
|
231
|
+
assert result["monthly_projection"] == expected_monthly
|
232
|
+
|
233
|
+
def test_elastic_ip_cost_with_no_idle_time(self, networking_cost_engine):
|
234
|
+
"""Test Elastic IP cost calculation with no idle time."""
|
235
|
+
result = networking_cost_engine.calculate_elastic_ip_cost(idle_hours=0, remaps=3)
|
236
|
+
|
237
|
+
expected_remap_cost = 3 * 0.10
|
238
|
+
|
239
|
+
assert result["idle_cost"] == 0.0
|
240
|
+
assert result["remap_cost"] == expected_remap_cost
|
241
|
+
assert result["total_cost"] == expected_remap_cost
|
242
|
+
assert result["monthly_projection"] == expected_remap_cost
|
243
|
+
|
244
|
+
def test_data_transfer_cost_calculation(self, networking_cost_engine):
|
245
|
+
"""Test data transfer cost calculation."""
|
246
|
+
result = networking_cost_engine.calculate_data_transfer_cost(
|
247
|
+
inter_az_gb=1000.0, inter_region_gb=500.0, internet_out_gb=200.0
|
248
|
+
)
|
249
|
+
|
250
|
+
# Validate data transfer cost calculation
|
251
|
+
expected_inter_az = 1000.0 * 0.01 # 1000 GB * $0.01/GB
|
252
|
+
expected_inter_region = 500.0 * 0.02 # 500 GB * $0.02/GB
|
253
|
+
expected_internet_out = 200.0 * 0.09 # 200 GB * $0.09/GB
|
254
|
+
expected_total = expected_inter_az + expected_inter_region + expected_internet_out
|
255
|
+
|
256
|
+
assert result["inter_az_gb"] == 1000.0
|
257
|
+
assert result["inter_region_gb"] == 500.0
|
258
|
+
assert result["internet_out_gb"] == 200.0
|
259
|
+
assert result["inter_az_cost"] == expected_inter_az
|
260
|
+
assert result["inter_region_cost"] == expected_inter_region
|
261
|
+
assert result["internet_out_cost"] == expected_internet_out
|
262
|
+
assert result["total_cost"] == expected_total
|
263
|
+
|
264
|
+
def test_get_actual_costs_from_cost_explorer(self, networking_cost_engine, mock_cost_explorer_responses):
|
265
|
+
"""Test getting actual costs from Cost Explorer."""
|
266
|
+
# Mock Cost Explorer response
|
267
|
+
mock_ce = Mock()
|
268
|
+
mock_ce.get_cost_and_usage.return_value = mock_cost_explorer_responses["vpc_costs"]
|
269
|
+
|
270
|
+
networking_cost_engine._cost_explorer_client = mock_ce
|
271
|
+
|
272
|
+
result = networking_cost_engine.get_actual_costs_from_cost_explorer(
|
273
|
+
service="Amazon Virtual Private Cloud",
|
274
|
+
start_date="2024-01-01",
|
275
|
+
end_date="2024-01-31",
|
276
|
+
granularity="MONTHLY",
|
277
|
+
)
|
278
|
+
|
279
|
+
# Validate Cost Explorer result
|
280
|
+
assert result["service"] == "Amazon Virtual Private Cloud"
|
281
|
+
assert result["granularity"] == "MONTHLY"
|
282
|
+
assert result["total_cost"] == 145.67
|
283
|
+
assert len(result["results_by_time"]) == 1
|
284
|
+
|
285
|
+
# Validate API call
|
286
|
+
mock_ce.get_cost_and_usage.assert_called_once()
|
287
|
+
call_args = mock_ce.get_cost_and_usage.call_args[1]
|
288
|
+
assert call_args["TimePeriod"]["Start"] == "2024-01-01"
|
289
|
+
assert call_args["TimePeriod"]["End"] == "2024-01-31"
|
290
|
+
assert call_args["Granularity"] == "MONTHLY"
|
291
|
+
|
292
|
+
def test_cost_explorer_error_handling(self, networking_cost_engine):
|
293
|
+
"""Test Cost Explorer error handling."""
|
294
|
+
# Mock Cost Explorer to raise exception
|
295
|
+
mock_ce = Mock()
|
296
|
+
mock_ce.get_cost_and_usage.side_effect = Exception("Cost Explorer API Error")
|
297
|
+
|
298
|
+
networking_cost_engine._cost_explorer_client = mock_ce
|
299
|
+
|
300
|
+
result = networking_cost_engine.get_actual_costs_from_cost_explorer(
|
301
|
+
service="Amazon Virtual Private Cloud", start_date="2024-01-01", end_date="2024-01-31"
|
302
|
+
)
|
303
|
+
|
304
|
+
# Should return error result
|
305
|
+
assert result["service"] == "Amazon Virtual Private Cloud"
|
306
|
+
assert "error" in result
|
307
|
+
assert result["total_cost"] == 0.0
|
308
|
+
|
309
|
+
def test_estimate_optimization_savings(self, networking_cost_engine):
|
310
|
+
"""Test optimization savings estimation."""
|
311
|
+
current_costs = {"nat_gateways": 200.0, "vpc_endpoints": 50.0, "elastic_ips": 10.0}
|
312
|
+
|
313
|
+
optimization_scenarios = [
|
314
|
+
{
|
315
|
+
"name": "Conservative",
|
316
|
+
"description": "Remove idle resources",
|
317
|
+
"reductions": {
|
318
|
+
"nat_gateways": 20, # 20% reduction
|
319
|
+
"elastic_ips": 50, # 50% reduction
|
320
|
+
},
|
321
|
+
"risk_level": "low",
|
322
|
+
"effort": "low",
|
323
|
+
},
|
324
|
+
{
|
325
|
+
"name": "Aggressive",
|
326
|
+
"description": "Comprehensive optimization",
|
327
|
+
"reductions": {
|
328
|
+
"nat_gateways": 40, # 40% reduction
|
329
|
+
"vpc_endpoints": 25, # 25% reduction
|
330
|
+
"elastic_ips": 80, # 80% reduction
|
331
|
+
},
|
332
|
+
"risk_level": "high",
|
333
|
+
"effort": "high",
|
334
|
+
},
|
335
|
+
]
|
336
|
+
|
337
|
+
result = networking_cost_engine.estimate_optimization_savings(current_costs, optimization_scenarios)
|
338
|
+
|
339
|
+
# Validate savings estimation
|
340
|
+
assert result["current_monthly_cost"] == 260.0 # Sum of current costs
|
341
|
+
assert len(result["scenarios"]) == 2
|
342
|
+
|
343
|
+
# Validate conservative scenario
|
344
|
+
conservative = next(s for s in result["scenarios"] if s["name"] == "Conservative")
|
345
|
+
expected_conservative_savings = (200.0 * 0.20) + (10.0 * 0.50) # 40 + 5 = 45
|
346
|
+
assert conservative["monthly_savings"] == expected_conservative_savings
|
347
|
+
assert conservative["annual_savings"] == expected_conservative_savings * 12
|
348
|
+
|
349
|
+
# Validate aggressive scenario
|
350
|
+
aggressive = next(s for s in result["scenarios"] if s["name"] == "Aggressive")
|
351
|
+
expected_aggressive_savings = (200.0 * 0.40) + (50.0 * 0.25) + (10.0 * 0.80) # 80 + 12.5 + 8 = 100.5
|
352
|
+
assert aggressive["monthly_savings"] == expected_aggressive_savings
|
353
|
+
|
354
|
+
# Validate recommended scenario (should be the one with maximum savings)
|
355
|
+
assert result["recommended_scenario"]["name"] == "Aggressive"
|
356
|
+
assert result["maximum_savings"] == expected_aggressive_savings
|
357
|
+
|
358
|
+
def test_estimate_optimization_with_no_scenarios(self, networking_cost_engine):
|
359
|
+
"""Test optimization estimation with no scenarios."""
|
360
|
+
current_costs = {"nat_gateways": 100.0}
|
361
|
+
optimization_scenarios = []
|
362
|
+
|
363
|
+
result = networking_cost_engine.estimate_optimization_savings(current_costs, optimization_scenarios)
|
364
|
+
|
365
|
+
assert result["current_monthly_cost"] == 100.0
|
366
|
+
assert len(result["scenarios"]) == 0
|
367
|
+
assert result["recommended_scenario"] is None
|
368
|
+
assert result["maximum_savings"] == 0.0
|
369
|
+
|
370
|
+
def test_estimate_optimization_with_empty_costs(self, networking_cost_engine):
|
371
|
+
"""Test optimization estimation with empty current costs."""
|
372
|
+
current_costs = {}
|
373
|
+
optimization_scenarios = [{"name": "Test", "reductions": {"nonexistent": 50}}]
|
374
|
+
|
375
|
+
result = networking_cost_engine.estimate_optimization_savings(current_costs, optimization_scenarios)
|
376
|
+
|
377
|
+
assert result["current_monthly_cost"] == 0.0
|
378
|
+
assert len(result["scenarios"]) == 1
|
379
|
+
assert result["scenarios"][0]["monthly_savings"] == 0.0
|
380
|
+
|
381
|
+
@pytest.mark.integration
|
382
|
+
def test_cost_calculation_consistency(self, networking_cost_engine):
|
383
|
+
"""Test consistency across different cost calculation methods."""
|
384
|
+
# Calculate costs using different methods and ensure consistency
|
385
|
+
|
386
|
+
# NAT Gateway costs for different periods
|
387
|
+
nat_cost_30_days = networking_cost_engine.calculate_nat_gateway_cost(
|
388
|
+
"nat-test", days=30, include_data_processing=False
|
389
|
+
)
|
390
|
+
|
391
|
+
nat_cost_60_days = networking_cost_engine.calculate_nat_gateway_cost(
|
392
|
+
"nat-test", days=60, include_data_processing=False
|
393
|
+
)
|
394
|
+
|
395
|
+
# Daily average should be consistent
|
396
|
+
daily_30 = nat_cost_30_days["daily_average"]
|
397
|
+
daily_60 = nat_cost_60_days["daily_average"]
|
398
|
+
|
399
|
+
assert abs(daily_30 - daily_60) < 0.01, "Daily averages should be consistent across periods"
|
400
|
+
|
401
|
+
# Total cost should scale linearly
|
402
|
+
expected_60_day_cost = nat_cost_30_days["base_cost"] * 2
|
403
|
+
assert abs(nat_cost_60_days["base_cost"] - expected_60_day_cost) < 0.01
|
404
|
+
|
405
|
+
@pytest.mark.performance
|
406
|
+
def test_cost_engine_memory_efficiency(self, networking_cost_engine):
|
407
|
+
"""Test cost engine memory efficiency with large datasets."""
|
408
|
+
import sys
|
409
|
+
|
410
|
+
initial_size = sys.getsizeof(networking_cost_engine.__dict__)
|
411
|
+
|
412
|
+
# Perform multiple calculations
|
413
|
+
for i in range(100):
|
414
|
+
networking_cost_engine.calculate_nat_gateway_cost(f"nat-{i}", days=30, include_data_processing=False)
|
415
|
+
|
416
|
+
final_size = sys.getsizeof(networking_cost_engine.__dict__)
|
417
|
+
|
418
|
+
# Memory usage should not grow significantly
|
419
|
+
growth = final_size - initial_size
|
420
|
+
assert growth < 10000, f"Memory usage grew too much: {growth} bytes"
|
421
|
+
|
422
|
+
@pytest.mark.security
|
423
|
+
def test_cost_calculation_input_validation(self, networking_cost_engine):
|
424
|
+
"""Test cost calculation input validation and sanitization."""
|
425
|
+
# Test with potentially malicious inputs
|
426
|
+
malicious_inputs = [
|
427
|
+
"nat-0123456789abcdef0; rm -rf /",
|
428
|
+
"nat-0123456789abcdef0 && malicious_command",
|
429
|
+
"nat-$(curl malicious-site.com)",
|
430
|
+
]
|
431
|
+
|
432
|
+
for malicious_input in malicious_inputs:
|
433
|
+
# Should not raise exception and should handle input safely
|
434
|
+
result = networking_cost_engine.calculate_nat_gateway_cost(
|
435
|
+
malicious_input, days=30, include_data_processing=False
|
436
|
+
)
|
437
|
+
|
438
|
+
# Verify malicious input is stored as-is but doesn't cause execution
|
439
|
+
assert result["nat_gateway_id"] == malicious_input
|
440
|
+
assert isinstance(result["total_cost"], (int, float))
|
441
|
+
|
442
|
+
@pytest.mark.integration
|
443
|
+
def test_full_cost_analysis_workflow(self, networking_cost_engine):
|
444
|
+
"""Test complete cost analysis workflow."""
|
445
|
+
# Mock all required services
|
446
|
+
mock_cloudwatch = Mock()
|
447
|
+
mock_cost_explorer = Mock()
|
448
|
+
|
449
|
+
# Configure CloudWatch mock
|
450
|
+
mock_cloudwatch.get_metric_statistics.return_value = {
|
451
|
+
"Datapoints": [{"Sum": 5368709120.0, "Average": 100.0, "Maximum": 150.0}]
|
452
|
+
}
|
453
|
+
|
454
|
+
# Configure Cost Explorer mock
|
455
|
+
mock_cost_explorer.get_cost_and_usage.return_value = {
|
456
|
+
"ResultsByTime": [
|
457
|
+
{
|
458
|
+
"TimePeriod": {"Start": "2024-01-01", "End": "2024-01-31"},
|
459
|
+
"Total": {"BlendedCost": {"Amount": "123.45", "Unit": "USD"}},
|
460
|
+
}
|
461
|
+
]
|
462
|
+
}
|
463
|
+
|
464
|
+
networking_cost_engine._cloudwatch_client = mock_cloudwatch
|
465
|
+
networking_cost_engine._cost_explorer_client = mock_cost_explorer
|
466
|
+
|
467
|
+
# Execute full workflow
|
468
|
+
nat_costs = networking_cost_engine.calculate_nat_gateway_cost("nat-test", days=30)
|
469
|
+
vpc_costs = networking_cost_engine.calculate_vpc_endpoint_cost("Interface", 2, 100.0)
|
470
|
+
actual_costs = networking_cost_engine.get_actual_costs_from_cost_explorer("VPC", "2024-01-01", "2024-01-31")
|
471
|
+
|
472
|
+
# Validate workflow results
|
473
|
+
assert nat_costs["total_cost"] > 0
|
474
|
+
assert vpc_costs["total_monthly_cost"] > 0
|
475
|
+
assert actual_costs["total_cost"] == 123.45
|
476
|
+
|
477
|
+
# Validate cost relationships
|
478
|
+
total_calculated = nat_costs["total_cost"] + vpc_costs["total_monthly_cost"]
|
479
|
+
assert total_calculated > 0
|