chore: 初始开源发布

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
This commit is contained in:
MiFaZhan
2026-10-22 15:42:29 +08:00
commit 96b1edff45
191 changed files with 30805 additions and 0 deletions
@@ -0,0 +1,200 @@
package upstreamimpl
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"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") != "138****8000" || 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: "138****8000"}}})
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: " 8986110000000000000 "}, param: "iccid", value: "8986110000000000000"},
{name: "imsi", key: &model.SimKey{IMSI: "460030000000000"}, param: "imsi", value: "460030000000000"},
{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") != "138****8000" {
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: "138****8000", 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)
}
})
}
}
// 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: "138****8000"},
{name: "carrier rejected", operType: "1", msisdn: "138****8000", body: `<?xml version="1.0"?><SvcCont><Response><RspCode>1000</RspCode><RspDesc>rejected</RspDesc></Response></SvcCont>`, wantRequests: 1},
{name: "missing result code", operType: "0", msisdn: "138****8000", 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: "138****8000", body: `<?xml version="1.0"?><SvcCont><Response><RspCode>0000</RspCode><RspDesc>ok</RspDesc></Response></SvcCont>`, wantRequests: 1},
{name: "malformed response", operType: "0", msisdn: "138****8000", body: `{"code":"0"}`, wantRequests: 1},
{name: "HTTP failed no retry", operType: "0", msisdn: "138****8000", 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)
}
})
}
}