528 lines
21 KiB
Go
528 lines
21 KiB
Go
package upstream
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gcfg"
|
||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||
)
|
||
|
||
func startAPNHTTPChainHelper(t *testing.T, module string, extraEnv ...string) string {
|
||
t.Helper()
|
||
root, err := filepath.Abs(filepath.Join("..", "..", "..", "..", module))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
buildCtx, buildCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||
defer buildCancel()
|
||
binary := filepath.Join(t.TempDir(), module+"-http-test.exe")
|
||
build := exec.CommandContext(buildCtx, "go", "-C", root, "test", "-work", "-c", "-o", binary, "./internal/cmd")
|
||
build.Env = append(os.Environ(), "GOTMPDIR="+t.TempDir())
|
||
if output, err := build.CombinedOutput(); err != nil {
|
||
t.Fatalf("%s模拟进程编译失败:%v\n%s", module, err, output)
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 170*time.Second)
|
||
cmd := exec.CommandContext(ctx, binary, "-test.run=^TestAPNHTTPHarness$", "-test.v", "-test.count=1")
|
||
cmd.Dir = filepath.Join(root, "internal", "cmd")
|
||
cmd.Env = append(append(os.Environ(), "APN_HTTP_CHAIN_HELPER=1"), extraEnv...)
|
||
stdout, err := cmd.StdoutPipe()
|
||
if err != nil {
|
||
cancel()
|
||
t.Fatal(err)
|
||
}
|
||
cmd.Stderr = cmd.Stdout
|
||
if err := cmd.Start(); err != nil {
|
||
cancel()
|
||
t.Fatal(err)
|
||
}
|
||
ready := make(chan string, 1)
|
||
var mu sync.Mutex
|
||
var log strings.Builder
|
||
go func() {
|
||
scanner := bufio.NewScanner(stdout)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
mu.Lock()
|
||
log.WriteString(line + "\n")
|
||
mu.Unlock()
|
||
if strings.HasPrefix(line, "APN_CHAIN_READY=") {
|
||
ready <- strings.TrimPrefix(line, "APN_CHAIN_READY=")
|
||
}
|
||
}
|
||
}()
|
||
done := make(chan error, 1)
|
||
go func() { done <- cmd.Wait() }()
|
||
var address string
|
||
select {
|
||
case address = <-ready:
|
||
case err := <-done:
|
||
cancel()
|
||
mu.Lock()
|
||
text := log.String()
|
||
mu.Unlock()
|
||
t.Fatalf("%s模拟进程启动即退出:%v\n%s", module, err, text)
|
||
case <-ctx.Done():
|
||
cancel()
|
||
<-done
|
||
mu.Lock()
|
||
text := log.String()
|
||
mu.Unlock()
|
||
t.Fatalf("%s模拟进程启动失败:%s", module, text)
|
||
}
|
||
t.Cleanup(func() {
|
||
requestCtx, stop := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer stop()
|
||
req, _ := http.NewRequestWithContext(requestCtx, http.MethodPost, address+"/__test/stop", nil)
|
||
if res, err := http.DefaultClient.Do(req); err == nil {
|
||
_ = res.Body.Close()
|
||
if res.StatusCode != http.StatusOK {
|
||
t.Errorf("%s停止请求失败:%d", module, res.StatusCode)
|
||
}
|
||
} else {
|
||
t.Errorf("%s停止请求失败:%v", module, err)
|
||
}
|
||
if err := <-done; err != nil {
|
||
mu.Lock()
|
||
text := log.String()
|
||
mu.Unlock()
|
||
t.Errorf("%s模拟进程异常退出:%v\n%s", module, err, text)
|
||
} else {
|
||
t.Logf("%s helper正常shutdown并退出0", module)
|
||
}
|
||
cancel()
|
||
})
|
||
return address
|
||
}
|
||
|
||
func TestAPNHTTPChainWorkerClientProxyChanCarrier(t *testing.T) {
|
||
if os.Getenv("APN_HTTP_CHAIN_INTEGRATION") != "1" {
|
||
t.Skip("未启用本机跨模块HTTP及专用MySQL模拟链")
|
||
}
|
||
chanURL := startAPNHTTPChainHelper(t, "iotlcm-upstream-chan")
|
||
proxyURL := startAPNHTTPChainHelper(t, "iotlcm-upstream-proxy", "APN_HTTP_CHAIN_CHAN="+chanURL)
|
||
oldAdapter := g.Cfg().GetAdapter()
|
||
defer g.Cfg().SetAdapter(oldAdapter)
|
||
adapter, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"upstream":{"proxy":{"address":%q,"timeout":"2s","apnAsyncProtocolVersion":1}}}`, strings.TrimPrefix(proxyURL, "http://")))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g.Cfg().SetAdapter(adapter)
|
||
client := New()
|
||
expectedWrites := map[string]int{}
|
||
expectedQueries := map[string]int{}
|
||
expectedSubmits := 0
|
||
for _, scenario := range []string{"drop-response", "top-error", "lost-response", "failed-task"} {
|
||
t.Run(scenario, func(t *testing.T) {
|
||
input := &model.SimCardApnChangeInput{ReqData: model.SimCardApnChangeInputItem{OperationID: fmt.Sprintf("http-chain-%s-%d", scenario, time.Now().UnixNano()), OperType: "1", ApnName: "CMIOT"}}
|
||
prefix := scenario + "-"
|
||
if scenario == "lost-response" {
|
||
prefix = "lost-"
|
||
}
|
||
if scenario == "failed-task" {
|
||
prefix = "failed-"
|
||
}
|
||
for i := 0; i < 10; i++ {
|
||
input.SimKeys = append(input.SimKeys, &model.SimKey{ChanKey: "fake-channel", ICCID: fmt.Sprintf("fake-card-%s-%d", scenario, i), MSISDN: fmt.Sprintf("%s%d", prefix, i)})
|
||
}
|
||
out, err := client.SubmitSimApnFunction(context.Background(), input)
|
||
if err != nil || len(out) != 10 {
|
||
t.Fatalf("首发结果无效:%+v %v", out, err)
|
||
}
|
||
for _, item := range out {
|
||
if scenario != "failed-task" && !item.ResData.Uncertain {
|
||
t.Fatalf("丢响应被确认:%+v", item)
|
||
}
|
||
}
|
||
res, err := http.Post(chanURL+"/__test/restart", "application/json", nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_ = res.Body.Close()
|
||
recovered, err := client.QuerySimApnFunctionResult(context.Background(), input)
|
||
if err != nil || len(recovered) != 10 {
|
||
t.Fatalf("恢复失败:%v", err)
|
||
}
|
||
duplicate, err := client.SubmitSimApnFunction(context.Background(), input)
|
||
if err != nil || len(duplicate) != 10 {
|
||
t.Fatalf("重复恢复失败:%v", err)
|
||
}
|
||
if scenario == "lost-response" {
|
||
for i, item := range recovered {
|
||
if !item.ResData.Uncertain || item.ResData.OrderNum != "" || !duplicate[i].ResData.Uncertain {
|
||
t.Fatalf("无jobId必须未知:%+v", item)
|
||
}
|
||
}
|
||
} else {
|
||
job := recovered[0].ResData.OrderNum
|
||
for i, item := range recovered {
|
||
if job == "" || item.ResData.OrderNum != job || !item.ResData.Pending || duplicate[i].ResData.OrderNum != job {
|
||
t.Fatalf("恢复丢失已持久化jobId:%+v", item)
|
||
}
|
||
}
|
||
expectedQueries[job] = 3
|
||
input.ReqData.QueryJobID = job
|
||
for attempt := 0; attempt < 3; attempt++ {
|
||
result, e := client.QuerySimApnFunctionResult(context.Background(), input)
|
||
if e != nil || len(result) != 10 {
|
||
t.Fatalf("单次查询失败:%v", e)
|
||
}
|
||
for _, item := range result {
|
||
if attempt < 2 && (!item.ResData.Pending || item.Message == "") {
|
||
t.Fatalf("14008/处理中不得终态:%+v", item)
|
||
}
|
||
if attempt == 2 && (item.ResData.Pending || item.ResData.Uncertain || (scenario == "failed-task" && item.Code != 9999) || (scenario != "failed-task" && item.Code != 0)) {
|
||
t.Fatalf("终态语义错误:%+v", item)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
statsResponse, e := http.Get(chanURL + "/__test/stats")
|
||
if e != nil {
|
||
t.Fatal(e)
|
||
}
|
||
var stats struct {
|
||
Writes map[string]int `json:"writes"`
|
||
Queries map[string]int `json:"queries"`
|
||
Submits int `json:"submits"`
|
||
Unexpected int `json:"unexpected"`
|
||
}
|
||
e = json.NewDecoder(statsResponse.Body).Decode(&stats)
|
||
_ = statsResponse.Body.Close()
|
||
expectedSubmits++
|
||
for _, key := range input.SimKeys {
|
||
expectedWrites[key.MSISDN] = 1
|
||
}
|
||
if e != nil || stats.Submits != expectedSubmits || stats.Unexpected != 0 || len(stats.Writes) != len(expectedWrites) || len(stats.Queries) != len(expectedQueries) {
|
||
t.Fatalf("额外或缺失上游请求:stats=%+v err=%v", stats, e)
|
||
}
|
||
for key, count := range expectedWrites {
|
||
if stats.Writes[key] != count {
|
||
t.Fatalf("逐卡办理次数错误:%s=%d,预期%d", key, stats.Writes[key], count)
|
||
}
|
||
}
|
||
for job, count := range expectedQueries {
|
||
if stats.Queries[job] != count {
|
||
t.Fatalf("纯查询次数错误:%s=%d,预期%d", job, stats.Queries[job], count)
|
||
}
|
||
}
|
||
counts := make([]int, 0, len(input.SimKeys))
|
||
for _, key := range input.SimKeys {
|
||
counts = append(counts, stats.Writes[key.MSISDN])
|
||
}
|
||
t.Logf("十卡实际办理计数=%v 累计提交=%d 无额外请求;丢失jobId=%v 明确失败终态=%v", counts, stats.Submits, scenario == "lost-response", scenario == "failed-task")
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAsyncApnCapabilityGateBlocksOldChanBeforeSubmit(t *testing.T) {
|
||
oldAdapter := g.Cfg().GetAdapter()
|
||
defer g.Cfg().SetAdapter(oldAdapter)
|
||
for _, tc := range []struct {
|
||
name string
|
||
enabled int
|
||
version int
|
||
wantError bool
|
||
}{{"显式关闭", 0, 1, true}, {"旧版无能力", 1, 0, true}, {"能力就绪", 1, 1, false}} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
probes, submits := 0, 0
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path == "/sim/query/sim-apn-capability" {
|
||
probes++
|
||
_, _ = fmt.Fprintf(w, `{"code":0,"data":[{"code":0,"simKey":{"chanKey":"test","iccid":"fake-card"},"resData":{"protocolVersion":%d}}]}`, tc.version)
|
||
return
|
||
}
|
||
submits++
|
||
_, _ = w.Write([]byte(`{"code":0,"data":[{"code":0,"resData":{"pending":true,"orderNum":"fake-job"}}]}`))
|
||
}))
|
||
defer server.Close()
|
||
adapter, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"upstream":{"proxy":{"address":%q,"timeout":"1s","apnAsyncProtocolVersion":%d}}}`, strings.TrimPrefix(server.URL, "http://"), tc.enabled))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g.Cfg().SetAdapter(adapter)
|
||
_, err = New().SubmitSimApnFunction(context.Background(), &model.SimCardApnChangeInput{SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "fake-card"}}, ReqData: model.SimCardApnChangeInputItem{OperationID: "fake-operation"}})
|
||
var notSent interface{ APNRequestNotSent() bool }
|
||
if (err != nil) != tc.wantError || (tc.wantError && (submits != 0 || !errors.As(err, ¬Sent) || !notSent.APNRequestNotSent())) || (!tc.wantError && (submits != 1 || probes != 1)) {
|
||
t.Fatalf("能力门禁失效:probes=%d submits=%d err=%v", probes, submits, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAsyncApnSubmitDefaultsEnabledWithoutGateConfig 固化默认开启:
|
||
// 配置里不写 apnAsyncProtocolVersion 时,提交不应再被门禁拦下,兼容性交给协议能力探测。
|
||
func TestAsyncApnSubmitDefaultsEnabledWithoutGateConfig(t *testing.T) {
|
||
oldAdapter := g.Cfg().GetAdapter()
|
||
defer g.Cfg().SetAdapter(oldAdapter)
|
||
probes, submits := 0, 0
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path == "/sim/query/sim-apn-capability" {
|
||
probes++
|
||
_, _ = w.Write([]byte(`{"code":0,"data":[{"code":0,"simKey":{"chanKey":"test","iccid":"fake-card"},"resData":{"protocolVersion":1}}]}`))
|
||
return
|
||
}
|
||
submits++
|
||
_, _ = w.Write([]byte(`{"code":0,"data":[{"code":0,"resData":{"pending":true,"orderNum":"fake-job"}}]}`))
|
||
}))
|
||
defer server.Close()
|
||
adapter, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"upstream":{"proxy":{"address":%q,"timeout":"1s"}}}`, strings.TrimPrefix(server.URL, "http://")))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g.Cfg().SetAdapter(adapter)
|
||
if _, err = New().SubmitSimApnFunction(context.Background(), &model.SimCardApnChangeInput{SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "fake-card"}}, ReqData: model.SimCardApnChangeInputItem{OperationID: "fake-operation"}}); err != nil {
|
||
t.Fatalf("未配置开关时提交应默认放行:%v", err)
|
||
}
|
||
if probes != 1 || submits != 1 {
|
||
t.Fatalf("默认放行后仍应做协议探测:probes=%d submits=%d", probes, submits)
|
||
}
|
||
}
|
||
|
||
func TestAsyncApnReadOnlyQueryBypassesSubmitGateButRequiresProtocol(t *testing.T) {
|
||
oldAdapter := g.Cfg().GetAdapter()
|
||
defer g.Cfg().SetAdapter(oldAdapter)
|
||
for _, tc := range []struct {
|
||
name, jobID, operationID string
|
||
version int
|
||
missingProbe bool
|
||
wantQuery bool
|
||
}{
|
||
{name: "existing_job", jobID: "existing-job", version: 1, wantQuery: true},
|
||
{name: "existing_operation", operationID: "existing-operation", version: 1, wantQuery: true},
|
||
{name: "legacy_protocol", jobID: "existing-job", version: 0},
|
||
{name: "missing_capability", jobID: "existing-job", missingProbe: true},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
probes, queries := 0, 0
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path == "/sim/query/sim-apn-capability" {
|
||
probes++
|
||
if tc.missingProbe {
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 404, "message": "missing capability"})
|
||
return
|
||
}
|
||
_, _ = fmt.Fprintf(w, `{"code":0,"data":[{"code":0,"simKey":{"chanKey":"test","iccid":"fake-card"},"resData":{"protocolVersion":%d}}]}`, tc.version)
|
||
return
|
||
}
|
||
if r.URL.Path != "/sim/change/sim-apn" {
|
||
t.Errorf("unexpected endpoint: %s", r.URL.Path)
|
||
}
|
||
queries++
|
||
var request model.SimCardApnChangeInput
|
||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||
t.Error(err)
|
||
}
|
||
if request.ReqData.SubmitOnly || request.ReqData.QueryJobID != tc.jobID || request.ReqData.RecoverOnly != (tc.jobID == "") {
|
||
t.Errorf("query accidentally submitted: %+v", request.ReqData)
|
||
}
|
||
_, _ = w.Write([]byte(`{"code":0,"data":[{"code":0,"simKey":{"chanKey":"test","iccid":"fake-card"},"resData":{"orderNum":"existing-job","pending":true}}]}`))
|
||
}))
|
||
defer server.Close()
|
||
adapter, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"upstream":{"proxy":{"address":%q,"timeout":"1s","apnAsyncProtocolVersion":0}}}`, strings.TrimPrefix(server.URL, "http://")))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g.Cfg().SetAdapter(adapter)
|
||
result, err := New().QuerySimApnFunctionResult(context.Background(), &model.SimCardApnChangeInput{
|
||
SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "fake-card"}},
|
||
ReqData: model.SimCardApnChangeInputItem{OperationID: tc.operationID, QueryJobID: tc.jobID},
|
||
})
|
||
if probes != 1 || (err == nil) != tc.wantQuery || queries != map[bool]int{true: 1, false: 0}[tc.wantQuery] {
|
||
t.Fatalf("read-only protocol guard failed: probes=%d queries=%d err=%v result=%+v", probes, queries, err, result)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAsyncApnSecondProbeRejectionIsTypedAndTransportIsNot(t *testing.T) {
|
||
oldAdapter := g.Cfg().GetAdapter()
|
||
defer g.Cfg().SetAdapter(oldAdapter)
|
||
for _, probeFails := range []bool{true, false} {
|
||
probes, submits := 0, 0
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// 夹具不共享连接:并发跑包时复用空闲连接会被对端关闭,
|
||
// 使失败从"提交阶段"漂移到"只读探测阶段",造成与被测语义无关的抖动。
|
||
w.Header().Set("Connection", "close")
|
||
if r.URL.Path == "/sim/query/sim-apn-capability" {
|
||
probes++
|
||
if probes == 2 && probeFails {
|
||
http.Error(w, "模拟第二次探测失败", http.StatusNotFound)
|
||
return
|
||
}
|
||
fmt.Fprint(w, `{"code":0,"data":[{"code":0,"simKey":{"chanKey":"test","iccid":"fake-card"},"resData":{"protocolVersion":1}}]}`)
|
||
return
|
||
}
|
||
submits++
|
||
conn, _, err := w.(http.Hijacker).Hijack()
|
||
if err != nil {
|
||
t.Error(err)
|
||
return
|
||
}
|
||
_ = conn.Close()
|
||
}))
|
||
adapter, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"upstream":{"proxy":{"address":%q,"timeout":"1s","apnAsyncProtocolVersion":1}}}`, strings.TrimPrefix(server.URL, "http://")))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g.Cfg().SetAdapter(adapter)
|
||
input := &model.SimCardApnChangeInput{SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "fake-card"}}, ReqData: model.SimCardApnChangeInputItem{OperationID: "fake-operation"}}
|
||
client := New()
|
||
if err := client.CheckSimApnAsyncCapability(context.Background(), input); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, err = client.SubmitSimApnFunction(context.Background(), input)
|
||
server.Close()
|
||
var notSent interface{ APNRequestNotSent() bool }
|
||
isNotSent := errors.As(err, ¬Sent) && notSent.APNRequestNotSent()
|
||
if err == nil || isNotSent != probeFails || (probeFails && submits != 0) || (!probeFails && submits != 1) || probes != 2 {
|
||
t.Fatalf("零写类型边界错误:probeFails=%v probes=%d submits=%d err=%v", probeFails, probes, submits, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSimCardStatusChangeProtocol(t *testing.T) {
|
||
input := model.SimCardStatusChangeInput{
|
||
SimKeys: []*model.SimKey{{ChanKey: "channel-a", ICCID: "89860001"}},
|
||
ReqData: model.SimCardStatusChangeInputItem{OperType: "1"},
|
||
}
|
||
body, err := json.Marshal(input)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const want = `{"simKeys":[{"chanKey":"channel-a","iccid":"89860001","imsi":"","msisdn":""}],"reqData":{"operType":"1"}}`
|
||
if got := string(body); got != want {
|
||
t.Fatalf("request body = %s, want %s", got, want)
|
||
}
|
||
}
|
||
|
||
func TestCurrentUsageConsumerProtocol(t *testing.T) {
|
||
input := model.SimDataUsageInput{
|
||
SimKeys: []*model.SimKey{{ChanKey: "channel-a", ICCID: "89860001", IMSI: "460001", MSISDN: "13800000000"}},
|
||
}
|
||
body, err := json.Marshal(input)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const want = `{"simKeys":[{"chanKey":"channel-a","iccid":"89860001","imsi":"460001","msisdn":"13800000000"}],"reqData":{}}`
|
||
if got := string(body); got != want {
|
||
t.Fatalf("request body = %s, want %s", got, want)
|
||
}
|
||
client := NewWithCurrentUsageClient("iotlcmupstreamchan-http-svc:8110")
|
||
if client.currentUsageClientName != "iotlcmupstreamchan-http-svc:8110" {
|
||
t.Fatalf("current usage client = %q", client.currentUsageClientName)
|
||
}
|
||
if client.currentUsageTimeout != 0 {
|
||
t.Fatalf("default current usage timeout = %s, want unset", client.currentUsageTimeout)
|
||
}
|
||
configured := NewWithCurrentUsageClient("iotlcmupstreamchan-http-svc:8110", 90*time.Second)
|
||
if configured.currentUsageTimeout != 90*time.Second {
|
||
t.Fatalf("configured current usage timeout = %s, want 90s", configured.currentUsageTimeout)
|
||
}
|
||
}
|
||
|
||
func TestInboundBasicInfoClientCanUseDedicatedEndpoint(t *testing.T) {
|
||
client := NewWithInboundBasicInfoClient("iotlcm.example.com:8110")
|
||
clientName, err := client.inboundBasicInfoAddress(context.Background())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if clientName != "iotlcm.example.com:8110" {
|
||
t.Fatalf("inbound basic info client = %q, want dedicated chan endpoint", clientName)
|
||
}
|
||
}
|
||
|
||
func TestInboundBasicInfoClientCanUseEnvEndpoint(t *testing.T) {
|
||
const endpoint = "iotlcm.example.com:8110"
|
||
t.Setenv(inboundBasicInfoClientNameEnv, endpoint)
|
||
client := New()
|
||
clientName, err := client.inboundBasicInfoAddress(context.Background())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if clientName != endpoint {
|
||
t.Fatalf("inbound basic info client = %q, want env endpoint", clientName)
|
||
}
|
||
}
|
||
|
||
func TestConfiguredInboundBasicInfoClientUsesDedicatedEndpoint(t *testing.T) {
|
||
if value := os.Getenv(inboundBasicInfoClientNameEnv); value != "" {
|
||
t.Setenv(inboundBasicInfoClientNameEnv, "")
|
||
}
|
||
client := New()
|
||
clientName, err := client.inboundBasicInfoAddress(context.Background())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(clientName, ":8110") {
|
||
t.Fatalf("inbound basic info client = %q, want dedicated chan endpoint", clientName)
|
||
}
|
||
}
|
||
|
||
func TestDecodeAsyncApnResponseKeepsUnconfirmedState(t *testing.T) {
|
||
body := []byte(`{"code":0,"data":[{"code":0,"resData":{"orderNum":"job-1","pending":true}},{"code":1,"message":"响应未确认","resData":{"orderNum":"job-2","uncertain":true}}]}`)
|
||
items, err := decodeResponse[model.SimCardApnChangeOutputItem](body)
|
||
if err != nil || len(items) != 2 {
|
||
t.Fatalf("解析异步响应失败:%v", err)
|
||
}
|
||
if !items[0].ResData.Pending || items[0].ResData.OrderNum != "job-1" || !items[1].ResData.Uncertain {
|
||
t.Fatalf("异步待查字段丢失:%+v", items)
|
||
}
|
||
}
|
||
|
||
func TestSimCardApnChangeProtocol(t *testing.T) {
|
||
input := model.SimCardApnChangeInput{
|
||
SimKeys: []*model.SimKey{{ChanKey: "channel-a", ICCID: "89860001"}},
|
||
ReqData: model.SimCardApnChangeInputItem{OperType: "0", ApnName: "CMIOT"},
|
||
}
|
||
body, err := json.Marshal(input)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const want = `{"simKeys":[{"chanKey":"channel-a","iccid":"89860001","imsi":"","msisdn":""}],"reqData":{"operType":"0","apnName":"CMIOT"}}`
|
||
if got := string(body); got != want {
|
||
t.Fatalf("request body = %s, want %s", got, want)
|
||
}
|
||
}
|
||
|
||
func TestDecodeSimCardApnChangeResponseKeepsFailedItems(t *testing.T) {
|
||
body := []byte(`{"code":0,"message":"ok","data":[{"code":0,"message":"success","simKey":{"iccid":"89860001"},"resData":{"orderNum":"order-1"}},{"code":4001,"message":"denied","simKey":{"iccid":"89860002"},"resData":{"orderNum":"order-2"}}]}`)
|
||
items, err := decodeResponse[model.SimCardApnChangeOutputItem](body)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(items) != 2 {
|
||
t.Fatalf("item count = %d, want 2", len(items))
|
||
}
|
||
if items[1].Code != 4001 || items[1].ResData.OrderNum != "order-2" {
|
||
t.Fatalf("failed item was not preserved: %+v", items[1])
|
||
}
|
||
}
|
||
|
||
func TestDecodeSimCardStatusChangeResponseKeepsFailedItems(t *testing.T) {
|
||
body := []byte(`{"code":0,"message":"ok","data":[{"code":0,"message":"success","simKey":{"iccid":"89860001"},"resData":{"orderNum":"order-1"}},{"code":4001,"message":"denied","simKey":{"iccid":"89860002"},"resData":{"orderNum":"order-2"}}]}`)
|
||
items, err := decodeResponse[model.SimCardStatusChangeOutputItem](body)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(items) != 2 {
|
||
t.Fatalf("item count = %d, want 2", len(items))
|
||
}
|
||
if items[1].Code != 4001 || items[1].ResData.OrderNum != "order-2" {
|
||
t.Fatalf("failed item was not preserved: %+v", items[1])
|
||
}
|
||
}
|