package app import ( "encoding/json" "strings" "testing" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" ) func TestAppDiagnosisDataServiceStatus(t *testing.T) { tests := []struct { name string raw string want string }{ {name: "disabled", raw: "0", want: "0"}, {name: "enabled", raw: "1", want: "1"}, {name: "unknown", raw: "2", want: "-1"}, {name: "empty", raw: "", want: ""}, {name: "invalid", raw: "unexpected", want: ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := appDiagnosisDataServiceStatus(test.raw) if test.want == "" { if got != nil { t.Fatalf("appDiagnosisDataServiceStatus(%q) = %q, want nil", test.raw, *got) } return } if got == nil || *got != test.want { t.Fatalf("appDiagnosisDataServiceStatus(%q) = %v, want %q", test.raw, got, test.want) } }) } } func TestAppDiagnosisDataServiceStatusFromApn(t *testing.T) { tests := []struct { name string raw json.RawMessage want string }{ {name: "mobile APN enabled", raw: json.RawMessage(`{"apnList":[{"apnName":"CMIOT","status":"1"}]}`), want: "1"}, {name: "mobile APN disabled", raw: json.RawMessage(`{"apnList":[{"apnName":"CMIOT","status":"0"}]}`), want: "0"}, {name: "ignore unrelated APN", raw: json.RawMessage(`{"apnList":[{"apnName":"OTHER","status":"0"},{"apnName":"cmiot","status":"1"}]}`), want: "1"}, {name: "no APN entry", raw: json.RawMessage(`{"status":"1","apnList":[]}`)}, {name: "other APN only", raw: json.RawMessage(`{"apnList":[{"apnName":"OTHER","status":"1"}]}`)}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := appDiagnosisDataServiceStatusFromApn(test.raw) if test.want == "" { if got != nil { t.Fatalf("status = %q, want no data", *got) } return } if got == nil || *got != test.want { t.Fatalf("status = %v, want %q", got, test.want) } }) } } func TestAppDiagnosisDataServiceStatusFromItems(t *testing.T) { items := []appUpstreamSimQueryItem{ {Code: 0, ResData: json.RawMessage(`{"apnList":[{"apnName":"OTHER","status":"0"}]}`)}, {Code: 5000, ResData: json.RawMessage(`{"apnList":[{"apnName":"CMIOT","status":"0"}]}`)}, {Code: 0, ResData: json.RawMessage(`{"apnList":[{"apnName":"CMIOT","status":"1"}]}`)}, } status := appDiagnosisDataServiceStatusFromItems(items) if status == nil || *status != "1" { t.Fatalf("status = %v, want enabled CMIOT from successful upstream item", status) } } func TestAppDiagnosisDataServiceStatusForCarrier(t *testing.T) { tests := []struct { name string tempKey string chanKey string raw json.RawMessage want string }{ {name: "mobile only reads CMIOT", tempKey: "sim_yd_v5", raw: json.RawMessage(`{"apnList":[{"apnName":"OTHER","status":"1"}]}`)}, {name: "telecom reads returned DNN", tempKey: "sim_dx_v1", raw: json.RawMessage(`{"apnList":[{"apnName":"ctnet","status":"1"}]}`), want: "1"}, {name: "telecom reads disabled DNN", tempKey: "sim_dx_v1", raw: json.RawMessage(`{"apnList":[{"apnName":"ctnet","status":"0"}]}`), want: "0"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := appDiagnosisDataServiceStatusFromItemsForChannel([]appUpstreamSimQueryItem{{Code: 0, ResData: test.raw}}, test.tempKey, test.chanKey) if test.want == "" { if got != nil { t.Fatalf("status = %q, want no data", *got) } return } if got == nil || *got != test.want { t.Fatalf("status = %v, want %q", got, test.want) } }) } } func TestAppDiagnosisAPNStatusRequiresAgreement(t *testing.T) { tests := []struct { name string raw string want string }{ {name: "all enabled", raw: `{"apnList":[{"apnName":"ctnet","status":"1"},{"apnName":"ctiot","status":"1"}]}`, want: "1"}, {name: "all stopped", raw: `{"apnList":[{"apnName":"ctnet","status":"0"},{"apnName":"ctiot","status":"0"}]}`, want: "0"}, {name: "mixed", raw: `{"apnList":[{"apnName":"ctnet","status":"1"},{"apnName":"ctiot","status":"0"}]}`}, {name: "mixed reversed", raw: `{"apnList":[{"apnName":"ctiot","status":"0"},{"apnName":"ctnet","status":"1"}]}`}, {name: "missing status", raw: `{"apnList":[{"apnName":"ctnet","status":"1"},{"apnName":"ctiot"}]}`}, {name: "unknown status", raw: `{"apnList":[{"apnName":"ctnet","status":"1"},{"apnName":"ctiot","status":"-1"}]}`}, {name: "missing name", raw: `{"apnList":[{"status":"1"}]}`}, {name: "no mobile preference for telecom", raw: `{"apnList":[{"apnName":"CMIOT","status":"1"},{"apnName":"ctnet","status":"0"}]}`}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := appDiagnosisDataServiceStatusFromItemsForChannel([]appUpstreamSimQueryItem{{Code: 0, ResData: json.RawMessage(test.raw)}}, "sim_dx_v1", "channel_dx") if test.want == "" { if got != nil { t.Fatalf("status = %q, want no data", *got) } } else if got == nil || *got != test.want { t.Fatalf("status = %v, want %q", got, test.want) } }) } } func TestQueryDiagnosisFlowByCardRejectsUnsupportedPeriod(t *testing.T) { _, err := NewAppDiagnosis().queryDiagnosisFlowByCard(nil, nil, "12M") if err == nil || !strings.Contains(err.Error(), "3M") { t.Fatalf("unsupported period error = %v, want query type hint", err) } } func TestValidateDiagnosisCardChannelInfo(t *testing.T) { tests := []struct { name string info *appCardChannelInfo wantErr bool }{ {name: "missing channel", wantErr: true}, {name: "empty channel key", info: &appCardChannelInfo{}, wantErr: true}, {name: "disabled channel", info: &appCardChannelInfo{ChanKey: "channel-1", ChanStatus: 1}, wantErr: true}, {name: "enabled channel", info: &appCardChannelInfo{ChanKey: "channel-1", ChanStatus: 0}}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := validateDiagnosisCardChannelInfo(test.info) if test.wantErr { if err == nil || !strings.Contains(err.Error(), appDiagnosisNotIncludedMessage) { t.Fatalf("error = %v, want %q", err, appDiagnosisNotIncludedMessage) } return } if err != nil { t.Fatalf("error = %v, want nil", err) } }) } } func TestAppDiagnosisUnsupportedMessageIncludesUnsubscribedCapabilities(t *testing.T) { for _, message := range []string{"接口未订购", "能力未开通", "服务未开通", "未开通该接口", "通道未申请"} { if !appDiagnosisIsUnsupportedMessage(message) { t.Fatalf("message %q should be treated as unsupported", message) } } } func TestAppDiagnosisModuleReason(t *testing.T) { tests := []struct { name string status string message string want string }{ {name: "unsubscribed", status: "skipped", message: "该卡未订购机卡绑定服务", want: "not_subscribed"}, {name: "unsupported", status: "skipped", message: "运营商暂不支持", want: "unsupported"}, {name: "empty skipped", status: "skipped", want: "unsupported"}, {name: "no data", status: "skipped", message: "上游会话信息未返回有效结果", want: "no_data"}, {name: "query error", status: "error", message: "查询超时", want: "query_error"}, {name: "success", status: "success"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := appDiagnosisModuleReason(test.status, test.message); got != test.want { t.Fatalf("appDiagnosisModuleReason() = %q, want %q", got, test.want) } }) } } func TestAppSetDiagnosisModulePreservesStructuredReason(t *testing.T) { res := &model.AppDiagnosisInfoModel{} appSetDiagnosisModule(res, "cardBind", "skipped", "该卡未订购机卡绑定服务", false, "") module := res.Modules["cardBind"] if module == nil || module.Reason != "not_subscribed" || module.Message != "" { t.Fatalf("module = %+v, want hidden not_subscribed reason", module) } } func TestAppDiagnosisDoesNotExposeUpstreamErrors(t *testing.T) { for _, message := range []string{"missing msisdn", "Missing MSISDN"} { res := &model.AppDiagnosisInfoModel{} status, displayMessage := appDiagnosisUpstreamFailure("上游机卡绑定状态查询失败", 9999, message) appSetDiagnosisModule(res, "cardBind", status, displayMessage, false, "") module := res.Modules["cardBind"] if module == nil || module.Status != "error" || module.Message != appDiagnosisRetryMessage { t.Fatalf("message %q: module = %+v", message, module) } } } func TestAppDiagnosisNormalizesLegacyBusyMessage(t *testing.T) { status, displayMessage := appDiagnosisUpstreamFailure("上游实时流量查询失败", 13000, "系统忙,请稍后再试") if status != "error" || displayMessage != appDiagnosisRetryMessage { t.Fatalf("status = %q, message = %q", status, displayMessage) } } func TestAppDiagnosisHidesConnectivityFailureMessage(t *testing.T) { status, displayMessage := appDiagnosisUpstreamFailure("上游实时流量查询失败", 9999, "dial tcp 127.0.0.1:8510: connection refused") if status != "error" || displayMessage != "" { t.Fatalf("status = %q, message = %q", status, displayMessage) } } func TestAppCachedDiagnosisRegionModuleTreatsCachedDataAsSuccess(t *testing.T) { raw, err := json.Marshal(map[string]string{ "regionLimitStatus": "1", "orderStatus": "0", "limitStatus": "1", "limitType": "province", "limitArea": "山东", }) if err != nil { t.Fatalf("marshal region data: %v", err) } item := &appUpstreamSimQueryItem{ Code: 1001, Message: "上游一小时内限制重复查询", ResData: raw, } status, message := appCachedDiagnosisRegionModule(item) if status != "cached" { t.Fatalf("status = %q, want cached", status) } if !strings.Contains(message, "区域限制结果来自缓存") { t.Fatalf("message = %q, want cache hint", message) } res := &model.AppDiagnosisInfoModel{Info: &model.AppDiagnosisInfoItem{}} appDiagnosisApplyRegionLimit(res, item) if res.Info.RegionLimitStatus == nil || *res.Info.RegionLimitStatus != "1" { t.Fatalf("RegionLimitStatus = %#v, want 1", res.Info.RegionLimitStatus) } if res.Info.RegionLimitArea == nil || *res.Info.RegionLimitArea != "山东" { t.Fatalf("RegionLimitArea = %#v, want 山东", res.Info.RegionLimitArea) } } func TestAppCachedDiagnosisRegionModuleFailsEmptyCacheData(t *testing.T) { status, _ := appCachedDiagnosisRegionModule(&appUpstreamSimQueryItem{Code: 0}) if status != "error" { t.Fatalf("status = %q, want error", status) } } func TestAppDiagnosisApplyRateLimitIncludesApnDetails(t *testing.T) { inner, err := json.Marshal(map[string]any{ "rateLimitStatus": "0", "rateLimitReason": "套餐达量", "rateLimitDetail": "APN触发限速", "apnDetails": []map[string]any{ { "apnName": "cmiot", "apnStatus": "0", "modifyTime": "2026-08-28 10:00:00", "overUseDetails": "流量超额", "apnServiceUsageState": "超额", "pccDetails": []map[string]any{ { "pccServiceCode": "PCC001", "pccName": "白名单", "pccPolicyStatus": "1", "pccServiceUsageState": "正常", "modifyTime": "2026-08-28 10:01:00", }, }, }, }, }) if err != nil { t.Fatalf("marshal rate limit data: %v", err) } raw, err := json.Marshal(map[string]any{"resData": json.RawMessage(inner)}) if err != nil { t.Fatalf("marshal wrapped rate limit data: %v", err) } res := &model.AppDiagnosisInfoModel{Info: &model.AppDiagnosisInfoItem{}} appDiagnosisApplyRateLimit(res, &appUpstreamSimQueryItem{Code: 0, ResData: raw}) if res.Info.RateLimitStatus == nil || *res.Info.RateLimitStatus != "0" { t.Fatalf("RateLimitStatus = %#v, want 0", res.Info.RateLimitStatus) } if len(res.Info.RateLimitApnDetails) != 1 { t.Fatalf("RateLimitApnDetails length = %d, want 1", len(res.Info.RateLimitApnDetails)) } apn := res.Info.RateLimitApnDetails[0] if apn.ApnName != "cmiot" || len(apn.PccDetails) != 1 || apn.PccDetails[0].PccServiceCode != "PCC001" { t.Fatalf("unexpected APN detail: %#v", apn) } } func TestSanitizeOpenDiagnosisInfoKeepsImeiAndRemovesOtherNetworkData(t *testing.T) { res := &model.AppDiagnosisInfoModel{ Info: &model.AppDiagnosisInfoItem{ Iccid: appDiagnosisStringPtr("8986001234567890123"), Imsi: appDiagnosisStringPtr("460001234567890"), Msisdn: appDiagnosisStringPtr("13800008000"), RateLimitApnDetails: []model.AppDiagnosisRateLimitApnItem{ {ApnName: "cmiot"}, }, }, Session: &model.AppDiagnosisSessionItem{ ApnId: appDiagnosisStringPtr("internet"), Imei: appDiagnosisStringPtr("860123456789012"), Ipv4: appDiagnosisStringPtr("192.0.2.10"), Ipv6: appDiagnosisStringPtr("2001:db8::1"), }, Card: &model.AppCardModel{ Iccid: "8986001234567890123", Msisdn: "13800008000", Imsi: "460001234567890", Imei: "860123456789012", Ipv4: "192.0.2.10", }, } sanitizeOpenDiagnosisInfo(res) if got := *res.Info.Iccid; got != "8986****0123" { t.Fatalf("masked ICCID = %q", got) } if got := *res.Info.Msisdn; got != "1380****8000" { t.Fatalf("masked MSISDN = %q", got) } if res.Info.Imsi != nil || len(res.Info.RateLimitApnDetails) != 0 || res.Session.ApnId != nil || res.Session.Ipv4 != nil { t.Fatal("sensitive diagnosis fields were not removed") } if res.Session.Imei == nil || *res.Session.Imei != "860123456789012" { t.Fatalf("session IMEI = %#v, want public 30-day IMEI", res.Session.Imei) } if res.Card.Imsi != "" || res.Card.Imei != "860123456789012" || res.Card.Ipv4 != "" { t.Fatal("sensitive card fields were not removed") } } func TestAppDiagnosisLatestImeiUsesSuccessfulGatewayResult(t *testing.T) { raw, err := json.Marshal(map[string]any{ "resData": map[string]string{"imei": "861789024851199"}, }) if err != nil { t.Fatalf("marshal IMEI response: %v", err) } items := []appUpstreamSimQueryItem{ {Code: 5000, ResData: json.RawMessage(`{"imei":"ignored"}`)}, {Code: 0, ResData: raw}, } if got := appDiagnosisLatestImei(items); got != "861789024851199" { t.Fatalf("latest IMEI = %q, want 861789024851199", got) } } func TestAppDiagnosisStatusMappingsMatchConsoleContract(t *testing.T) { tests := []struct { name string input string want string mapper func(string) *string }{ {name: "imei separated", input: "0", want: "0", mapper: appDiagnosisImeiStatus}, {name: "imei bound", input: "1", want: "1", mapper: appDiagnosisImeiStatus}, {name: "imei unknown", input: "2", want: "2", mapper: appDiagnosisImeiStatus}, {name: "online", input: "0", want: "1", mapper: appDiagnosisOnlineStatus}, {name: "offline", input: "-1", want: "0", mapper: appDiagnosisOnlineStatus}, {name: "unknown", input: "-2", want: "2", mapper: appDiagnosisOnlineStatus}, {name: "human limited", input: "1", want: "0", mapper: appDiagnosisHumanLimitStatus}, {name: "human normal", input: "0", want: "1", mapper: appDiagnosisHumanLimitStatus}, {name: "human unknown", input: "2", want: "2", mapper: appDiagnosisHumanLimitStatus}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := test.mapper(test.input) if got == nil || *got != test.want { t.Fatalf("mapping %q = %#v, want %q", test.input, got, test.want) } }) } } func TestAppDiagnosisBindStatusSupportsTelecomCardBindingType(t *testing.T) { tests := []struct { bindStatus string cardBindingType string want string }{ {bindStatus: "0", cardBindingType: "1", want: "0"}, {bindStatus: "1", cardBindingType: "2", want: "1"}, {bindStatus: "2", cardBindingType: "1", want: "2"}, {cardBindingType: "1", want: "1"}, {cardBindingType: "5", want: "1"}, {cardBindingType: "-1", want: "2"}, {cardBindingType: "unknown", want: "2"}, } for _, test := range tests { if got := appDiagnosisBindStatus(test.bindStatus, test.cardBindingType); got != test.want { t.Fatalf("bindStatus=%q cardBindingType=%q => %q, want %q", test.bindStatus, test.cardBindingType, got, test.want) } } } func TestAppDiagnosisShouldCacheRegionLimitMatchesConsoleChannels(t *testing.T) { tests := []struct { name string tempKey string chanKey string want bool }{ {name: "mobile template", tempKey: "sim_yd", want: true}, {name: "mobile mock channel", chanKey: "channel_yd_mock", want: true}, {name: "onelink", chanKey: "onelink_shared", want: true}, {name: "shared token", tempKey: "shared_token", want: true}, {name: "telecom", tempKey: "sim_dx", chanKey: "channel_dx", want: false}, {name: "unicom", tempKey: "sim_lt", chanKey: "channel_lt", want: false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := appDiagnosisShouldCacheRegionLimit(test.tempKey, test.chanKey); got != test.want { t.Fatalf("appDiagnosisShouldCacheRegionLimit(%q, %q) = %v, want %v", test.tempKey, test.chanKey, got, test.want) } }) } } func TestAppDiagnosisSessionStatusNormalizesUpstreamValues(t *testing.T) { tests := map[string]string{ "0": "0", "01": "0", "ONLINE": "0", "-1": "-1", "00": "-1", "OFFLINE": "-1", "-2": "-2", "02": "-2", "2": "-2", "": "-2", } for input, want := range tests { if got := appDiagnosisSessionStatus(input); got != want { t.Fatalf("session status %q => %q, want %q", input, got, want) } } }