334 lines
15 KiB
Go
334 lines
15 KiB
Go
package upstreamimpl
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/dx"
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
|
)
|
|
|
|
func TestDXQueryAPNV3(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
wantCode int
|
|
wantStatus string
|
|
wantCount int
|
|
wantErrCode string
|
|
}{
|
|
{name: "table status enabled", body: `{"code":"0","result":{"apnInfoList":[{"apnName":"ctnet","status":"1"}]}}`, wantStatus: "1", wantCount: 1},
|
|
{name: "example apnStatus stopped", body: `{"code":"0","result":{"apnInfoList":[{"apnName":"ctnet","apnStatus":"0"}]}}`, wantStatus: "0", wantCount: 1},
|
|
{name: "missing status stays unknown", body: `{"code":"0","result":{"apnInfoList":[{"apnName":"ctnet"}]}}`, wantCount: 1},
|
|
{name: "conflicting aliases stay unknown", body: `{"code":"0","result":{"apnInfoList":[{"apnName":"ctnet","status":"1","apnStatus":"0"}]}}`, wantCount: 1},
|
|
{name: "invalid status stays unknown", body: `{"code":"0","result":{"apnInfoList":[{"apnName":"ctnet","status":"2"}]}}`, wantCount: 1},
|
|
{name: "empty list", body: `{"code":0,"result":{"apnInfoList":[]}}`},
|
|
{name: "missing code", body: `{"result":{"apnInfoList":[]}}`, wantCode: -1},
|
|
{name: "null code", body: `{"code":null,"result":{"apnInfoList":[]}}`, wantCode: -1, wantErrCode: "null"},
|
|
{name: "missing result", body: `{"code":"0"}`, wantCode: -1},
|
|
{name: "missing list", body: `{"code":"0","result":{}}`, wantCode: -1},
|
|
{name: "carrier failure", body: `{"code":"14008","message":"not found","result":null}`, wantCode: 14008, wantErrCode: "14008"},
|
|
{name: "symbolic carrier failure", body: `{"code":"S00004","message":"denied"}`, wantCode: -1, wantErrCode: "S00004"},
|
|
{name: "malformed", body: `<xml/>`, wantCode: -1},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.URL.Path != "/api/v3/getApnInfo" || request.Method != http.MethodGet {
|
|
t.Errorf("unexpected request: %s %s", request.Method, request.URL.Path)
|
|
}
|
|
if request.URL.Query().Get("msisdn") != "13300000000" || request.URL.Query().Get("access_number") != "" {
|
|
t.Errorf("query = %s", request.URL.RawQuery)
|
|
}
|
|
if request.Header.Get("AppKey") != "test" || request.Header.Get("Sign") == "" || request.Header.Get("Timestamp") == "" {
|
|
t.Error("missing signed request headers")
|
|
}
|
|
fmt.Fprint(writer, test.body)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "secret"})}
|
|
output, err := sim.QuerySimApnInfo(context.Background(), &model.SimApnInfoInput{SimKeys: []*model.SimKey{{MSISDN: "13300000000"}}})
|
|
if err != nil || len(output) != 1 {
|
|
t.Fatalf("output = %+v, error = %v", output, err)
|
|
}
|
|
item := output[0]
|
|
if (test.wantCode == -1 && item.Code == 0) || (test.wantCode != -1 && item.Code != test.wantCode) || item.ErrCode != test.wantErrCode {
|
|
t.Fatalf("code = %d, raw = %q", item.Code, item.ErrCode)
|
|
}
|
|
if len(item.ResData.ApnList) != test.wantCount {
|
|
t.Fatalf("APNs = %+v", item.ResData.ApnList)
|
|
}
|
|
if test.wantCount != 0 && (item.ResData.ApnList[0].Status != test.wantStatus || item.ResData.ApnList[0].ApnName != "ctnet") {
|
|
t.Fatalf("APN = %+v", item.ResData.ApnList[0])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDXAPNIdentifierFallback(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key *model.SimKey
|
|
param string
|
|
value string
|
|
}{
|
|
{name: "iccid", key: &model.SimKey{ICCID: " 8986000000000000006 "}, param: "iccid", value: "8986000000000000006"},
|
|
{name: "imsi", key: &model.SimKey{IMSI: "460000000000001"}, param: "imsi", value: "460000000000001"},
|
|
{name: "nil", key: nil},
|
|
{name: "empty", key: &model.SimKey{}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
requests++
|
|
if request.URL.Query().Get(test.param) != test.value {
|
|
t.Errorf("query = %s", request.URL.RawQuery)
|
|
}
|
|
fmt.Fprint(writer, `{"code":"0","result":{"apnInfoList":[]}}`)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL})}
|
|
output, err := sim.QuerySimApnInfo(context.Background(), &model.SimApnInfoInput{SimKeys: []*model.SimKey{test.key}})
|
|
if err != nil || len(output) != 1 {
|
|
t.Fatalf("output = %+v, error = %v", output, err)
|
|
}
|
|
if test.param == "" {
|
|
if output[0].Code == 0 || requests != 0 {
|
|
t.Fatal("missing identifier must not request upstream")
|
|
}
|
|
} else if output[0].Code != 0 || requests != 1 {
|
|
t.Fatalf("code = %d, requests = %d", output[0].Code, requests)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDXChangeAPNSingleCutNet 验证电信数据开停走 singleCutNet,且 ADD/DEL 方向与平台语义相反。
|
|
func TestDXChangeAPNSingleCutNet(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
operType string
|
|
wantAction string
|
|
}{
|
|
{name: "platform open resumes network", operType: "0", wantAction: "DEL"},
|
|
{name: "platform stop cuts network", operType: "1", wantAction: "ADD"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
requests++
|
|
if request.URL.Path != "/5gcmp/openapi/v1/common/singleCutNet" || request.Method != http.MethodGet {
|
|
t.Errorf("unexpected request: %s %s", request.Method, request.URL.Path)
|
|
}
|
|
query := request.URL.Query()
|
|
if query.Get("action") != test.wantAction {
|
|
t.Errorf("action = %q, want %q", query.Get("action"), test.wantAction)
|
|
}
|
|
if query.Get("access_number") != "13300000000" {
|
|
t.Errorf("access_number = %q", query.Get("access_number"))
|
|
}
|
|
// 不能携带 APN 定向参数,singleCutNet 是整卡语义。
|
|
if query.Get("apnName") != "" || query.Get("operType") != "" {
|
|
t.Errorf("APN-scoped params must not be sent: %s", request.URL.RawQuery)
|
|
}
|
|
if request.Header.Get("AppKey") != "test" || request.Header.Get("Sign") == "" || request.Header.Get("Timestamp") == "" {
|
|
t.Error("missing signed request headers")
|
|
}
|
|
fmt.Fprint(writer, `<?xml version="1.0" encoding="UTF-8"?><SvcCont><Response><RspType>0</RspType><RspCode>0000</RspCode><RspDesc>成功接收消息</RspDesc></Response><GROUP_TRANSACTIONID>order-1</GROUP_TRANSACTIONID></SvcCont>`)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "secret"})}
|
|
// 即使调用方传了移动默认的 apnName="CMIOT",也必须正常执行。
|
|
output, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{
|
|
SimKeys: []*model.SimKey{{MSISDN: "13300000000", ChanKey: "71"}},
|
|
ReqData: model.SimCardOperateChangeApnInputItem{ApnName: "CMIOT", OperType: test.operType},
|
|
})
|
|
if err != nil || len(output) != 1 || output[0].Code != 0 || output[0].ResData.OrderNum != "order-1" || requests != 1 {
|
|
t.Fatalf("output = %+v, error = %v, requests = %d", output, err, requests)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDXSingleCutNetAcceptanceModes(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
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
writes++
|
|
code := "1000"
|
|
if accepted {
|
|
code = "0000"
|
|
}
|
|
fmt.Fprintf(w, `<SvcCont><Response><RspCode>%s</RspCode><RspDesc>响应</RspDesc></Response><GROUP_TRANSACTIONID>order-%d</GROUP_TRANSACTIONID></SvcCont>`, code, writes)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL})}
|
|
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.ErrCode != "0000" || 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 != "1000" || 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 := sim.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 TestDXSingleCutNetRedirectDoesNotReplay(t *testing.T) {
|
|
for _, status := range []int{301, 302, 307, 308} {
|
|
t.Run(fmt.Sprint(status), func(t *testing.T) {
|
|
writes, redirected := 0, 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/redirected" {
|
|
redirected++
|
|
return
|
|
}
|
|
writes++
|
|
w.Header().Set("Location", "/redirected")
|
|
w.WriteHeader(status)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL})}
|
|
output, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{
|
|
SimKeys: []*model.SimKey{{MSISDN: "13300000000"}},
|
|
ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1"},
|
|
})
|
|
if err != nil || len(output) != 1 {
|
|
t.Fatalf("output=%+v err=%v", output, err)
|
|
}
|
|
if writes != 1 || redirected != 0 || output[0].Code == 0 || !output[0].ResData.Uncertain {
|
|
t.Fatalf("writes=%d redirected=%d output=%+v", writes, redirected, output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDXSingleCutNetResponseLossIsUncertain(t *testing.T) {
|
|
writes := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
writes++
|
|
conn, _, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
t.Errorf("hijack: %v", err)
|
|
return
|
|
}
|
|
_ = conn.Close()
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL})}
|
|
output, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{
|
|
SimKeys: []*model.SimKey{{MSISDN: "13300000000"}},
|
|
ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1"},
|
|
})
|
|
if err != nil || len(output) != 1 {
|
|
t.Fatalf("output=%+v err=%v", output, err)
|
|
}
|
|
if writes != 1 || output[0].Code == 0 || !output[0].ResData.Uncertain {
|
|
t.Fatalf("writes=%d output=%+v", writes, output)
|
|
}
|
|
}
|
|
|
|
func TestDXSingleCutNetDurableCheckpoint(t *testing.T) {
|
|
writes, checkpoints := 0, 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
writes++
|
|
fmt.Fprint(w, `<?xml version="1.0"?><SvcCont><Response><RspCode>0000</RspCode></Response><GROUP_TRANSACTIONID>job-1</GROUP_TRANSACTIONID></SvcCont>`)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL})}
|
|
ctx := context.WithValue(context.Background(), model.ApnOperationCheckpointKey{}, func(items model.SimCardOperateChangeApnOutput) error {
|
|
checkpoints++
|
|
if len(items) != 1 || items[0].ResData.OrderNum != "job-1" {
|
|
t.Errorf("checkpoint must preserve the first card job ID: %+v", items)
|
|
}
|
|
return fmt.Errorf("stop after durable checkpoint")
|
|
})
|
|
_, err := sim.ChangeSimApnFunction(ctx, &model.SimCardOperateChangeApnInput{
|
|
SimKeys: []*model.SimKey{{MSISDN: "13300000000"}, {MSISDN: "13300000001"}},
|
|
ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1"},
|
|
})
|
|
if err == nil || writes != 1 || checkpoints != 1 {
|
|
t.Fatalf("must stop before second write: writes=%d checkpoints=%d err=%v", writes, checkpoints, err)
|
|
}
|
|
}
|
|
|
|
// TestDXChangeAPNSingleCutNetFailClosed 验证失败保护:不成功不重试、不盲目视为成功。
|
|
func TestDXChangeAPNSingleCutNetFailClosed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
operType string
|
|
msisdn string
|
|
body string
|
|
httpStatus int
|
|
wantRequests int
|
|
}{
|
|
{name: "missing access number", operType: "0"},
|
|
{name: "invalid operation", operType: "2", msisdn: "13300000000"},
|
|
{name: "carrier rejected", operType: "1", msisdn: "13300000000", body: `<?xml version="1.0"?><SvcCont><Response><RspCode>1000</RspCode><RspDesc>rejected</RspDesc></Response></SvcCont>`, wantRequests: 1},
|
|
{name: "missing result code", operType: "0", msisdn: "13300000000", body: `<?xml version="1.0"?><SvcCont><Response><RspDesc>ok</RspDesc></Response><GROUP_TRANSACTIONID>order</GROUP_TRANSACTIONID></SvcCont>`, wantRequests: 1},
|
|
{name: "missing order number", operType: "0", msisdn: "13300000000", body: `<?xml version="1.0"?><SvcCont><Response><RspCode>0000</RspCode><RspDesc>ok</RspDesc></Response></SvcCont>`, wantRequests: 1},
|
|
{name: "malformed response", operType: "0", msisdn: "13300000000", body: `{"code":"0"}`, wantRequests: 1},
|
|
{name: "HTTP failed no retry", operType: "0", msisdn: "13300000000", body: `gateway down`, httpStatus: 503, wantRequests: 1},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
requests++
|
|
if request.URL.Path != "/5gcmp/openapi/v1/common/singleCutNet" {
|
|
t.Errorf("unexpected endpoint: %s", request.URL.Path)
|
|
}
|
|
if test.httpStatus != 0 {
|
|
writer.WriteHeader(test.httpStatus)
|
|
}
|
|
fmt.Fprint(writer, test.body)
|
|
}))
|
|
defer server.Close()
|
|
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL})}
|
|
output, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{
|
|
SimKeys: []*model.SimKey{{MSISDN: test.msisdn, ChanKey: "71"}},
|
|
ReqData: model.SimCardOperateChangeApnInputItem{ApnName: "CMIOT", OperType: test.operType},
|
|
})
|
|
if err != nil || len(output) != 1 || output[0].Code == 0 || output[0].ResData.OrderNum != "" || requests != test.wantRequests {
|
|
t.Fatalf("output = %+v, error = %v, requests = %d", output, err, requests)
|
|
}
|
|
})
|
|
}
|
|
}
|