package upstreamimpl import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/dx" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" ) func TestDXBatchDayAvailable(t *testing.T) { now := time.Date(2026, 9, 20, 12, 0, 0, 0, dxBatchLocation) tests := map[string]bool{ "20260913": true, "20260919": true, "20260912": false, "20260920": false, "20260921": false, } for queryDate, want := range tests { if got := dxBatchDayAvailable(queryDate, now); got != want { t.Fatalf("dxBatchDayAvailable(%q) = %v, want %v", queryDate, got, want) } } } func TestDXBatchMonthAvailable(t *testing.T) { now := time.Date(2026, 9, 20, 12, 0, 0, 0, dxBatchLocation) if !dxBatchMonthAvailable("202609", now) { t.Fatal("current month should use telecom batch API") } if dxBatchMonthAvailable("202608", now) || dxBatchMonthAvailable("invalid", now) { t.Fatal("historical or invalid month must not use telecom current-month batch API") } } func TestQuerySimDataUsageMonthlyUsesBatchForCurrentMonth(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) if r.URL.Path != "/api/v1/batchQry/batchQryFlowByMonth" { t.Fatalf("path = %q", r.URL.Path) } var request dxBatchRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatal(err) } if len(request.AccessNumbers) != 2 || request.CustNumber != "cust-1" { t.Fatalf("request = %+v", request) } _, _ = w.Write([]byte(`{"code":0,"msg":"查询成功","data":[{"accessNumber":"138****8001","result":"1","unit":"MB","flowAmount":"12.5"},{"accessNumber":"138****8002","result":"1","unit":"MB","flowAmount":"7.5"}]}`)) })) defer server.Close() sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "secret"}), custNumber: "cust-1"} queryMonth := time.Now().In(dxBatchLocation).Format("200601") output, err := sim.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{ SimKeys: []*model.SimKey{{MSISDN: "138****8001"}, {MSISDN: "138****8002"}}, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: queryMonth}, }) if err != nil { t.Fatal(err) } if calls.Load() != 1 || len(output) != 2 || output[0].ResData.DataAmount != 12.5 || output[1].ResData.DataAmount != 7.5 { t.Fatalf("calls=%d output=%+v", calls.Load(), output) } } func TestQuerySimDataUsageDailyLegacyFallbackIsBounded(t *testing.T) { var active atomic.Int32 var maximum atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { current := active.Add(1) defer active.Add(-1) for { previous := maximum.Load() if current <= previous || maximum.CompareAndSwap(previous, current) { break } } time.Sleep(30 * time.Millisecond) w.Header().Set("Content-Type", "application/xml") _, _ = w.Write([]byte(`0OK1MB`)) })) defer server.Close() sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "secret", Timeout: time.Second})} keys := make([]*model.SimKey, 12) for index := range keys { keys[index] = &model.SimKey{MSISDN: "138****8003"} } output, err := sim.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{ SimKeys: keys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: "20260101"}, }) if err != nil { t.Fatal(err) } if len(output) != len(keys) { t.Fatalf("results = %d, want %d", len(output), len(keys)) } if maximum.Load() > dxLegacyUsageConcurrency || maximum.Load() < 2 { t.Fatalf("maximum concurrency = %d, want 2..%d", maximum.Load(), dxLegacyUsageConcurrency) } } func TestQuerySimDataUsageDailyRejectsMalformedXML(t *testing.T) { cases := []struct { name string body string }{ {name: "missing business node", body: `denied`}, {name: "missing result", body: `12MB`}, {name: "missing amount", body: `0`}, {name: "invalid amount", body: `0invalidMB`}, {name: "non numeric result", body: `ERROR`}, } for _, item := range cases { t.Run(item.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "application/xml") _, _ = writer.Write([]byte(item.body)) })) defer server.Close() sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "test"})} output, err := sim.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{ SimKeys: []*model.SimKey{{ICCID: "audit-card", MSISDN: "138****8001"}}, ReqData: model.SimDataUsageDailyInputItem{QueryDate: "20260101"}, }) if err != nil { t.Fatal(err) } if len(output) != 1 || output[0].Code == 0 { t.Fatalf("malformed response accepted: %+v", output) } }) } } func TestQuerySimDataUsageDailyBatchFailureFallsBackToLegacy(t *testing.T) { var batchCalls atomic.Int32 var singleCalls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.URL.Path == "/api/v1/batchQry/batchQryFlowByDate" { batchCalls.Add(1) writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{"code":403,"msg":"denied"}`)) return } singleCalls.Add(1) writer.Header().Set("Content-Type", "application/xml") _, _ = writer.Write([]byte(`012MB`)) })) defer server.Close() sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "test"}), custNumber: "audit-customer"} queryDate := time.Now().In(dxBatchLocation).AddDate(0, 0, -1).Format("20060102") output, err := sim.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{ SimKeys: []*model.SimKey{{MSISDN: "138****8001"}}, ReqData: model.SimDataUsageDailyInputItem{QueryDate: queryDate}, }) if err != nil { t.Fatal(err) } if batchCalls.Load() != 1 || singleCalls.Load() != 1 { t.Fatalf("batch calls=%d, single calls=%d, want 1 each", batchCalls.Load(), singleCalls.Load()) } if len(output) != 1 || output[0].Code != 0 || output[0].ResData.DataAmount != 12 { t.Fatalf("unexpected fallback output: %+v", output) } } func TestParseDXDataUsage(t *testing.T) { cases := []struct { value string want float64 invalid bool }{ {value: "0MB", want: 0}, {value: "12.5MB", want: 12.5}, {value: " 1 gb ", want: 1024}, {value: "1024KB", want: 1}, {value: "12", want: 12}, {value: "", invalid: true}, {value: "invalidMB", invalid: true}, {value: "-1MB", invalid: true}, {value: "NaNMB", invalid: true}, {value: "InfMB", invalid: true}, {value: "1e308GB", invalid: true}, } for _, item := range cases { t.Run(item.value, func(t *testing.T) { amount, err := parseDXDataUsage(item.value) if (err != nil) != item.invalid || (!item.invalid && amount != item.want) { t.Fatalf("parseDXDataUsage(%q) = %v, %v, want %v, invalid=%v", item.value, amount, err, item.want, item.invalid) } }) } }