2026-01-31 19:11:25 +08:00
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
from src.services.billing.formula_engine import (
|
|
|
|
|
|
BillingIncompleteError,
|
|
|
|
|
|
FormulaEngine,
|
|
|
|
|
|
UnsafeExpressionError,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSafeExpression:
|
|
|
|
|
|
def test_reject_attribute_access(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
with pytest.raises(UnsafeExpressionError):
|
|
|
|
|
|
engine.evaluate(
|
|
|
|
|
|
expression="(1).__class__",
|
|
|
|
|
|
variables={},
|
|
|
|
|
|
dimensions={},
|
|
|
|
|
|
dimension_mappings={},
|
|
|
|
|
|
strict_mode=True, # 确保抛出异常,便于断言类型
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_reject_import(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
with pytest.raises(UnsafeExpressionError):
|
|
|
|
|
|
engine.evaluate(
|
|
|
|
|
|
expression="__import__('os').system('echo hacked')",
|
|
|
|
|
|
variables={},
|
|
|
|
|
|
dimensions={},
|
|
|
|
|
|
dimension_mappings={},
|
|
|
|
|
|
strict_mode=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFormulaEngine:
|
|
|
|
|
|
def test_basic_video_formula(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
result = engine.evaluate(
|
|
|
|
|
|
expression="(base_price + duration_seconds * price_per_second) * resolution_multiplier",
|
|
|
|
|
|
variables={"base_price": 0.05, "price_per_second": 0.02},
|
|
|
|
|
|
dimensions={"duration_seconds": 10, "resolution": "720p"},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"duration_seconds": {
|
|
|
|
|
|
"source": "dimension",
|
|
|
|
|
|
"key": "duration_seconds",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"resolution_multiplier": {
|
|
|
|
|
|
"source": "matrix",
|
|
|
|
|
|
"key": "resolution",
|
|
|
|
|
|
"map": {"720p": 1.0, "1080p": 1.5},
|
|
|
|
|
|
"default": 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert result.status == "complete"
|
|
|
|
|
|
assert result.missing_required == []
|
2026-02-03 18:48:39 +08:00
|
|
|
|
assert abs(float(result.cost) - 0.25) < 1e-9
|
2026-01-31 19:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
def test_required_dimension_missing_non_strict(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
result = engine.evaluate(
|
|
|
|
|
|
expression="base_price + duration_seconds * price_per_second",
|
|
|
|
|
|
variables={"base_price": 0.05, "price_per_second": 0.02},
|
|
|
|
|
|
dimensions={"duration_seconds": None},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"duration_seconds": {
|
|
|
|
|
|
"source": "dimension",
|
|
|
|
|
|
"key": "duration_seconds",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
strict_mode=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert result.status == "incomplete"
|
2026-02-03 18:48:39 +08:00
|
|
|
|
assert float(result.cost) == 0.0
|
2026-01-31 19:11:25 +08:00
|
|
|
|
assert result.missing_required == ["duration_seconds"]
|
|
|
|
|
|
|
|
|
|
|
|
def test_required_dimension_missing_strict_raises(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
with pytest.raises(BillingIncompleteError) as exc:
|
|
|
|
|
|
engine.evaluate(
|
|
|
|
|
|
expression="base_price + duration_seconds * price_per_second",
|
|
|
|
|
|
variables={"base_price": 0.05, "price_per_second": 0.02},
|
|
|
|
|
|
dimensions={"duration_seconds": None},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"duration_seconds": {
|
|
|
|
|
|
"source": "dimension",
|
|
|
|
|
|
"key": "duration_seconds",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
strict_mode=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert exc.value.missing_required == ["duration_seconds"]
|
|
|
|
|
|
|
|
|
|
|
|
def test_allow_zero(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
|
|
|
|
|
|
# allow_zero=false(默认):0 视为缺失
|
|
|
|
|
|
result = engine.evaluate(
|
|
|
|
|
|
expression="duration_seconds * 1",
|
|
|
|
|
|
variables={},
|
|
|
|
|
|
dimensions={"duration_seconds": 0},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"duration_seconds": {
|
|
|
|
|
|
"source": "dimension",
|
|
|
|
|
|
"key": "duration_seconds",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert result.status == "incomplete"
|
|
|
|
|
|
assert result.missing_required == ["duration_seconds"]
|
|
|
|
|
|
|
|
|
|
|
|
# allow_zero=true:0 合法
|
|
|
|
|
|
result = engine.evaluate(
|
|
|
|
|
|
expression="duration_seconds * 1",
|
|
|
|
|
|
variables={},
|
|
|
|
|
|
dimensions={"duration_seconds": 0},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"duration_seconds": {
|
|
|
|
|
|
"source": "dimension",
|
|
|
|
|
|
"key": "duration_seconds",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
"allow_zero": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert result.status == "complete"
|
2026-02-03 18:48:39 +08:00
|
|
|
|
assert float(result.cost) == 0.0
|
2026-01-31 19:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
def test_tiered_mapping(self) -> None:
|
|
|
|
|
|
engine = FormulaEngine()
|
|
|
|
|
|
|
|
|
|
|
|
result = engine.evaluate(
|
|
|
|
|
|
expression="input_price",
|
|
|
|
|
|
variables={},
|
|
|
|
|
|
dimensions={"total_input_tokens": 100_000},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"input_price": {
|
|
|
|
|
|
"source": "tiered",
|
|
|
|
|
|
"tier_key": "total_input_tokens",
|
|
|
|
|
|
"tiers": [
|
|
|
|
|
|
{"up_to": 128_000, "value": 3.0},
|
|
|
|
|
|
{"up_to": None, "value": 1.5},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert result.status == "complete"
|
2026-02-03 18:48:39 +08:00
|
|
|
|
assert float(result.cost) == 3.0
|
2026-01-31 19:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
result = engine.evaluate(
|
|
|
|
|
|
expression="input_price",
|
|
|
|
|
|
variables={},
|
|
|
|
|
|
dimensions={"total_input_tokens": 200_000},
|
|
|
|
|
|
dimension_mappings={
|
|
|
|
|
|
"input_price": {
|
|
|
|
|
|
"source": "tiered",
|
|
|
|
|
|
"tier_key": "total_input_tokens",
|
|
|
|
|
|
"tiers": [
|
|
|
|
|
|
{"up_to": 128_000, "value": 3.0},
|
|
|
|
|
|
{"up_to": None, "value": 1.5},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert result.status == "complete"
|
2026-02-03 18:48:39 +08:00
|
|
|
|
assert float(result.cost) == 1.5
|