package iot import ( "strings" "testing" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" ) func TestCardChannelAssocExistsSQLUsesChannelKeyOnly(t *testing.T) { query := cardChannelAssocExistsSQL("") if !strings.Contains(query, "c.`chan_key` = a.`channel_id`") { t.Fatalf("channel association must join by chan_key: %s", query) } if strings.Contains(query, "CAST(a.`channel_id`") { t.Fatalf("channel association must not cast channel keys: %s", query) } if strings.Contains(query, "c.`id`") { t.Fatalf("channel association must not join by channel primary key: %s", query) } } func TestShouldCacheDiagnoseRegionLimit(t *testing.T) { tests := []struct { name string tempKey string chanKey string want bool }{ {name: "mobile direct", tempKey: "sim_yd_v5", want: true}, {name: "mobile shared token", tempKey: "sim_onelink_daihe_v1", want: true}, {name: "shared token model", tempKey: "shared_token", want: true}, {name: "telecom", tempKey: "sim_dx_v1", want: false}, {name: "third party", tempKey: "sim_sdtywl_v1", want: false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := shouldCacheDiagnoseRegionLimit(test.tempKey, test.chanKey); got != test.want { t.Fatalf("shouldCacheDiagnoseRegionLimit(%q, %q) = %v, want %v", test.tempKey, test.chanKey, got, test.want) } }) } } func TestIsDiagnoseRegionLimitCacheable(t *testing.T) { tests := []struct { name string item *model.SimBaseRes[model.SimRegionLimitStatusItem] want bool }{ {name: "nil response"}, { name: "successful region status", item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{Code: 0, ResData: model.SimRegionLimitStatusItem{RegionLimitStatus: "1"}}, want: true, }, { name: "successful order status", item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{Code: 0, ResData: model.SimRegionLimitStatusItem{OrderStatus: "1"}}, want: true, }, { name: "successful trigger status", item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{Code: 0, ResData: model.SimRegionLimitStatusItem{OrderStatus: "0", LimitStatus: "0"}}, want: true, }, { name: "upstream failure with populated status", item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{Code: 9999, ResData: model.SimRegionLimitStatusItem{RegionLimitStatus: "1"}}, }, { name: "unknown status placeholders", item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{Code: 0, ResData: model.SimRegionLimitStatusItem{RegionLimitStatus: "2", OrderStatus: "-", LimitStatus: "-"}}, }, { name: "details without status", item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{Code: 0, ResData: model.SimRegionLimitStatusItem{LimitType: "1", LimitArea: "area"}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := isDiagnoseRegionLimitCacheable(test.item); got != test.want { t.Fatalf("isDiagnoseRegionLimitCacheable() = %v, want %v", got, test.want) } }) } } func TestDiagnoseRegionModuleDoesNotCooldownForInvalidCachedData(t *testing.T) { result := &simDiagnoseRegionQueryResult{ Cached: true, CachedAt: "2026-09-24 12:00:00", Item: &model.SimBaseRes[model.SimRegionLimitStatusItem]{ Code: 0, ResData: model.SimRegionLimitStatusItem{ RegionLimitStatus: "2", LimitStatus: "-", }, }, } status, message, updatedAt := diagnoseRegionModule(result) if status != "error" { t.Fatalf("status = %q, want error", status) } if strings.Contains(message, "一小时内只能查询一次") || strings.Contains(message, "后可再次查看") { t.Fatalf("invalid cached result must not show cooldown hint: %q", message) } if updatedAt != "" { t.Fatalf("updatedAt = %q, want empty for invalid cached data", updatedAt) } } func TestDiagnoseCachedModule(t *testing.T) { status, message := diagnoseCachedModule(true) if status != "cached" || !strings.Contains(message, "缓存") { t.Fatalf("cached module = (%q, %q)", status, message) } status, message = diagnoseCachedModule(false) if status != "success" || message != "" { t.Fatalf("fresh module = (%q, %q)", status, message) } } func TestApplyDiagnoseRegionLimit(t *testing.T) { tests := []struct { name string data model.SimRegionLimitStatusItem want model.TSimDiagnoseInfoItem }{ { name: "not ordered is not applicable", data: model.SimRegionLimitStatusItem{RegionLimitStatus: "1", OrderStatus: "1", LimitStatus: "0", LimitType: "0", LimitArea: "park"}, want: model.TSimDiagnoseInfoItem{RegionLimitStatus: emptyStringPtr("2"), RegionLimitOrderStatus: emptyStringPtr("1")}, }, { name: "ordered and disconnected is triggered", data: model.SimRegionLimitStatusItem{RegionLimitStatus: "0", OrderStatus: "0", LimitStatus: "0", LimitType: "4", LimitArea: "park"}, want: model.TSimDiagnoseInfoItem{RegionLimitStatus: emptyStringPtr("1"), RegionLimitOrderStatus: emptyStringPtr("0"), RegionLimitTriggerStatus: emptyStringPtr("0"), RegionLimitType: emptyStringPtr("4"), RegionLimitArea: emptyStringPtr("park")}, }, { name: "ordered and connected is not triggered", data: model.SimRegionLimitStatusItem{RegionLimitStatus: "1", OrderStatus: "0", LimitStatus: "1", LimitType: "1", LimitArea: "province"}, want: model.TSimDiagnoseInfoItem{RegionLimitStatus: emptyStringPtr("0"), RegionLimitOrderStatus: emptyStringPtr("0"), RegionLimitTriggerStatus: emptyStringPtr("1"), RegionLimitType: emptyStringPtr("1"), RegionLimitArea: emptyStringPtr("province")}, }, { name: "ordered network unknown", data: model.SimRegionLimitStatusItem{OrderStatus: "0", LimitStatus: "-", LimitType: "0"}, want: model.TSimDiagnoseInfoItem{RegionLimitStatus: emptyStringPtr("2"), RegionLimitOrderStatus: emptyStringPtr("0"), RegionLimitTriggerStatus: emptyStringPtr("-")}, }, { name: "channel without order status keeps normalized status", data: model.SimRegionLimitStatusItem{RegionLimitStatus: "2"}, want: model.TSimDiagnoseInfoItem{RegionLimitStatus: emptyStringPtr("2")}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := &model.TSimDiagnoseInfoOutput{Info: &model.TSimDiagnoseInfoItem{}} applyDiagnoseRegionLimit(result, &model.SimBaseRes[model.SimRegionLimitStatusItem]{ResData: test.data}) assertStringPtrValue(t, "region status", result.Info.RegionLimitStatus, test.want.RegionLimitStatus) assertStringPtrValue(t, "order status", result.Info.RegionLimitOrderStatus, test.want.RegionLimitOrderStatus) assertStringPtrValue(t, "trigger status", result.Info.RegionLimitTriggerStatus, test.want.RegionLimitTriggerStatus) assertStringPtrValue(t, "limit type", result.Info.RegionLimitType, test.want.RegionLimitType) assertStringPtrValue(t, "limit area", result.Info.RegionLimitArea, test.want.RegionLimitArea) }) } } func assertStringPtrValue(t *testing.T, name string, got *string, want *string) { t.Helper() if stringPtrValue(got) != stringPtrValue(want) { t.Fatalf("%s = %q, want %q", name, stringPtrValue(got), stringPtrValue(want)) } } func stringPtrValue(value *string) string { if value == nil { return "" } return *value } func TestValidateSimSessionResult(t *testing.T) { t.Run("rejects empty upstream output", func(t *testing.T) { if _, err := validateSimSessionResult(nil); err == nil { t.Fatal("expected an error for empty output") } }) t.Run("rejects upstream business failure", func(t *testing.T) { if _, err := validateSimSessionResult([]model.SimBaseRes[model.SimSessionItem]{{ Code: 1001, Message: "upstream unavailable", }}); err == nil { t.Fatal("expected an error for failed output") } }) t.Run("returns successful session output", func(t *testing.T) { want := model.SimSessionItem{Status: "0"} got, err := validateSimSessionResult([]model.SimBaseRes[model.SimSessionItem]{{ Code: 0, ResData: want, }}) if err != nil { t.Fatalf("validate session output: %v", err) } if got.ResData != want { t.Fatalf("got %+v, want %+v", got.ResData, want) } }) } // 13000 是移动 OneLink 在自身繁忙时随机返回的业务状态,属上游侧问题。 // 对用户展示时必须归属于上游,否则用户会误认为本平台故障。 func TestUpstreamBusyStatusIsAttributedToUpstream(t *testing.T) { t.Run("busy code is rewritten to upstream message", func(t *testing.T) { got := upstreamStatusDisplayMessage(simUpstreamBusyCode, "系统忙,请稍后再试") if got != simUpstreamBusyMessage { t.Fatalf("got %q, want %q", got, simUpstreamBusyMessage) } }) t.Run("busy message is recognized without the known code", func(t *testing.T) { got := upstreamStatusDisplayMessage(8888, "系统忙,请稍后再试") if got != simUpstreamBusyMessage { t.Fatalf("got %q, want %q", got, simUpstreamBusyMessage) } }) t.Run("forbidden message is rewritten separately", func(t *testing.T) { got := upstreamStatusDisplayMessage(0, "IP禁止访问") if got != simUpstreamBusyForbiddenMessage { t.Fatalf("got %q, want %q", got, simUpstreamBusyForbiddenMessage) } }) t.Run("unrelated business status keeps the original error", func(t *testing.T) { got := upstreamStatusDisplayMessage(11011, "卡号不存在") if got != "" { t.Fatalf("got %q, want empty so the caller keeps the original message", got) } }) } func TestNewSimDataUsageErrorHidesUpstreamBusy(t *testing.T) { err := newSimDataUsageError(simUpstreamBusyCode, "系统忙,请稍后再试") if err == nil { t.Fatal("expected an error") } if err.Error() != simUpstreamBusyMessage { t.Fatalf("got %q, want %q", err.Error(), simUpstreamBusyMessage) } if strings.Contains(err.Error(), "13000") { t.Fatalf("upstream status code must not leak to users: %q", err.Error()) } } func TestValidateSimCardStatusResultHidesUpstreamBusy(t *testing.T) { _, err := validateSimCardStatusResult([]model.SimBaseRes[model.SimCardStatusItem]{{ Code: simUpstreamBusyCode, Message: "系统忙,请稍后再试", }}, "同步卡状态") if err == nil { t.Fatal("expected an error") } if err.Error() != simUpstreamBusyMessage { t.Fatalf("got %q, want %q", err.Error(), simUpstreamBusyMessage) } } func TestValidateSimCardStatusResultRejectsUnsupportedValue(t *testing.T) { for _, status := range []int{-2, 3, 5, 10} { _, err := validateSimCardStatusResult([]model.SimBaseRes[model.SimCardStatusItem]{{ Code: 0, ResData: model.SimCardStatusItem{CardStatus: status}, }}, "sync status") if err == nil { t.Fatalf("unsupported status %d must not be written to card info", status) } } for _, status := range []int{0, 1, 2, 4, 6, 7, 8, 9} { _, err := validateSimCardStatusResult([]model.SimBaseRes[model.SimCardStatusItem]{{ Code: 0, ResData: model.SimCardStatusItem{CardStatus: status}, }}, "sync status") if err != nil { t.Fatalf("platform status %d was rejected: %v", status, err) } } } func TestFormatUpstreamTestMessage(t *testing.T) { tests := []struct { name string code int message string want string }{ {name: "busy code", code: simUpstreamBusyCode, message: "系统忙,请稍后再试", want: simUpstreamBusyMessage}, {name: "empty message", code: 0, message: " ", want: "上游未返回结果"}, {name: "other business message", code: 11011, message: "卡号不存在", want: "卡号不存在"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := formatUpstreamTestMessage(test.code, test.message); got != test.want { t.Fatalf("got %q, want %q", got, test.want) } }) } }