package controller import ( "encoding/json" "net/http/httptest" "testing" "time" "ezops/config" "ezops/model" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "gorm.io/gorm" ) // TestCurrencyExpression 验证 CNY 与 USD 报告币种使用同一组可逆汇率。 // Logan.Li func TestCurrencyExpression(t *testing.T) { handler := NewBillingHandler(nil, &config.BillingConfig{ BaseCurrency: "CNY", ExchangeRates: map[string]float64{"cny": 1, "usd": 7.2}, }) _, cnyArgs, ok := handler.currencyExpression("CNY") if !ok || len(cnyArgs) != 2 { t.Fatalf("CNY 汇率表达式无效: ok=%v args=%v", ok, cnyArgs) } metadata := handler.reportCurrencyMetadata("USD") rates := metadata["exchange_rates"].(map[string]float64) if rates["USD"] != 1 || rates["CNY"] != 0.138889 { t.Fatalf("USD 折算汇率错误: %+v", rates) } if _, _, supported := handler.currencyExpression("EUR"); supported { t.Fatal("未配置币种不应被接受") } } // TestBillingCurrencyConversionAPI 验证统计接口不会直接混加不同原币金额。 // Logan.Li func TestBillingCurrencyConversionAPI(t *testing.T) { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&model.CloudBilling{}, &model.BillingAlertRule{}); err != nil { t.Fatal(err) } today := time.Now().Format("2006-01-02") records := []model.CloudBilling{ {Provider: "tencent", BillingDate: today, Cost: 100, Currency: "CNY", Service: "CVM"}, {Provider: "aws", BillingDate: today, Cost: 10, Currency: "USD", Service: "EC2"}, } if err := db.Create(&records).Error; err != nil { t.Fatal(err) } handler := NewBillingHandler(db, &config.BillingConfig{BaseCurrency: "CNY", ExchangeRates: map[string]float64{"CNY": 1, "USD": 7.2}}) assertMonthCost := func(currency string, expected float64) { t.Helper() recorder := httptest.NewRecorder() context, _ := gin.CreateTestContext(recorder) context.Request = httptest.NewRequest("GET", "/?currency="+currency, nil) handler.GetBillingStats(context) var response struct { Code int `json:"code"` Data struct { MonthCost float64 `json:"month_cost"` } `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatal(err) } if response.Code != 200 || mathRound(response.Data.MonthCost, 2) != expected { t.Fatalf("%s 月费用错误: code=%d cost=%f", currency, response.Code, response.Data.MonthCost) } } assertMonthCost("CNY", 172) assertMonthCost("USD", 23.89) } // TestBuildBillingAnalysisMonths 验证空月份补齐和非 Top 分类合并。 // Logan.Li func TestBuildBillingAnalysisMonths(t *testing.T) { start, _ := time.Parse("2006-01", "2026-03") end, _ := time.Parse("2006-01", "2026-04") costs := map[string]map[string]float64{ "2026-03": {"CVM": 100, "TCR": 20}, } months := buildBillingAnalysisMonths(start, end, costs, []string{"CVM", "其他"}) if len(months) != 2 || months[0].Total != 120 || months[0].Items[1].Cost != 20 { t.Fatalf("月度聚合错误: %+v", months) } if months[1].Month != "2026-04" || months[1].Total != 0 { t.Fatalf("空月份未正确补齐: %+v", months[1]) } } // TestBuildBillingAttributions 验证归因变化额与贡献度。 // Logan.Li func TestBuildBillingAttributions(t *testing.T) { items := buildBillingAttributions(map[string]float64{"CVM": 100}, map[string]float64{"CVM": 180, "TCR": 20}) if len(items) != 2 || items[0].Name != "CVM" || items[0].Delta != 80 || items[0].Contribution != 80 { t.Fatalf("费用归因错误: %+v", items) } }