chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
package mingqian
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
|
||||
)
|
||||
|
||||
func testAdapter(t *testing.T, handler http.HandlerFunc) *Adapter {
|
||||
t.Helper()
|
||||
server := httptest.NewServer(handler)
|
||||
t.Cleanup(server.Close)
|
||||
client, err := NewClient(server.URL, "test-app", "test-secret", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &Adapter{client: client}
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
params := map[string]string{"appId": "test-app", "reqtime": "2026-09-17", "timestamp": "1789603260000", "transid": "20260917080000000abcdef", "version": "3.0", "sign": "ignored"}
|
||||
if got := Sign(params, "test-secret"); got != "78e5d4f24b86159ad2f664ddbcc66282" {
|
||||
t.Fatalf("wrong sign: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireAndUsageMapping(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodPost || (request.URL.Path != usagePath && request.URL.Path != packageUsagePath) || request.Header.Get("Content-Type") != "application/json" {
|
||||
t.Error("incorrect HTTP contract")
|
||||
}
|
||||
var payload struct {
|
||||
Body map[string]string `json:"body"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
if payload.Body["sign"] != Sign(payload.Body, "test-secret") {
|
||||
t.Error("invalid signature")
|
||||
}
|
||||
stamp, err := strconv.ParseInt(payload.Body["timestamp"], 10, 64)
|
||||
if err != nil || len(payload.Body["timestamp"]) != 13 || time.Until(time.UnixMilli(stamp)) < 55*time.Second || time.Until(time.UnixMilli(stamp)) > 65*time.Second {
|
||||
t.Error("wrong millisecond timestamp offset")
|
||||
}
|
||||
if !regexp.MustCompile(`^\d{17}[0-9a-f]{6}$`).MatchString(payload.Body["transid"]) {
|
||||
t.Error("invalid transaction ID")
|
||||
}
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payload.Data), &data); err != nil || len(data.Numbers) != 3 {
|
||||
t.Error("data must be escaped JSON containing requested cards")
|
||||
}
|
||||
if request.URL.Path == packageUsagePath {
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"B","totalpackage":10240,"targetpackageusage":2048},{"iccid":"A","totalpackage":"10240","targetpackageusage":"8192"}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"B","use":2048},{"iccid":"A","use":"1024"}]}`)
|
||||
})
|
||||
result, err := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{ICCID: "A"}, {ICCID: "B"}, {ICCID: "missing"}}})
|
||||
if err != nil || len(result) != 3 {
|
||||
t.Fatalf("result: %v %v", result, err)
|
||||
}
|
||||
if result[0].Code != 0 || result[0].ResData.DataUsage != 1 || result[1].ResData.DataUsage != 2 || result[2].Code == 0 {
|
||||
t.Fatalf("incorrect per-card mapping: %+v", result)
|
||||
}
|
||||
if result[0].ResData.TotalFlow != nil {
|
||||
t.Fatal("monthly usage must not invent package total")
|
||||
}
|
||||
packageFlow := result[0].ResData.PackageFlow
|
||||
if packageFlow == nil || packageFlow.TotalFlow != 10240 || packageFlow.DataUsage != 8192 || packageFlow.RemainingFlow != 2048 {
|
||||
t.Fatalf("package cumulative flow must remain in MB and separate from monthly usage: %+v", packageFlow)
|
||||
}
|
||||
if result[1].ResData.PackageFlow == nil || result[1].ResData.PackageFlow.RemainingFlow != 8192 {
|
||||
t.Fatal("package response must match the requested card, not array position")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidUsage(t *testing.T) {
|
||||
for _, data := range []string{`{"iccid":"A"}`, `{"iccid":"A","use":null}`, `{"iccid":"A","use":-1}`, `{"iccid":"A","use":"NaN"}`, `{"iccid":"A","use":""}`} {
|
||||
t.Run(data, func(t *testing.T) {
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
fmt.Fprintf(writer, `{"code":200,"data":[%s]}`, data)
|
||||
})
|
||||
result, _ := adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if result[0].Code == 0 {
|
||||
t.Fatal("unknown usage reported as zero success")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardStates(t *testing.T) {
|
||||
for state, want := range map[int]int{1: 1, 2: 2, 3: 4, 4: 6, 5: 7, 6: 9} {
|
||||
got, err := cardStatus(&state)
|
||||
if err != nil || got != want {
|
||||
t.Fatalf("state %d mapped to %d: %v", state, got, err)
|
||||
}
|
||||
}
|
||||
unknown := 7
|
||||
if _, err := cardStatus(&unknown); err == nil {
|
||||
t.Fatal("expired state cannot be assumed to mean cancelled")
|
||||
}
|
||||
if _, err := cardStatus(nil); err == nil {
|
||||
t.Fatal("missing state must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAndBasicInfo(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls++
|
||||
var payload struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
json.NewDecoder(request.Body).Decode(&payload)
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
json.Unmarshal([]byte(payload.Data), &data)
|
||||
if len(data.Numbers) > 50 {
|
||||
t.Error("query batch exceeds 50")
|
||||
}
|
||||
rows := []map[string]any{}
|
||||
for _, value := range data.Numbers {
|
||||
rows = append(rows, map[string]any{"iccid": value, "msisdn": "144000", "state": 2, "statename": "active", "activedate": "2026-01-01"})
|
||||
}
|
||||
json.NewEncoder(writer).Encode(map[string]any{"code": 200, "data": rows})
|
||||
})
|
||||
keys := []*model.SimKey{}
|
||||
for index := 0; index < 51; index++ {
|
||||
keys = append(keys, &model.SimKey{ICCID: strconv.Itoa(index)})
|
||||
}
|
||||
result, _ := adapter.QuerySimBasicInfo(context.Background(), &model.SimBasicInfoInput{SimKeys: keys})
|
||||
if calls != 2 || len(result) != 51 {
|
||||
t.Fatalf("calls=%d rows=%d", calls, len(result))
|
||||
}
|
||||
for _, item := range result {
|
||||
if item.Code != 0 || item.ResData.ActiveDate != "" || item.ResData.IMSI != "" {
|
||||
t.Fatal("incorrect or fabricated basic fields")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationsAreSafe(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls++
|
||||
if request.URL.Path != "/simlist/test-resume" && request.URL.Path != "/simlist/get-sim-switchoff-by-number" {
|
||||
t.Error("wrong operation route")
|
||||
}
|
||||
fmt.Fprint(writer, `{"code":200,"data":[{"iccid":"A","issuccess":1},{"iccid":"B","issuccess":0}]}`)
|
||||
})
|
||||
input := &model.SimCardStatusChangeInput{SimKeys: []*model.SimKey{{ICCID: "A"}, {ICCID: "B"}}, ReqData: model.SimCardStatusChangeInputItem{OperType: "0"}}
|
||||
result, _ := adapter.ChangeSimStatus(context.Background(), input)
|
||||
if calls != 0 || result[0].Code == 0 {
|
||||
t.Fatal("operations must be disabled by default")
|
||||
}
|
||||
adapter.operations = true
|
||||
input.ReqData.OperType = "1"
|
||||
result, _ = adapter.ChangeSimStatus(context.Background(), input)
|
||||
if calls != 0 || result[0].Code == 0 {
|
||||
t.Fatal("unconfirmed resume path must not send any request")
|
||||
}
|
||||
adapter.resumePath = "/simlist/test-resume"
|
||||
for _, operation := range []string{"0", "1"} {
|
||||
input.ReqData.OperType = operation
|
||||
result, _ = adapter.ChangeSimStatus(context.Background(), input)
|
||||
if result[0].Code != 0 || result[1].Code == 0 {
|
||||
t.Fatal("must inspect per-card success flags")
|
||||
}
|
||||
}
|
||||
before := calls
|
||||
input.ReqData.OperType = "invalid"
|
||||
adapter.ChangeSimStatus(context.Background(), input)
|
||||
if calls != before {
|
||||
t.Fatal("invalid operation sent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseFailuresAndNoRetry(t *testing.T) {
|
||||
for _, body := range []string{`{}`, `{"code":401,"msg":"test-secret"}`, `{"code":200}`, `{"code":200,"data":null}`, `{"code":200,"data":[{"iccid":"A","state":2},{"iccid":"A","state":3}]}`} {
|
||||
t.Run(body, func(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) { calls++; fmt.Fprint(writer, body) })
|
||||
result, _ := adapter.QuerySimCardStatus(context.Background(), &model.SimCardStatusInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if calls != 1 || result[0].Code == 0 || strings.Contains(result[0].Message, "test-secret") {
|
||||
t.Fatal("invalid response accepted, retried or leaked secret")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
cfg := &model.SimChannelConfigItemCache{Template: &entity.TSimChannelTemplate{TempConfig: `{"api":{"timeout":"1s"}}`}, Channel: &entity.TSimChannels{AppKey: "app", AppSecret: "secret"}}
|
||||
adapter, err := New(cfg)
|
||||
if err != nil || adapter.operations {
|
||||
t.Fatalf("invalid default configuration: %v", err)
|
||||
}
|
||||
cfg.Channel.AppSecret = ""
|
||||
if _, err := New(cfg); err == nil {
|
||||
t.Fatal("missing secret accepted")
|
||||
}
|
||||
cfg.Channel.AppSecret = "secret"
|
||||
for _, raw := range []string{`{"api":`, `{"api":{"timeout":"0s"}}`, `{"api":{"resumePath":"/simlist/get-sim-switchoff-by-number"}}`} {
|
||||
cfg.Template.TempConfig = raw
|
||||
if _, err := New(cfg); err == nil {
|
||||
t.Fatal("invalid config accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationBatchLimitAndHTTPFailure(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
calls++
|
||||
var payload struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var data struct {
|
||||
Numbers []string `json:"numberArray"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payload.Data), &data); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(data.Numbers) > 20 {
|
||||
t.Error("operation batch exceeds 20")
|
||||
}
|
||||
writer.WriteHeader(http.StatusTooManyRequests)
|
||||
})
|
||||
adapter.operations = true
|
||||
keys := make([]*model.SimKey, 21)
|
||||
for index := range keys {
|
||||
keys[index] = &model.SimKey{ICCID: strconv.Itoa(index)}
|
||||
}
|
||||
output, err := adapter.ChangeSimStatus(context.Background(), &model.SimCardStatusChangeInput{SimKeys: keys, ReqData: model.SimCardStatusChangeInputItem{OperType: "0"}})
|
||||
if err != nil || calls != 2 || len(output) != 21 {
|
||||
t.Fatalf("calls=%d len=%d err=%v", calls, len(output), err)
|
||||
}
|
||||
for _, item := range output {
|
||||
if item.Code == 0 || !strings.Contains(item.Message, "429") {
|
||||
t.Fatal("HTTP failure must not appear as successful operation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountGateSharing(t *testing.T) {
|
||||
first, err := NewClient("http://127.0.0.1", "same-account", "secret", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := NewClient("http://127.0.0.1", "same-account", "secret", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.gate != second.gate {
|
||||
t.Fatal("same account must share rate limiter")
|
||||
}
|
||||
first.gate.mu.Lock()
|
||||
first.gate.next = time.Now().Add(time.Second)
|
||||
first.gate.mu.Unlock()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := second.gate.wait(ctx); err == nil {
|
||||
t.Fatal("rate limit wait must honor context timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelledAndUnsupportedDoNotCall(t *testing.T) {
|
||||
calls := 0
|
||||
adapter := testAdapter(t, func(writer http.ResponseWriter, request *http.Request) { calls++ })
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
adapter.QuerySimCardStatus(ctx, &model.SimCardStatusInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
adapter.QuerySimDataUsage(context.Background(), &model.SimDataUsageInput{SimKeys: []*model.SimKey{nil, {IMSI: "only-imsi"}}})
|
||||
adapter.QuerySimImei(context.Background(), &model.SimImeiInput{SimKeys: []*model.SimKey{{ICCID: "A"}}})
|
||||
if calls != 0 {
|
||||
t.Fatal("unexpected upstream request")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user