Files

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")
}
}