Files
iotlcm/internal/logic/upstreamimpl/sim_dx_v3_test.go
T
MiFaZhan 96b1edff45 chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。

- 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件),
  yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方
  gogf/gf/contrib/registry/consul/v2(API 兼容)
- Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan
- 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除,
  测试断言按当前实现语义重写(不改产品行为)
- 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、
  内网域名、真实手机号、注释里的真实 token/transid
- 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档
- Dockerfile 改用公共基础镜像,移除写死的 registry 凭据
- 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
2026-10-22 16:08:06 +08:00

469 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package upstreamimpl
import (
"context"
"encoding/json"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/dx"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func newDXV3TestClient(t *testing.T, handler http.HandlerFunc) *sSimDXV3 {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
return &sSimDXV3{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test-key", AppSecret: "test-secret", UppercaseSign: true}), custNbr: "test-customer"}
}
func TestDXV3TemplateCoexistsWithV1(t *testing.T) {
for _, key := range []string{"sim_dx_v1", "sim_dx_v3"} {
if _, ok := service.UpstreamAPI().GetTemp(key); !ok {
t.Fatalf("missing template %s", key)
}
}
factory := &sSimDXV3{}
config := &model.SimChannelConfigItemCache{Channel: &entity.TSimChannels{AppKey: "key", AppSecret: "secret", ChanConfig: `{"custNumber":"customer"}`}, Template: &entity.TSimChannelTemplate{TempConfig: `{"api":{"baseUrl":"https://cmp-api.ctwing.cn:20164"}}`}}
instance, ok := factory.NewTemp(context.Background(), config).(*sSimDXV3)
if !ok || instance.enableOperations || instance.custNbr != "customer" {
t.Fatalf("unexpected v3 configuration: %+v", instance)
}
config.Channel.ChanConfig = ""
if factory.NewTemp(context.Background(), config) == nil {
t.Fatal("single-card configuration should not require custNbr")
}
config.Template.TempConfig = `{"api":{"baseUrl":"https://cmp-api.ctwing.cn:20164/api/v3"}}`
if factory.NewTemp(context.Background(), config) != nil {
t.Fatal("accepted base URL containing protocol path")
}
}
func TestDXV3SessionAndRealTimeUsageContracts(t *testing.T) {
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("msisdn") != "106123" || len(r.URL.Query()) != 1 {
t.Errorf("unexpected query: %s", r.URL.RawQuery)
}
want, _ := dx.BuildSignV3(r.URL.RawQuery, "", "test-secret", r.Header.Get("Timestamp"))
if r.Header.Get("Sign") != want || r.Header.Get("AppKey") != "test-key" {
t.Error("unexpected signed request")
}
switch r.URL.Path {
case "/api/v3/getOnlineStatus":
fmt.Fprint(w, `{"code":"0","result":{"onlineStatus":"1","apnName":"iot.apn","ipv4Address":"192.0.2.1"}}`)
case "/api/v3/getDataUsage":
fmt.Fprint(w, `{"code":0,"result":{"dataAmount":"12.5"}}`)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
})
keys := []*model.SimKey{{MSISDN: "106123", ICCID: "8986", IMSI: "46011"}}
session, err := client.QuerySimSession(context.Background(), &model.SimSessionInput{SimKeys: keys})
if err != nil || len(session) != 1 || session[0].Code != 0 || session[0].ResData.Status != consts.CardSessionStatusOnline || session[0].ResData.ApnId != "iot.apn" {
t.Fatalf("session: %+v, %v", session, err)
}
usage, err := client.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: keys})
if err != nil || usage[0].Code != 0 || usage[0].ResData.DataUsage != 12.5 {
t.Fatalf("usage: %+v, %v", usage, err)
}
}
func TestDXV3ErrorsNeverBecomeZeroUsage(t *testing.T) {
for _, test := range []struct {
name, body, code string
httpStatus int
}{
{"missing code", `{"result":{"dataAmount":"0"}}`, "", 200},
{"null code", `{"code":null,"result":{"dataAmount":"0"}}`, "", 200},
{"mismatched card", `{"code":"0","result":{"msisdn":"2","dataAmount":"5"}}`, "", 200},
{"invalid amount", `{"code":"0","result":{"dataAmount":"garbage"}}`, "", 200},
{"missing result", `{"code":"0","result":null}`, "", 200},
{"business failure", `{"code":"S00004","message":"limited"}`, "S00004", 200},
{"gateway failure", `{}`, "S00004", 799},
} {
t.Run(test.name, func(t *testing.T) {
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
if test.httpStatus == 799 {
w.Header().Set("gw-errorCode", "S00004")
}
w.WriteHeader(test.httpStatus)
fmt.Fprint(w, test.body)
})
items, err := client.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{MSISDN: "1"}}})
if err != nil || items[0].Code == 0 || items[0].ErrCode != test.code {
t.Fatalf("unexpected error result: %+v, %v", items[0], err)
}
})
}
}
func TestDXV3HistoryDayAndDiagnosticMappings(t *testing.T) {
day := time.Now().In(dxBatchLocation).AddDate(0, 0, -1).Format("20060102")
calls := 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
calls++
switch r.URL.Path {
case "/api/v3/getDataUsageBetweenDate":
if r.URL.Query().Get("beginDate") != day || r.URL.Query().Get("endDate") != day {
t.Error("history day must use the same beginning and ending date")
}
fmt.Fprint(w, `{"code":"0","result":{"totalUsageAmount":"4.5"}}`)
case "/api/v3/getApnInfo":
fmt.Fprint(w, `{"code":"0","result":{"apnInfoList":[{"apnName":"iot","status":"1"}]}}`)
case "/api/v3/getSimBindingInfo":
fmt.Fprint(w, `{"code":"0","result":{"isOrdered":"0","bindType":"2","simSeparationStatus":"0","bindImei":"123"}}`)
case "/api/v3/getSimRegionLimitStatus":
fmt.Fprint(w, `{"code":"0","result":{"orderStatus":0,"limitType":3,"regionLimitStatus":"0","restrictedArea":"110000"}}`)
case "/api/v3/getDevicePowerStatus":
// 网关实测只接受 GET(文档写 POST 与网关不符),参数走 query。
if r.Method != "GET" || r.URL.Query().Get("msisdn") != "1" {
t.Errorf("invalid power-status request: %s %s", r.Method, r.URL.RawQuery)
}
fmt.Fprint(w, `{"code":"0","result":{"status":"0"}}`)
default:
t.Errorf("unexpected diagnostic path: %s", r.URL.Path)
}
})
ctx := context.Background()
keys := []*model.SimKey{{MSISDN: "1"}}
usage, err := client.QuerySimDataUsageDaily(ctx, &model.SimDataUsageDailyInput{SimKeys: keys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: day}})
if err != nil || usage[0].Code != 0 || usage[0].ResData.QueryDate != day || usage[0].ResData.DataAmount != 4.5 {
t.Fatalf("history day: %+v, %v", usage, err)
}
today := time.Now().In(dxBatchLocation).Format("20060102")
usage, _ = client.QuerySimDataUsageDaily(ctx, &model.SimDataUsageDailyInput{SimKeys: keys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: today}})
if usage[0].Code == 0 || calls != 1 {
t.Fatal("unfinished day reached the historical endpoint")
}
apns, _ := client.QuerySimApnInfo(ctx, &model.SimApnInfoInput{SimKeys: keys})
if apns[0].Code != 0 || len(apns[0].ResData.ApnList) != 1 || apns[0].ResData.ApnList[0].Status != "1" {
t.Fatalf("APN mapping: %+v", apns)
}
bind, _ := client.QuerySimCardBindStatus(ctx, &model.SimCardBindStatusInput{SimKeys: keys})
if bind[0].Code != 0 || bind[0].ResData.BindStatus != "0" || bind[0].ResData.OrderStatus != "0" || bind[0].ResData.CardBindingType != "2" || bind[0].ResData.BindImei != "123" {
t.Fatalf("binding mapping: %+v", bind)
}
region, _ := client.QuerySimRegionLimitStatus(ctx, &model.SimRegionLimitStatusInput{SimKeys: keys})
if region[0].Code != 0 || region[0].ResData.RegionLimitStatus != "1" || region[0].ResData.LimitType != "3" || region[0].ResData.LimitArea != "110000" {
t.Fatalf("region mapping: %+v", region)
}
power, _ := client.QuerySimDeviceStatus(ctx, &model.SimDeviceStatusInput{SimKeys: keys})
if power[0].Code != 0 || power[0].ResData.OperatingStatus != "1" || power[0].ResData.DeviceStatus != consts.CardSessionStatusUnknown {
t.Fatalf("power mapping: %+v", power)
}
}
// TestDXV3DevicePowerStatusSupportsEmptyRecordShape 锁定该接口的真实返回形态:
// 仅 3G 接口对 4G/5G 卡返回 {"Result":"-1","resultName":"无终端开关机信息"}(值是 Result,
// 大写 R),应映射为“未知”而不是报错;同时确认请求走 GET(文档写 POST 与网关不符)。
func TestDXV3DevicePowerStatusSupportsEmptyRecordShape(t *testing.T) {
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("device power status must be sent with GET, got %s", r.Method)
}
fmt.Fprint(w, `{"code":"0","result":{"Result":"-1","resultName":"无终端开关机信息"}}`)
})
items, err := client.QuerySimDeviceStatus(context.Background(), &model.SimDeviceStatusInput{SimKeys: []*model.SimKey{{MSISDN: "1"}}})
if err != nil || items[0].Code != 0 || items[0].ResData.OperatingStatus != consts.CardSessionStatusUnknown {
t.Fatalf("empty power record must map to unknown: %+v, %v", items, err)
}
}
func TestDXV3BasicInfoAllowsDocumentedPartialIdentifiers(t *testing.T) {
for _, body := range []string{
`{"code":"0","result":{"msisdn":"1","createTime":"2023-03-16 11:07:50","activeTime":"2023-03-16 11:07:50","prodName":"test-product"}}`,
`{"code":"0","result":{"msisdn":"1","iccid":"8986","imsi":"46011","prodName":"test-product"}}`,
} {
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, body) })
items, err := client.QuerySimBasicInfo(context.Background(), &model.SimBasicInfoInput{SimKeys: []*model.SimKey{{MSISDN: "1"}}})
if err != nil || items[0].Code != 0 || items[0].ResData.MSISDN != "1" || items[0].ResData.MainOfferName != "test-product" {
t.Fatalf("basic info: %+v, %v", items, err)
}
}
if _, err := dxV3BasicInfo(map[string]any{}); err == nil {
t.Fatal("accepted basic info with no identifiers")
}
}
func TestDXV3StatusBatchChunksAndMatchesCards(t *testing.T) {
calls := 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
calls++
if r.Method != "POST" || r.URL.Path != "/api/v3/getSimStatusBatch" {
t.Errorf("unexpected batch request")
}
body, _ := io.ReadAll(r.Body)
want, _ := dx.BuildSignV3("", string(body), "test-secret", r.Header.Get("Timestamp"))
if r.Header.Get("Sign") != want {
t.Error("body differs from signed body")
}
var request struct {
Numbers []string `json:"msisdnList"`
Customer string `json:"custNbr"`
}
if err := json.Unmarshal(body, &request); err != nil {
t.Fatal(err)
}
if len(request.Numbers) > 100 || request.Customer != "test-customer" {
t.Fatalf("invalid batch: %+v", request)
}
list := make([]map[string]string, 0, len(request.Numbers))
for i := len(request.Numbers) - 1; i >= 0; i-- {
number := request.Numbers[i]
if number == "7" {
continue
}
item := map[string]string{"msisdn": number, "result": "0", "simStatus": "4"}
if number == "8" {
item["result"] = "-1"
}
list = append(list, item)
}
json.NewEncoder(w).Encode(map[string]any{"code": "0", "result": map[string]any{"simStatusList": list}})
})
keys := make([]*model.SimKey, 101)
for i := range keys {
keys[i] = &model.SimKey{MSISDN: fmt.Sprint(i)}
}
items, err := client.QuerySimCardStatusBatch(context.Background(), &model.SimCardStatusInput{SimKeys: keys})
if err != nil || len(items) != 101 || calls != 2 {
t.Fatalf("batch size/calls: %d/%d err=%v", len(items), calls, err)
}
for i, item := range items {
if item.SimKey != keys[i] {
t.Fatal("card identity/order changed")
}
if i == 7 || i == 8 {
if item.Code == 0 {
t.Fatalf("partial failure %d was successful", i)
}
} else if item.Code != 0 || item.ResData.CardStatus != 2 {
t.Fatalf("card %d: %+v", i, item)
}
}
}
func TestDXV3BatchUsageChecksPeriodAndItemCode(t *testing.T) {
month := time.Now().In(dxBatchLocation).Format("200601")
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"code":"0","result":{"batchTrafficList":[{"msisdn":"1","result":"0","month":%q,"unit":"MB","dataAmount":"2.5"},{"msisdn":"2","result":"1","month":%q,"dataAmount":"9"},{"msisdn":"3","result":"0","month":"200001","dataAmount":"9"}]}}`, month, month)
})
items, err := client.QuerySimDataUsageMonthlyBatch(context.Background(), &model.SimDataUsageMonthlyInput{SimKeys: []*model.SimKey{{MSISDN: "1"}, {MSISDN: "2"}, {MSISDN: "3"}}, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: month}})
if err != nil || items[0].Code != 0 || items[0].ResData.DataAmount != 2.5 || items[1].Code == 0 || items[2].Code == 0 {
t.Fatalf("unexpected usage batch: %+v, %v", items, err)
}
}
func TestDXV3OperationsDefaultOffAndUseNewSemantics(t *testing.T) {
calls := 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
calls++
switch r.URL.Path {
case "/api/v3/singleCutNet":
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
if r.Method != "POST" || body["operType"] != "2" || body["msisdn"] != "1" {
t.Errorf("invalid cut-net request: %+v", body)
}
case "/api/v3/updateSimStatus":
if r.URL.Query().Get("operType") != "19" {
t.Error("wrong status operation")
}
default:
t.Error("unexpected operation path")
}
fmt.Fprint(w, `{"code":"0","result":{"transactionId":"order-1"}}`)
})
status := &model.SimCardStatusChangeInput{SimKeys: []*model.SimKey{{MSISDN: "1"}}, ReqData: model.SimCardStatusChangeInputItem{OperType: "0"}}
items, _ := client.ChangeSimStatus(context.Background(), status)
if items[0].Code == 0 || calls != 0 {
t.Fatal("disabled operation sent a request")
}
client.enableOperations = true
items, _ = client.ChangeSimStatus(context.Background(), status)
if items[0].Code != 0 || items[0].ResData.OrderNum != "order-1" {
t.Fatalf("status: %+v", items[0])
}
data, _ := client.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{SimKeys: status.SimKeys, ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1"}})
if data[0].Code != 0 || calls != 2 {
t.Fatal("new cut-net operation did not succeed")
}
if _, err := dxV3Params(nil); err == nil {
t.Fatal("nil key accepted")
}
if !strings.Contains(items[0].ResData.OrderNum, "order") {
t.Fatal("missing operation reference")
}
}
func TestDXV3CutNetIgnoresApnNameLikeV1(t *testing.T) {
calls := 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
calls++
if r.URL.Path != "/api/v3/singleCutNet" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
if body["operType"] != "2" || body["msisdn"] != "1" {
t.Errorf("invalid cut-net request: %+v", body)
}
if _, ok := body["apnName"]; ok {
t.Errorf("ApnName must not be forwarded: %+v", body)
}
fmt.Fprint(w, `{"code":"0","result":{"transactionId":"order-9"}}`)
})
client.enableOperations = true
items, err := client.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{
SimKeys: []*model.SimKey{{MSISDN: "1"}},
ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1", ApnName: "CMIOT"},
})
if err != nil || calls != 1 || len(items) != 1 || items[0].Code != 0 || items[0].ResData.OrderNum != "order-9" {
t.Fatalf("named APN must be ignored, not rejected: %+v, %v", items, err)
}
}
func TestDXV3UnsupportedQueriesDegradeLikeV1(t *testing.T) {
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unsupported queries must not reach the upstream: %s", r.URL.Path)
})
keys := []*model.SimKey{{MSISDN: "1"}}
frozen, err := client.QuerySimFrozenState(context.Background(), &model.SimFrozenStateInput{SimKeys: keys})
if err != nil || len(frozen) != 1 || frozen[0].Code != consts.ApiStatusSuccess || frozen[0].ResData.FrozenStatus != "不支持查询" {
t.Fatalf("frozen state should degrade gracefully: %+v, %v", frozen, err)
}
diagnosis, err := client.QuerySimDataDiagnosis(context.Background(), &model.SimDataDiagnosisInput{SimKeys: keys})
if err != nil || len(diagnosis) != 1 || diagnosis[0].Code != consts.ApiInvokeException || diagnosis[0].Message == "" {
t.Fatalf("data diagnosis should report as unsupported: %+v, %v", diagnosis, err)
}
}
func TestDXV3MultiCardUsageUpgradesToBatch(t *testing.T) {
month := time.Now().In(dxBatchLocation).Format("200601")
day := time.Now().In(dxBatchLocation).AddDate(0, 0, -1).Format("20060102")
paths := make([]string, 0, 4)
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
switch r.URL.Path {
case "/api/v3/getDataUsageByMonthBatch":
fmt.Fprintf(w, `{"code":"0","result":{"batchTrafficList":[{"msisdn":"1","result":"0","month":%q,"unit":"MB","dataAmount":"2.5"},{"msisdn":"2","result":"0","month":%q,"unit":"MB","dataAmount":"3.5"}]}}`, month, month)
case "/api/v3/getDataUsageByDayBatch":
fmt.Fprintf(w, `{"code":"0","result":{"batchTrafficList":[{"msisdn":"1","result":"0","date":%q,"unit":"MB","dataAmount":"1.5"},{"msisdn":"2","result":"0","date":%q,"unit":"MB","dataAmount":"2.5"}]}}`, day, day)
default:
fmt.Fprint(w, `{"code":"0","result":{"dataAmount":"1","unit":"MB"}}`)
}
})
keys := []*model.SimKey{{MSISDN: "1"}, {MSISDN: "2"}}
monthly, err := client.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{SimKeys: keys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: month}})
if err != nil || len(monthly) != 2 || monthly[0].ResData.DataAmount != 2.5 || monthly[1].ResData.DataAmount != 3.5 {
t.Fatalf("monthly upgrade: %+v, %v", monthly, err)
}
if len(paths) != 1 || paths[0] != "/api/v3/getDataUsageByMonthBatch" {
t.Fatalf("monthly query should collapse into one batch call, got %v", paths)
}
paths = paths[:0]
daily, err := client.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{SimKeys: keys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: day}})
if err != nil || len(daily) != 2 || daily[0].ResData.DataAmount != 1.5 {
t.Fatalf("daily upgrade: %+v, %v", daily, err)
}
if len(paths) != 1 || paths[0] != "/api/v3/getDataUsageByDayBatch" {
t.Fatalf("daily query should collapse into one batch call, got %v", paths)
}
}
func TestDXV3MultiCardUsageFallsBackToConcurrentPerCard(t *testing.T) {
month := time.Now().In(dxBatchLocation).Format("200601")
calls := 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
calls++
if r.URL.Path != "/api/v3/getDataUsageByMonth" {
t.Errorf("batch must not be used without custNbr: %s", r.URL.Path)
}
fmt.Fprint(w, `{"code":"0","result":{"dataAmount":"1","unit":"MB"}}`)
})
client.custNbr = ""
keys := []*model.SimKey{{MSISDN: "1"}, {MSISDN: "2"}, {MSISDN: "3"}}
items, err := client.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{SimKeys: keys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: month}})
if err != nil || len(items) != 3 || calls != 3 {
t.Fatalf("fallback should query every card once: %d, %+v, %v", calls, items, err)
}
for index, item := range items {
if item.SimKey != keys[index] || item.Code != 0 {
t.Fatalf("fallback lost order or failed: %+v", item)
}
}
}
// 批量接口整批不可用(网关/鉴权/报文错误)时,必须回退单卡查询,而不是整批报废。
func TestDXV3BatchOutageFallsBackToSingleCard(t *testing.T) {
month := time.Now().In(dxBatchLocation).Format("200601")
batchCalls, singleCalls := 0, 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v3/getDataUsageByMonthBatch":
batchCalls++
w.Header().Set("gw-errorCode", "S00004")
w.WriteHeader(http.StatusBadGateway)
fmt.Fprint(w, `{}`)
case "/api/v3/getDataUsageByMonth":
singleCalls++
fmt.Fprint(w, `{"code":"0","result":{"dataAmount":"4","unit":"MB"}}`)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
})
keys := []*model.SimKey{{MSISDN: "1"}, {MSISDN: "2"}}
items, err := client.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{
SimKeys: keys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: month}})
if err != nil || len(items) != 2 {
t.Fatalf("batch outage must not fail the whole query: %+v, %v", items, err)
}
for index, item := range items {
if item.SimKey != keys[index] || item.Code != consts.ApiStatusSuccess || item.ResData.DataAmount != 4 {
t.Fatalf("card %d was not recovered by the single-card fallback: %+v", index, item)
}
}
if batchCalls != 1 || singleCalls != 2 {
t.Fatalf("unexpected call split: batch=%d single=%d", batchCalls, singleCalls)
}
}
// 批量只漏/错某张卡时,只对该卡用单卡补齐,批量成功项保留(不重复请求)。
func TestDXV3BatchOnlyRefillsFailedCards(t *testing.T) {
month := time.Now().In(dxBatchLocation).Format("200601")
singleCalls := 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v3/getDataUsageByMonthBatch":
fmt.Fprintf(w, `{"code":"0","result":{"batchTrafficList":[{"msisdn":"1","result":"0","month":%q,"unit":"MB","dataAmount":"2.5"}]}}`, month)
case "/api/v3/getDataUsageByMonth":
singleCalls++
if r.URL.Query().Get("msisdn") != "2" {
t.Errorf("only the failed card may be refilled, got %s", r.URL.Query().Get("msisdn"))
}
fmt.Fprint(w, `{"code":"0","result":{"dataAmount":"7","unit":"MB"}}`)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
})
keys := []*model.SimKey{{MSISDN: "1"}, {MSISDN: "2"}}
items, err := client.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{
SimKeys: keys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: month}})
if err != nil || len(items) != 2 || singleCalls != 1 {
t.Fatalf("only failed cards should be refilled: single=%d, %+v, %v", singleCalls, items, err)
}
if items[0].Code != consts.ApiStatusSuccess || items[0].ResData.DataAmount != 2.5 {
t.Fatalf("batch success must be kept: %+v", items[0])
}
if items[1].Code != consts.ApiStatusSuccess || items[1].ResData.DataAmount != 7 {
t.Fatalf("omitted card must be refilled by single query: %+v", items[1])
}
}