"""Regression: Tencent ``DescribeBillDetail`` cost lives in ``ComponentSet[].RealCost``, NOT a top-level ``RealTotalCost``. Prod symptom (腾讯云 账单/概览, all periods): 期内总支出 = 0, the by-account ranking empty, ``cmdb_cost_facts`` had zero Tencent rows — even though the connector authenticated (``last_test_status=ok``) and the API returned full line items (service names like "COS 对象存储" parsed fine). 136 ``cmdb_cloud_billing_daily`` rows existed for the active scope, every one with ``total_cost_usd=0``. Root cause: both driver billing methods read ``getattr(row, "RealTotalCost") or getattr(row, "TotalCost") or "0"``. A ``DescribeBillDetail`` ``BillDetail`` object has NEITHER field — that name belongs to a different API (``DescribeBillSummaryByProduct`` → ``SummaryOverview``). ``getattr`` returned ``None`` for every row → "0". The real amount is the sum of ``BillDetail.ComponentSet[].RealCost``. These tests use the REAL SDK shape (ComponentSet; no RealTotalCost) and fail against the pre-fix reader. The final test pins that a stray top-level ``RealTotalCost`` is IGNORED, so the bug can't silently return. """ # ruff: noqa: N802, N803, N815 from __future__ import annotations from datetime import date import pytest from app.integrations.driver_base import CloudScopeRef from app.integrations.drivers.tencent_cloud import ( TencentCloudDriver, _bill_detail_real_cost_cny, ) _CFG = { "secret_id": "AK-test", "secret_key": "SK-test", "region": "ap-guangzhou", "account_mode": "iam", } def _driver() -> TencentCloudDriver: return TencentCloudDriver(integration_id="tc-test", config=_CFG) def _scope() -> CloudScopeRef: return CloudScopeRef( scope_type="standalone_iam", external_id="100002610019", display_name="tencent-test", ) class _Comp: """A ``BillDetailComponent`` — strings, possibly ``None``/empty.""" def __init__(self, RealCost=None, Cost=None): self.RealCost = RealCost self.Cost = Cost class _Tag: def __init__(self, TagKey: str, TagValue: str): self.TagKey = TagKey self.TagValue = TagValue class _Row: """A ``DescribeBillDetail`` ``BillDetail`` row in its REAL shape: cost only in ``ComponentSet``, the rest top-level dimensions.""" def __init__( self, PayTime: str, components: list[_Comp], *, BusinessCodeName: str = "云服务器 CVM", ProductCodeName: str = "CVM", ResourceId: str = "ins-abc", ResourceName: str = "web-01", RegionName: str = "广州", ActionType: str = "包年包月新购", ProjectName: str = "默认项目", Tags: list | None = None, ): self.PayTime = PayTime self.ComponentSet = components self.BusinessCodeName = BusinessCodeName self.ProductCodeName = ProductCodeName self.ResourceId = ResourceId self.ResourceName = ResourceName self.RegionName = RegionName self.ActionType = ActionType self.ProjectName = ProjectName self.Tags = Tags or [] class _Resp: def __init__(self, **fields): self.__dict__.update(fields) class _BillStub: """One ``DescribeBillDetail`` page of rows (single in-month window).""" def __init__(self, rows: list[_Row]): self._rows = rows def DescribeBillDetail(self, req): return _Resp(DetailSet=self._rows, TotalCount=len(self._rows)) # ── the pure helper ────────────────────────────────────────────────── def test_helper_sums_components_real_cost(): row = _Row("2026-05-20 10:00:00", [_Comp("3.50"), _Comp("1.25")]) assert _bill_detail_real_cost_cny(row) == pytest.approx(4.75) def test_helper_reads_realcost_only_missing_contributes_zero(): # A component with no/empty RealCost contributes 0 — we never substitute # the list-price ``Cost`` (that would over-report on discounted accounts # by blending pre/post-discount numbers in one row). row = _Row( "2026-05-20 10:00:00", [_Comp(RealCost=None, Cost="2.00"), _Comp(RealCost="", Cost="0.50"), _Comp(RealCost="1.00", Cost="9.99")], ) assert _bill_detail_real_cost_cny(row) == pytest.approx(1.00) def test_helper_zero_when_no_components(): row = _Row("2026-05-20 10:00:00", []) assert _bill_detail_real_cost_cny(row) == 0.0 def test_helper_ignores_phantom_top_level_realtotalcost(): """Guard against reverting to the buggy reader: even if a row carries a stray top-level ``RealTotalCost``/``TotalCost``, the helper reads only ``ComponentSet`` — the amount must come from components.""" row = _Row("2026-05-20 10:00:00", [_Comp("7.00")]) row.RealTotalCost = "999.00" # phantom — must be ignored row.TotalCost = "888.00" assert _bill_detail_real_cost_cny(row) == pytest.approx(7.00) # ── billing_daily end-to-end ───────────────────────────────────────── @pytest.mark.asyncio async def test_billing_daily_uses_component_real_cost(): stub = _BillStub([ _Row("2026-05-20 10:00:00", [_Comp("3.00"), _Comp("2.00")], BusinessCodeName="对象存储 COS"), _Row("2026-05-20 12:00:00", [_Comp("4.00")], BusinessCodeName="对象存储 COS"), _Row("2026-05-21 09:00:00", [_Comp("10.00")], BusinessCodeName="内容分发 CDN"), ]) driver = _driver() driver._billing_client = lambda: stub # type: ignore[method-assign] daily = await driver.list_billing_daily( date(2026, 5, 20), date(2026, 5, 21), scope=_scope(), ) by_day = {r.date: r.total_cost_usd for r in daily} # CNY despite name assert by_day == { date(2026, 5, 20): pytest.approx(9.00), # 3+2+4 date(2026, 5, 21): pytest.approx(10.00), } # Per-service breakdown also driven by the real component cost. d20 = next(r for r in daily if r.date == date(2026, 5, 20)) assert d20.cost_by_service["对象存储 COS"] == pytest.approx(9.00) assert d20.currency == "CNY" @pytest.mark.asyncio async def test_billing_daily_nonzero_total_regression(): """The exact prod failure: a row with components but no top-level cost field must NOT roll up to zero.""" stub = _BillStub([_Row("2026-05-15 10:00:00", [_Comp("123.45")])]) driver = _driver() driver._billing_client = lambda: stub # type: ignore[method-assign] daily = await driver.list_billing_daily( date(2026, 5, 15), date(2026, 5, 15), scope=_scope(), ) assert len(daily) == 1 assert daily[0].total_cost_usd == pytest.approx(123.45) assert daily[0].total_cost_usd != 0 # ── cost_facts end-to-end ──────────────────────────────────────────── @pytest.mark.asyncio async def test_cost_facts_uses_component_real_cost_and_converts(): stub = _BillStub([ _Row( "2026-05-20 10:00:00", [_Comp("14.00"), _Comp("0.10")], BusinessCodeName="对象存储 COS", ProductCodeName="COS", ResourceId="cos-bucket-1", RegionName="广州", Tags=[_Tag("team", "infra")], ), ]) driver = _driver() driver._billing_client = lambda: stub # type: ignore[method-assign] facts = await driver.list_cost_facts( date(2026, 5, 20), date(2026, 5, 20), scope=_scope(), cny_per_usd=7.0, ) assert len(facts) == 1 f = facts[0] assert f.amount_native == pytest.approx(14.10) # 14.00 + 0.10 assert f.amount_usd == pytest.approx(14.10 / 7.0) assert f.currency == "CNY" assert f.service == "对象存储 COS" assert f.resource_id == "cos-bucket-1" assert f.tags == {"team": "infra"} @pytest.mark.asyncio async def test_cost_facts_drops_only_genuinely_zero_rows(): """A real zero-cost line (free tier / 精度差异) is still dropped, but a row with a positive component is kept (pre-fix: ALL rows were dropped because every native parsed to 0).""" stub = _BillStub([ _Row("2026-05-20 10:00:00", [_Comp("0")], BusinessCodeName="月度计费精度差异"), _Row("2026-05-20 11:00:00", [_Comp("5.00")], BusinessCodeName="对象存储 COS"), ]) driver = _driver() driver._billing_client = lambda: stub # type: ignore[method-assign] facts = await driver.list_cost_facts( date(2026, 5, 20), date(2026, 5, 20), scope=_scope(), cny_per_usd=7.0, ) assert len(facts) == 1 assert facts[0].service == "对象存储 COS" assert facts[0].amount_native == pytest.approx(5.00)