diff --git a/data/seed/aws_pricelist_seed.json b/data/seed/aws_pricelist_seed.json index dfc4bc9..315ce57 100644 --- a/data/seed/aws_pricelist_seed.json +++ b/data/seed/aws_pricelist_seed.json @@ -68,7 +68,6 @@ {"vendor": "aws", "service": "AmazonALB", "region": "us-east-1", "usage_metric": "ALB-LCU-NewConnections", "unit": "LCU-Hours", "price_usd": 0.008, "source": "seed", "description": "ALB LCU new connections pricing ($0.008 per LCU-hour, 1 LCU = 25 new connections/sec)"}, {"vendor": "aws", "service": "AmazonALB", "region": "us-east-1", "usage_metric": "ALB-LCU-ActiveConnections", "unit": "LCU-Hours", "price_usd": 0.008, "source": "seed", "description": "ALB LCU active connections pricing ($0.008 per LCU-hour, 1 LCU = 3000 active)"}, {"vendor": "aws", "service": "AmazonALB", "region": "us-east-1", "usage_metric": "ALB-LCU-RuleEvaluations", "unit": "LCU-Hours", "price_usd": 0.008, "source": "seed", "description": "ALB LCU rule evaluations pricing ($0.008 per LCU-hour, 1 LCU = 1000 rules/sec)"}, - {"vendor": "aws", "service": "AmazonCloudWatch", "region": "us-east-1", "usage_metric": "CloudWatch-Log-Storage", "unit": "GB-Mo", "price_usd": 0.03, "source": "seed", "description": "CloudWatch Logs storage pricing ($0.03 per GB-month stored)"}, {"vendor": "aws", "service": "AmazonVPC", "region": "us-east-1", "usage_metric": "NAT-Gateway-Hour", "unit": "Hours", "price_usd": 0.045, "source": "seed", "description": "NAT Gateway hourly pricing ($0.045 per NAT-hour)"}, {"vendor": "aws", "service": "AmazonVPC", "region": "us-east-1", "usage_metric": "NAT-Gateway-DataProcessed", "unit": "GB", "price_usd": 0.045, "source": "seed", "description": "NAT Gateway data processing pricing ($0.045 per GB processed)"}, {"vendor": "aws", "service": "AmazonVPC", "region": "us-east-1", "usage_metric": "VPC-Endpoint-Hour", "unit": "Hours", "price_usd": 0.01, "source": "seed", "description": "VPC Interface Endpoint hourly pricing ($0.01 per ENI-hour)"}, diff --git a/infra_cost_model/pricing/sources/infracost.py b/infra_cost_model/pricing/sources/infracost.py index 6911fc9..87b78ea 100644 --- a/infra_cost_model/pricing/sources/infracost.py +++ b/infra_cost_model/pricing/sources/infracost.py @@ -79,7 +79,7 @@ def _to_float(value) -> Optional[float]: "eu-west-1": "EU", "eu-west-2": "EUW2", "eu-west-3": "EUW3", "eu-central-1": "EUC1", "eu-central-2": "EUC2", "eu-north-1": "EUN1", "eu-south-1": "EUS1", "eu-south-2": "EUS2", - "ap-southeast-1": "APS1", "ap-southeast-2": "APS2", "ap-southeast-3": "APS3", + "ap-southeast-1": "APS1", "ap-southeast-2": "APS2", "ap-southeast-3": "APS4", "ap-south-1": "APS3", "ap-south-2": "APS5", "ap-northeast-1": "APN1", "ap-northeast-2": "APN2", "ap-northeast-3": "APN3", "ap-east-1": "APE1", diff --git a/infra_cost_model/resources/registry.py b/infra_cost_model/resources/registry.py index c9db70e..32b20fb 100644 --- a/infra_cost_model/resources/registry.py +++ b/infra_cost_model/resources/registry.py @@ -78,8 +78,7 @@ def _infer_provider(cls, resource_type: Type[ResourceType]) -> Optional[str]: "alb": "aws", "networking": "aws", "cloudwatch": "aws", - "rds": "aws", - "rds": "aws", "misc_services": "aws", + "misc_services": "aws", "kms": "aws", "data_transfer": "aws", "gcp": "gcp", "azure": "azure", diff --git a/tests/test_infracost_client.py b/tests/test_infracost_client.py index 8177f1e..9677fe1 100644 --- a/tests/test_infracost_client.py +++ b/tests/test_infracost_client.py @@ -416,3 +416,23 @@ def test_regionless_usagetype_unknown_region_upserts_nothing(monkeypatch): cache, "DataTransfer-Internet-Out-GB", "moon-base-1") assert n == 0 cache.upsert.assert_not_called() + + +# --- Region usagetype prefix map integrity ------------------------------------- + +def test_region_usagetype_prefixes_are_unique(): + """Each region must map to a DISTINCT usagetype prefix — a collision makes two + regions' prices indistinguishable in the usagetype filters (e.g. the data- + transfer region-pair / regionless sync).""" + from collections import Counter + from infra_cost_model.pricing.sources.infracost import _REGION_PREFIX + dupes = {v: n for v, n in Counter(_REGION_PREFIX.values()).items() if n > 1} + assert not dupes, f"duplicate region usagetype prefixes: {dupes}" + + +def test_ap_region_prefixes_match_aws_codes(): + """AWS assigns usage-type region codes by launch order, not name.""" + from infra_cost_model.pricing.sources.infracost import _REGION_PREFIX + assert _REGION_PREFIX["ap-south-1"] == "APS3" # Mumbai + assert _REGION_PREFIX["ap-southeast-3"] == "APS4" # Jakarta + assert _REGION_PREFIX["ap-south-2"] == "APS5" # Hyderabad diff --git a/tests/test_pricing.py b/tests/test_pricing.py index 5206295..f11ae4a 100644 --- a/tests/test_pricing.py +++ b/tests/test_pricing.py @@ -276,3 +276,18 @@ def test_seed_prices_is_idempotent(): assert count_once > 0 assert count_twice == count_once + + +def test_seed_pricelist_has_no_duplicate_rows(): + """The bundled seed list must not carry duplicate price rows: SQLite treats + seed rows (purchase_option=NULL) as distinct under the UNIQUE constraint, so a + dup would load twice and a single logical price could return as a spurious + multi-tier TieredPrice.""" + import json + from collections import Counter + from infra_cost_model.pricing.cache import SEED_PRICES_PATH + rows = json.loads(SEED_PRICES_PATH.read_text()) + keys = [(r["vendor"], r["service"], r["region"], r["usage_metric"], + r.get("start_usage_amount")) for r in rows] + dupes = {k: n for k, n in Counter(keys).items() if n > 1} + assert not dupes, f"duplicate seed rows: {dupes}"