chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:02 +08:00
192 changed files with 33119 additions and 0 deletions
@@ -0,0 +1,630 @@
package upstreamimpl
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"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"
)
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 TestDXV3APNWriteUncertainAndNoReplay(t *testing.T) {
for _, tc := range []struct {
name, body string
drop, uncertain bool
redirect int
}{
{name: "断响应", drop: true, uncertain: true},
{name: "坏JSON", body: "{", uncertain: true},
{name: "缺result", body: `{"code":0}`, uncertain: true},
{name: "卡号不符", body: `{"code":0,"result":{"msisdn":"other","transactionId":"job"}}`, uncertain: true},
{name: "缺transactionId", body: `{"code":0,"result":{}}`, uncertain: true},
{name: "明确拒绝", body: `{"code":9999,"message":"拒绝"}`},
{name: "明确成功", body: `{"code":0,"result":{"transactionId":"job"}}`},
{name: "跳转307", redirect: 307, uncertain: true},
{name: "跳转308", redirect: 308, uncertain: true},
} {
t.Run(tc.name, func(t *testing.T) {
var writes, targetWrites atomic.Int32
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("办理方法被改变:%s", r.Method)
}
writes.Add(1)
if r.URL.Path == "/redirect-target" {
targetWrites.Add(1)
fmt.Fprint(w, `{"code":0,"result":{"transactionId":"unexpected"}}`)
return
}
if r.URL.Path != "/api/v3/singleCutNet" {
t.Errorf("错误办理路径:%s", r.URL.Path)
}
var body map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body["msisdn"] != "fake-msisdn" || body["operType"] != "1" {
t.Errorf("办理请求不符:%+v %v", body, err)
}
if tc.drop {
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
t.Error(err)
return
}
_ = conn.Close()
return
}
if tc.redirect != 0 {
w.Header().Set("Location", "/redirect-target")
w.WriteHeader(tc.redirect)
return
}
fmt.Fprint(w, tc.body)
})
client.enableOperations = true
out, err := client.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{SimKeys: []*model.SimKey{{MSISDN: "fake-msisdn"}}, ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0"}})
if err != nil || len(out) != 1 || writes.Load() != 1 || targetWrites.Load() != 0 || out[0].ResData.Uncertain != tc.uncertain || out[0].ResData.Pending {
t.Fatalf("写后分类/重放错误:out=%+v writes=%d target=%d err=%v", out, writes.Load(), targetWrites.Load(), err)
}
if (tc.name == "明确成功") != (out[0].Code == 0) {
t.Fatalf("成功失败分类错误:%+v", out[0])
}
t.Logf("writes=%d targetWrites=%d uncertain=%v code=%d", writes.Load(), targetWrites.Load(), out[0].ResData.Uncertain, out[0].Code)
})
}
}
func TestDXV3SingleCutNetAcceptanceModes(t *testing.T) {
for _, mode := range []string{"同步兼容", "仅提交", "持久化回调"} {
for _, accepted := range []bool{true, false} {
t.Run(fmt.Sprintf("%s/受理=%t", mode, accepted), func(t *testing.T) {
writes, checkpoints := 0, 0
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
writes++
if !accepted {
fmt.Fprint(w, `{"code":9999,"message":"明确拒绝"}`)
return
}
fmt.Fprintf(w, `{"code":0,"result":{"transactionId":"order-%d"}}`, writes)
})
client.enableOperations = true
wantUncertain := accepted && mode != "同步兼容"
check := func(items model.SimCardOperateChangeApnOutput) {
t.Helper()
for i, item := range items {
if (item.Code == 0) != accepted || item.ResData.Uncertain != wantUncertain || item.ResData.Pending {
t.Fatalf("受理与完成分类错误:%+v", item)
}
if accepted && item.ResData.OrderNum != fmt.Sprintf("order-%d", i+1) {
t.Fatalf("未保留逐卡受理流水:%+v", item)
}
if wantUncertain && !strings.Contains(item.Message, "最终办理结果未确认") {
t.Fatalf("受理消息未明确最终结果未知:%s", item.Message)
}
if !accepted && (item.ErrCode != "9999" || item.ResData.OrderNum != "") {
t.Fatalf("明确拒绝不应携带受理流水:%+v", item)
}
}
}
ctx := context.Background()
if mode == "持久化回调" {
ctx = context.WithValue(ctx, model.ApnOperationCheckpointKey{}, func(items model.SimCardOperateChangeApnOutput) error {
checkpoints++
check(items)
return nil
})
}
items, err := client.ChangeSimApnFunction(ctx, &model.SimCardOperateChangeApnInput{
SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "card-1", MSISDN: "test-msisdn-1"}, {ChanKey: "test", ICCID: "card-2", MSISDN: "test-msisdn-2"}},
ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1", SubmitOnly: mode == "仅提交"},
})
if err != nil || len(items) != 2 || writes != 2 || (mode == "持久化回调" && checkpoints != 2) {
t.Fatalf("逐卡处理异常:items=%+v writes=%d checkpoints=%d err=%v", items, writes, checkpoints, err)
}
check(items)
})
}
}
}
func TestDXV3APNPrewriteValidationSendsNothing(t *testing.T) {
for _, tc := range []struct {
name string
enabled bool
key *model.SimKey
oper string
}{
{"禁用办理", false, &model.SimKey{MSISDN: "fake"}, "0"},
{"缺卡号", true, nil, "0"},
{"缺msisdn", true, &model.SimKey{ICCID: "fake"}, "0"},
{"错误操作", true, &model.SimKey{MSISDN: "fake"}, "invalid"},
} {
t.Run(tc.name, func(t *testing.T) {
var writes atomic.Int32
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) { writes.Add(1) })
client.enableOperations = tc.enabled
out, err := client.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{SimKeys: []*model.SimKey{tc.key}, ReqData: model.SimCardOperateChangeApnInputItem{OperType: tc.oper}})
if err != nil || len(out) != 1 || writes.Load() != 0 || out[0].Code == 0 || out[0].ResData.Uncertain {
t.Fatalf("预写拒绝错误:out=%+v writes=%d err=%v", out, writes.Load(), err)
}
})
}
}
func TestDXV3ReadRedirectUnchanged(t *testing.T) {
var calls atomic.Int32
client := newDXV3TestClient(t, func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
if r.Method != http.MethodGet {
t.Errorf("查询方法改变:%s", r.Method)
}
if r.URL.Path != "/read-target" {
w.Header().Set("Location", "/read-target")
w.WriteHeader(http.StatusTemporaryRedirect)
return
}
fmt.Fprint(w, `{"code":0,"result":{"dataAmount":"2"}}`)
})
out, err := client.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{MSISDN: "fake"}}})
if err != nil || len(out) != 1 || out[0].Code != 0 || calls.Load() != 2 {
t.Fatalf("查询跳转被改变:%+v %v calls=%d", out, err, calls.Load())
}
}
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":"127.0.0.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])
}
}