96b1edff45
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
469 lines
22 KiB
Go
469 lines
22 KiB
Go
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])
|
||
}
|
||
}
|