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":"13300000001","result":"1","unit":"MB","flowAmount":"12.5"},{"accessNumber":"13300000002","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: "13300000001"}, {MSISDN: "13300000002"}},
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: "13300000000"}
}
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: "13300000001"}},
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: "13300000001"}},
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)
}
})
}
}