Files
iotlcm/server/internal/logic/iot/t_sim_card_info_test.go
T
2026-10-29 14:36:49 +08:00

431 lines
16 KiB
Go

package iot
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
)
func TestSimCardBatchCondition(t *testing.T) {
condition, args, err := simCardBatchCondition(context.Background(), "\n iccid-1 \n\t\niccid-2 13800000000 460001234567890\n")
if err != nil {
t.Fatalf("parse batch input: %v", err)
}
if !strings.HasPrefix(condition, "((") || !strings.HasSuffix(condition, "))") {
t.Fatalf("batch conditions must be grouped: %s", condition)
}
if !strings.Contains(condition, "iccid IN (?) OR msisdn IN (?) OR imsi IN (?)") {
t.Fatalf("single values must be folded into IN lists: %s", condition)
}
if strings.Count(condition, "(iccid = ? OR msisdn = ? OR imsi = ?)") != 3 {
t.Fatalf("three-value row must require every value on the same card: %s", condition)
}
if strings.Contains(condition, " = ? AND msisdn = ? AND imsi = ?") {
t.Fatalf("field positions must not be assumed: %s", condition)
}
wantArgs := []interface{}{
"iccid-1", "iccid-1", "iccid-1",
"iccid-2", "iccid-2", "iccid-2",
"13800000000", "13800000000", "13800000000",
"460001234567890", "460001234567890", "460001234567890",
}
if len(args) != len(wantArgs) {
t.Fatalf("got %d args, want %d: %#v", len(args), len(wantArgs), args)
}
for i := range wantArgs {
if args[i] != wantArgs[i] {
t.Fatalf("arg %d = %#v, want %#v", i, args[i], wantArgs[i])
}
}
}
func TestSimCardBatchConditionAcceptsAnyTwoValuesPerRow(t *testing.T) {
condition, args, err := simCardBatchCondition(context.Background(),
"89860000000000000001 13800000000\n13800000001 460001234567891\n")
if err != nil {
t.Fatalf("two values per row must be accepted: %v", err)
}
if strings.Count(condition, "(iccid = ? OR msisdn = ? OR imsi = ?)") != 4 {
t.Fatalf("each value must match any field, two rows expected four groups: %s", condition)
}
if len(args) != 12 {
t.Fatalf("got %d args, want 12: %#v", len(args), args)
}
if !strings.Contains(condition, " AND ") {
t.Fatalf("values of one row must be paired on the same card: %s", condition)
}
}
func TestSimCardBatchConditionRejectsEmptyAndInvalidInput(t *testing.T) {
for _, input := range []string{"", " \n\t ", "one two three four", "one two three four five"} {
if _, _, err := simCardBatchCondition(context.Background(), input); err == nil {
t.Errorf("expected input %q to be rejected", input)
}
}
}
func TestSimCardBatchConditionLimitsNonEmptyRowsBeforeDeduplication(t *testing.T) {
input := strings.Repeat("same-number\n", simCardBatchMaxRows)
condition, args, err := simCardBatchCondition(context.Background(), input)
if err != nil {
t.Fatalf("accept exactly %d rows: %v", simCardBatchMaxRows, err)
}
if len(args) != 3 {
t.Fatalf("duplicate single values must collapse to one IN list, got %d args: %s", len(args), condition)
}
input += "one-more\n"
if _, _, err := simCardBatchCondition(context.Background(), input); err == nil {
t.Fatal("expected 1001 rows to be rejected even when values repeat")
}
}
func TestSimCardBatchInputPresenceAndExportValidation(t *testing.T) {
for _, payload := range []string{`{"batchInput":""}`, `{"batchInput":" \n\t"}`} {
var in model.TSimCardInfoListParam
if err := json.Unmarshal([]byte(payload), &in); err != nil {
t.Fatal(err)
}
if in.BatchInput == nil {
t.Fatal("显式空批量输入不能视为省略字段")
}
if _, _, err := NewIotTSimCardInfo().List(context.Background(), &in); err == nil {
t.Fatal("列表应在访问数据库前拒绝空批量输入")
}
if err := NewIotTSimCardInfo().Export(context.Background(), &in); err == nil {
t.Fatal("导出应在提交任务前拒绝空批量输入")
}
}
var omitted model.TSimCardInfoListParam
if err := json.Unmarshal([]byte(`{}`), &omitted); err != nil {
t.Fatal(err)
}
if omitted.BatchInput != nil {
t.Fatal("省略批量字段应保持原列表查询语义")
}
}
func TestSimCardBatchExportKeepsListReuse(t *testing.T) {
data, err := os.ReadFile("t_sim_card_info.go")
if err != nil {
t.Fatal(err)
}
source := string(data)
for _, fragment := range []string{"QueryParam: in,", "query := *in", "s.List(ctx, &query)", "mod := s.Model(ctx)", "mod = mod.Where(batchCondition, batchArgs...)"} {
if !strings.Contains(source, fragment) {
t.Errorf("批量查询必须保留现有权限及导出复用: %s", fragment)
}
}
}
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)
}
})
}
}