272 lines
8.8 KiB
Go
272 lines
8.8 KiB
Go
package simcardquery
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
|
)
|
|
|
|
func TestTemplateRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f, _, err := New().BaseTemplate(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
sheet := f.GetSheetName(0)
|
|
if err := f.SetCellStr(sheet, "A2", "89860000000000000001"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := f.WriteToBuffer()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lines, err := streamExcelInputs(ctx, b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(lines) != 1 || lines[0].Values[0] != "89860000000000000001" {
|
|
t.Fatalf("unexpected lines: %+v", lines)
|
|
}
|
|
}
|
|
|
|
func TestExcelInputLimit(t *testing.T) {
|
|
f := excelize.NewFile()
|
|
defer f.Close()
|
|
sheet := f.GetSheetName(0)
|
|
if err := f.SetCellStr(sheet, "A1", "ICCID"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 2; i <= model.SimCardQueryMaxRows+2; i++ {
|
|
cell, _ := excelize.CoordinatesToCellName(1, i)
|
|
if err := f.SetCellStr(sheet, cell, "89860000000000000001"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
b, err := f.WriteToBuffer()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := streamExcelInputs(context.Background(), b); err == nil {
|
|
t.Fatal("expected over-limit error")
|
|
}
|
|
}
|
|
|
|
func TestMatchCardAmbiguity(t *testing.T) {
|
|
cards := []*cardBrief{
|
|
{Id: 1, Iccid: "89860001", Msisdn: "10001", Imsi: "46000001"},
|
|
{Id: 2, Iccid: "89860002", Msisdn: "10002", Imsi: "46000001"},
|
|
}
|
|
a := indexCards(cards, func(c *cardBrief) string { return c.Iccid })
|
|
b := indexCards(cards, func(c *cardBrief) string { return c.Msisdn })
|
|
c := indexCards(cards, func(c *cardBrief) string { return c.Imsi })
|
|
if card, _ := matchCard(inputLine{Values: []string{"46000001"}}, a, b, c); card != nil {
|
|
t.Fatal("ambiguous IMSI must fail")
|
|
}
|
|
if card, reason := matchCard(inputLine{Values: []string{"89860002", "46000001"}}, a, b, c); card == nil || card.Id != 2 {
|
|
t.Fatalf("expected second card, got %+v %s", card, reason)
|
|
}
|
|
if card, _ := matchCard(inputLine{Values: []string{"89860001", "10002"}}, a, b, c); card != nil {
|
|
t.Fatal("mixed card values must fail")
|
|
}
|
|
}
|
|
|
|
func TestRangeValidation(t *testing.T) {
|
|
for _, pair := range [][3]string{{"1", "22", "iccid"}, {"aa", "bb", "iccid"}, {"22", "11", "iccid"}, {"11", "22", "imei"}} {
|
|
if parseRangeInputs(&model.SimCardQueryExportParam{StartNum: pair[0], EndNum: pair[1], Dimension: pair[2]}) == nil {
|
|
t.Fatalf("expected error: %v", pair)
|
|
}
|
|
}
|
|
if err := parseRangeInputs(&model.SimCardQueryExportParam{StartNum: "0011", EndNum: "0022", Dimension: "iccid"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestIsSafeIccid(t *testing.T) {
|
|
ok := []string{"89860000000000000001", "8986abc001", "ABC123"}
|
|
for _, v := range ok {
|
|
if !isSafeIccid(v) {
|
|
t.Fatalf("expected safe: %s", v)
|
|
}
|
|
}
|
|
// 单引号、空格、分号、括号都必须拒绝,否则拼进 SQL 会破坏语句结构。
|
|
bad := []string{"", "8986'001", "8986 001", "8986;001", "8986)001", strings.Repeat("1", 33)}
|
|
for _, v := range bad {
|
|
if isSafeIccid(v) {
|
|
t.Fatalf("expected unsafe: %q", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatMonthValuesThreeStates(t *testing.T) {
|
|
months := []string{"2026-09", "2026-08", "2026-07"}
|
|
|
|
// 查询成功:9 月有量、8 月记录为 0、7 月无记录 → 数值 / 0 / "-"
|
|
ok := &model.SimCardUsageHistoryRow{
|
|
MonthValues: map[string]float64{"2026-09": 12.3456, "2026-08": 0},
|
|
}
|
|
got := formatMonthValues(ok, months)
|
|
if got[0] != 12.346 {
|
|
t.Fatalf("9 月应导出数值,实际 %v", got[0])
|
|
}
|
|
if got[1] != float64(0) {
|
|
t.Fatalf("8 月零用量应导出 0,实际 %v", got[1])
|
|
}
|
|
if got[2] != usageNoDataText {
|
|
t.Fatalf("7 月无记录应导出 %q,实际 %v", usageNoDataText, got[2])
|
|
}
|
|
|
|
// 整卡查询失败:月份列一律留空(保证该列是纯数值),原因由「失败原因」列承载
|
|
failed := &model.SimCardUsageHistoryRow{
|
|
MonthValues: map[string]float64{"2026-09": 99},
|
|
UsageQueryFailed: true,
|
|
}
|
|
got = formatMonthValues(failed, months)
|
|
for i, v := range got {
|
|
if v != "" {
|
|
t.Fatalf("第 %d 列应留空,实际 %v", i+1, v)
|
|
}
|
|
}
|
|
if !strings.Contains(usageQueryFailedNote, "查询失败") {
|
|
t.Fatalf("失败原因文案应说明查询失败:%s", usageQueryFailedNote)
|
|
}
|
|
}
|
|
|
|
// TestFormatMonthValuesPartialHistory 覆盖「平台只同步了几个月」和「新卡没历史」两种常见情况:
|
|
// 这类卡不是查询失败,缺的月份必须显示 "-",不能整行留空或判失败。
|
|
func TestFormatMonthValuesPartialHistory(t *testing.T) {
|
|
months := []string{"2026-09", "2026-08", "2026-07", "2026-06", "2026-05", "2026-04"}
|
|
|
|
// 只有最近 3 个月有数据(新卡 / 平台同步只跑了 3 个月)
|
|
partial := &model.SimCardUsageHistoryRow{
|
|
MonthValues: map[string]float64{
|
|
"2026-09": 120.5,
|
|
"2026-08": 98,
|
|
"2026-07": 0,
|
|
},
|
|
}
|
|
got := formatMonthValues(partial, months)
|
|
if len(got) != len(months) {
|
|
t.Fatalf("列数应与月份数一致,实际 %d", len(got))
|
|
}
|
|
if got[0] != 120.5 {
|
|
t.Fatalf("有数据的月份应导出数值,实际 %v", got[0])
|
|
}
|
|
if got[2] != float64(0) {
|
|
t.Fatalf("有记录但用量为 0 应导出 0,实际 %v", got[2])
|
|
}
|
|
for i := 3; i < len(got); i++ {
|
|
if got[i] != usageNoDataText {
|
|
t.Fatalf("第 %d 列无记录应为 %q,实际 %v", i+1, usageNoDataText, got[i])
|
|
}
|
|
}
|
|
// 部分缺失不能被当成查询失败
|
|
if partial.UsageQueryFailed {
|
|
t.Fatal("缺月份不等于查询失败")
|
|
}
|
|
|
|
// 全新卡:12 个月一条记录都没有 → 全部 "-",仍然算查询成功
|
|
brandNew := &model.SimCardUsageHistoryRow{MonthValues: map[string]float64{}}
|
|
got = formatMonthValues(brandNew, months)
|
|
for i, v := range got {
|
|
if v != usageNoDataText {
|
|
t.Fatalf("新卡第 %d 列应为 %q,实际 %v", i+1, usageNoDataText, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildRangeRowsKeepsOneRowPerNumber 号段里只有部分号码可见时,
|
|
// 导出必须仍然每个号码一行:可见的出行数据,不可见的出「未查询到数据」,
|
|
// 总条数等于号段大小,客户才能按号对账。
|
|
func TestBuildRangeRowsKeepsOneRowPerNumber(t *testing.T) {
|
|
in := &model.SimCardQueryExportParam{
|
|
Dimension: "iccid",
|
|
StartNum: "89860000000000000001",
|
|
EndNum: "89860000000000000010",
|
|
}
|
|
// 10 个号里只有第 3、5、8 个在可见范围内
|
|
cards := []*cardBrief{
|
|
{Id: 1, Iccid: "89860000000000000003"},
|
|
{Id: 2, Iccid: "89860000000000000005"},
|
|
{Id: 3, Iccid: "89860000000000000008"},
|
|
}
|
|
stats := &resolveStats{}
|
|
rows := buildRangeRows(context.Background(), in, cards, stats)
|
|
|
|
if len(rows) != 10 {
|
|
t.Fatalf("应输出 10 行(与号段大小一致),实际 %d", len(rows))
|
|
}
|
|
hit, miss := 0, 0
|
|
seen := make(map[string]struct{}, len(rows))
|
|
for _, row := range rows {
|
|
if _, dup := seen[row.Iccid]; dup {
|
|
t.Fatalf("号码重复输出:%s", row.Iccid)
|
|
}
|
|
seen[row.Iccid] = struct{}{}
|
|
switch row.QueryResult {
|
|
case queryResultHit:
|
|
hit++
|
|
case queryResultMiss:
|
|
miss++
|
|
// 不能透露是因为权限查不到
|
|
if row.FailReason != rangeMissReason {
|
|
t.Fatalf("失败原因应为 %q,实际 %q", rangeMissReason, row.FailReason)
|
|
}
|
|
if strings.Contains(row.FailReason, "权限") {
|
|
t.Fatalf("失败原因不能提及权限:%s", row.FailReason)
|
|
}
|
|
}
|
|
}
|
|
if hit != 3 || miss != 7 {
|
|
t.Fatalf("成功/失败应为 3/7,实际 %d/%d", hit, miss)
|
|
}
|
|
if stats.hit != 3 || stats.miss != 7 {
|
|
t.Fatalf("统计应为 3/7,实际 %d/%d", stats.hit, stats.miss)
|
|
}
|
|
// 首尾号码要对得上,且保持等长(前导零不丢)
|
|
if _, ok := seen["89860000000000000001"]; !ok {
|
|
t.Fatal("缺少起始号码")
|
|
}
|
|
if _, ok := seen["89860000000000000010"]; !ok {
|
|
t.Fatal("缺少结束号码")
|
|
}
|
|
}
|
|
|
|
func TestRangeSizeAndLimit(t *testing.T) {
|
|
in := &model.SimCardQueryExportParam{StartNum: "89860000000000000001", EndNum: "89860000000000000010"}
|
|
size, err := rangeSize(in)
|
|
if err != nil || size != 10 {
|
|
t.Fatalf("号段大小应为 10,实际 %d err=%v", size, err)
|
|
}
|
|
// 超过单次上限的号段要在提交阶段直接拒绝,否则无法完整展开
|
|
big := &model.SimCardQueryExportParam{
|
|
Dimension: "iccid",
|
|
StartNum: "89860000000000000001",
|
|
EndNum: "89860000000000099999",
|
|
}
|
|
if err := parseRangeInputs(big); err == nil {
|
|
t.Fatal("超限号段应被拒绝")
|
|
}
|
|
}
|
|
|
|
func TestMonthsAndStats(t *testing.T) {
|
|
months := LastCompleteMonths(time.Date(2026, 10, 23, 14, 0, 0, 0, time.FixedZone("CST", 8*3600)), 12)
|
|
if len(months) != 12 || months[0] != "2026-09" || months[11] != "2025-10" {
|
|
t.Fatalf("unexpected months: %v", months)
|
|
}
|
|
stats := &resolveStats{}
|
|
stats.add(true)
|
|
if result, summary := stats.resultSummary(1); result != consts.BatchRunResultSuccess || !strings.Contains(summary, "成功 1 条") {
|
|
t.Fatalf("bad success: %d %s", result, summary)
|
|
}
|
|
stats.add(false)
|
|
if result, _ := stats.resultSummary(2); result != consts.BatchRunResultPartialSuccess {
|
|
t.Fatal("expected partial success")
|
|
}
|
|
}
|