package upstreamimpl import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "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 TestYdCommunicationBusinessType(t *testing.T) { for _, tc := range []struct { in string want string valid bool }{ {"0", "0", true}, // 平台 开 -> 7.2.1 businessType 开(同向) {"1", "1", true}, // 平台 停 -> 7.2.1 businessType 停 {"", "", false}, {"2", "", false}, } { got, ok := convertYdCommunicationBusinessType(context.Background(), tc.in) if got != tc.want || ok != tc.valid { t.Errorf("businessType(%q) = (%q,%v), want (%q,%v)", tc.in, got, ok, tc.want, tc.valid) } } } func TestYdCommunicationOperType(t *testing.T) { for _, tc := range []struct { in string want string valid bool }{ {"0", "1", true}, // 平台 开 -> 7.2.2 恢复(翻转) {"1", "0", true}, // 平台 停 -> 7.2.2 暂停 {"", "", false}, {"9", "", false}, } { got, ok := convertYdCommunicationOperType(context.Background(), tc.in) if got != tc.want || ok != tc.valid { t.Errorf("operType(%q) = (%q,%v), want (%q,%v)", tc.in, got, ok, tc.want, tc.valid) } } } type ydApnCapture struct { paths []string query []url.Values msisdns string } // shortenYdBatchPoll 把批量结果轮询周期压缩到毫秒级,返回恢复函数。 func shortenYdBatchPoll(timeout time.Duration) func() { oldInterval, oldTimeout := ydBatchResultPollInterval, ydBatchResultPollTimeout ydBatchResultPollInterval, ydBatchResultPollTimeout = time.Millisecond, timeout return func() { ydBatchResultPollInterval, ydBatchResultPollTimeout = oldInterval, oldTimeout } } func newYdApnChannelWithServer(server *httptest.Server) *sSimYD { return &sSimYD{ channelConfig: &model.SimChannelConfigItemCache{Channel: &entity.TSimChannels{ChanKey: "test", AppKey: "test"}}, tokenProvider: diagnosisTestToken{}, apiConfig: &oneLinkAPIConfig{BaseURL: server.URL + "/v5"}, } } func newYdApnTestChannel(capture *ydApnCapture) (*sSimYD, func()) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capture.paths = append(capture.paths, r.URL.Path) capture.query = append(capture.query, r.URL.Query()) switch { case strings.Contains(r.URL.Path, "sim-communication-function/batch"): capture.msisdns = r.URL.Query().Get("msisdns") _, _ = w.Write([]byte(`{"status":"0","message":"正确","result":[{"jobId":"JOB1"}]}`)) case strings.Contains(r.URL.Path, "sim-batch-result"): _, _ = w.Write([]byte(ydBatchResultAllSuccessBody(capture.msisdns))) default: _, _ = w.Write([]byte(`{"status":"0","message":"正确","result":[{"orderNum":"ORD1"}]}`)) } })) return newYdApnChannelWithServer(server), server.Close } // ydBatchResultAllSuccessBody 依据批量请求的 msisdns 拼一个"全部成功"的结果查询响应。 func ydBatchResultAllSuccessBody(msisdns string) string { items := make([]string, 0) for _, id := range strings.Split(msisdns, "_") { id = strings.TrimSpace(id) if id == "" { continue } items = append(items, fmt.Sprintf(`{"status":"0","message":"正确","resultType":"1","resultId":%q}`, id)) } return fmt.Sprintf(`{"status":"0","message":"正确","result":[{"jobStatus":"2","resultList":[%s]}]}`, strings.Join(items, ",")) } func ydSimKeysWithMsisdn(n int) []*model.SimKey { keys := make([]*model.SimKey, 0, n) for i := 0; i < n; i++ { keys = append(keys, &model.SimKey{ ICCID: fmt.Sprintf("898604F41523911%05d", i), MSISDN: fmt.Sprintf("14415436%05d", i), IMSI: fmt.Sprintf("46008743601%05d", i), }) } return keys } func ydApnCounts(paths []string) (batch, single, query int) { for _, p := range paths { switch { case strings.Contains(p, "sim-communication-function/batch"): batch++ case strings.Contains(p, "sim-batch-result"): query++ default: single++ } } return } // 少于 10 张:逐张走 7.2.1 单卡。 func TestYdApnChangeSmallBatchUsesSingle(t *testing.T) { defer shortenYdBatchPoll(2 * time.Second)() capture := &ydApnCapture{} sim, closeFn := newYdApnTestChannel(capture) defer closeFn() out, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: ydSimKeysWithMsisdn(5), ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0", ApnName: "CMIOT"}, }) if err != nil { t.Fatal(err) } if len(capture.paths) != 5 { t.Fatalf("expected 5 single calls, got %d (%v)", len(capture.paths), capture.paths) } for i, p := range capture.paths { if p != "/v5/ec/manage/sim-communication-service" { t.Fatalf("call %d unexpected path %s", i, p) } q := capture.query[i] if q.Get("functionType") != "01" || q.Get("businessType") != "0" || q.Get("apn") != "CMIOT" { t.Fatalf("call %d unexpected query %v", i, q) } if q.Get("iccid") == "" || q.Get("transid") == "" { t.Fatalf("call %d missing iccid/transid: %v", i, q) } } if len(out) != 5 || out[0].Code != 0 || out[0].ResData.OrderNum != "ORD1" { t.Fatalf("unexpected output %+v", out) } } // 10~100 张:一次走 7.2.2 批量,open 翻成 operType=1;受理后必须回查结果。 func TestYdApnChangeTenToHundredUsesBatch(t *testing.T) { defer shortenYdBatchPoll(2 * time.Second)() for _, n := range []int{10, 100} { capture := &ydApnCapture{} sim, closeFn := newYdApnTestChannel(capture) out, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: ydSimKeysWithMsisdn(n), ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0", ApnName: "CMIOT"}, }) if err != nil { t.Fatal(err) } batch, single, query := ydApnCounts(capture.paths) if batch != 1 || single != 0 || query == 0 { t.Fatalf("n=%d expected 1 batch + result query, got batch=%d single=%d query=%d (%v)", n, batch, single, query, capture.paths) } q := capture.query[0] if q.Get("serviceType") != "11" || q.Get("operType") != "1" || q.Get("apnName") != "CMIOT" { t.Fatalf("n=%d unexpected batch query %v", n, q) } if got := len(strings.Split(q.Get("msisdns"), "_")); got != n { t.Fatalf("n=%d msisdns count = %d", n, got) } if len(out) != n || out[0].Code != 0 || out[0].ResData.OrderNum != "JOB1" { t.Fatalf("n=%d unexpected output %+v", n, out) } closeFn() } } // 批量任务部分失败:结果查询返回的失败项必须如实回填,不能整批记成功。 func TestYdApnChangeBatchReflectsPartialFailure(t *testing.T) { defer shortenYdBatchPoll(2 * time.Second)() keys := ydSimKeysWithMsisdn(10) failId := keys[3].MSISDN server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "sim-communication-function/batch"): _, _ = w.Write([]byte(`{"status":"0","message":"正确","result":[{"jobId":"JOB9"}]}`)) case strings.Contains(r.URL.Path, "sim-batch-result"): items := make([]string, 0, len(keys)) for _, k := range keys { if k.MSISDN == failId { items = append(items, fmt.Sprintf(`{"status":"12047","message":"用户该服务已经是暂停状态,不能重复暂停","resultType":"1","resultId":%q}`, k.MSISDN)) continue } items = append(items, fmt.Sprintf(`{"status":"0","message":"正确","resultType":"1","resultId":%q}`, k.MSISDN)) } _, _ = w.Write([]byte(fmt.Sprintf(`{"status":"0","message":"正确","result":[{"jobStatus":"3","resultList":[%s]}]}`, strings.Join(items, ",")))) default: t.Fatalf("unexpected path %s", r.URL.Path) } })) defer server.Close() out, err := newYdApnChannelWithServer(server).ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: keys, ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0", ApnName: "CMIOT"}, }) if err != nil { t.Fatal(err) } if len(out) != 10 { t.Fatalf("output len = %d, want 10", len(out)) } failed := 0 for _, item := range out { if item.Code != 0 { failed++ if !strings.Contains(item.Message, "不能重复暂停") { t.Fatalf("unexpected failure message %q", item.Message) } } } if failed != 1 { t.Fatalf("failed count = %d, want 1 (%+v)", failed, out) } } // 轮询超时:未取得终态时按失败记录,并注明"未确认",避免误报成功。 func TestYdApnChangeBatchResultTimeout(t *testing.T) { defer shortenYdBatchPoll(20 * time.Millisecond)() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "sim-communication-function/batch"): _, _ = w.Write([]byte(`{"status":"0","message":"正确","result":[{"jobId":"JOB5"}]}`)) case strings.Contains(r.URL.Path, "sim-batch-result"): _, _ = w.Write([]byte(`{"status":"0","message":"正确","result":[{"jobStatus":"1","resultList":[]}]}`)) default: t.Fatalf("unexpected path %s", r.URL.Path) } })) defer server.Close() out, err := newYdApnChannelWithServer(server).ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: ydSimKeysWithMsisdn(10), ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0", ApnName: "CMIOT"}, }) if err != nil { t.Fatal(err) } if len(out) != 10 { t.Fatalf("output len = %d, want 10", len(out)) } for _, item := range out { if item.Code == 0 { t.Fatalf("timeout must not be reported as success: %+v", item) } if !strings.Contains(item.Message, "查询上游通信结果失败") { t.Fatalf("unexpected timeout message %q", item.Message) } } } // 停用数据:批量 operType 翻成 0,单卡 businessType=1。 func TestYdApnChangeStopDirection(t *testing.T) { defer shortenYdBatchPoll(2 * time.Second)() capture := &ydApnCapture{} sim, closeFn := newYdApnTestChannel(capture) defer closeFn() if _, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: ydSimKeysWithMsisdn(10), ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1", ApnName: "CMIOT"}, }); err != nil { t.Fatal(err) } capture2 := &ydApnCapture{} sim2, closeFn2 := newYdApnTestChannel(capture2) defer closeFn2() if _, err := sim2.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: ydSimKeysWithMsisdn(3), ReqData: model.SimCardOperateChangeApnInputItem{OperType: "1", ApnName: "CMIOT"}, }); err != nil { t.Fatal(err) } if capture.query[0].Get("operType") != "0" { t.Fatalf("batch stop operType = %q, want 0", capture.query[0].Get("operType")) } if capture2.query[0].Get("businessType") != "1" { t.Fatalf("single stop businessType = %q, want 1", capture2.query[0].Get("businessType")) } } // 超过 100 张:按 100 拆块,不足 10 的尾块退回单卡。 func TestYdApnChangeOverHundredSplits(t *testing.T) { defer shortenYdBatchPoll(2 * time.Second)() capture := &ydApnCapture{} sim, closeFn := newYdApnTestChannel(capture) defer closeFn() out, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: ydSimKeysWithMsisdn(105), ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0", ApnName: "CMIOT"}, }) if err != nil { t.Fatal(err) } batch, single, _ := ydApnCounts(capture.paths) if batch != 1 || single != 5 { t.Fatalf("expected 1 batch + 5 singles, got batch=%d single=%d (%v)", batch, single, capture.paths) } if len(out) != 105 { t.Fatalf("output len = %d, want 105", len(out)) } } // 批量里有卡缺 msisdn:缺号降级走 7.2.1,其余凑够 10 张仍走 7.2.2。 func TestYdApnChangeMissingMsisdnFallsBackToSingle(t *testing.T) { defer shortenYdBatchPoll(2 * time.Second)() capture := &ydApnCapture{} sim, closeFn := newYdApnTestChannel(capture) defer closeFn() keys := ydSimKeysWithMsisdn(15) keys[0].MSISDN = "" keys[5].MSISDN = "" keys[14].MSISDN = "" out, err := sim.ChangeSimApnFunction(context.Background(), &model.SimCardOperateChangeApnInput{ SimKeys: keys, ReqData: model.SimCardOperateChangeApnInputItem{OperType: "0", ApnName: "CMIOT"}, }) if err != nil { t.Fatal(err) } batch, single, _ := ydApnCounts(capture.paths) if batch != 1 || single != 3 { t.Fatalf("expected 1 batch + 3 singles, got batch=%d single=%d (%v)", batch, single, capture.paths) } if len(out) != 15 { t.Fatalf("output len = %d, want 15", len(out)) } for _, item := range out { if item.Code != 0 { t.Fatalf("unexpected failure item %+v", item) } } }