sync: 同步 v1.0.4 开源代码并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 14:52:43 +08:00
parent 32e4316ee4
commit 6893e21f07
136 changed files with 9851 additions and 436 deletions
+394
View File
@@ -1,16 +1,399 @@
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, &notSent) || !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, &notSent) && 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"}},
@@ -89,6 +472,17 @@ func TestConfiguredInboundBasicInfoClientUsesDedicatedEndpoint(t *testing.T) {
}
}
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"}},