Files
iotlcm/internal/repository/imei/repository_test.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。

- Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件)
- 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、
  库名 xiujie_admin → iotlcm、哨兵名占位
- 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异,
  不破坏「校验 MSISDN 格式」等测试断言)
- 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像
  digest 记录
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23),
  移除写死的 git 凭据
- README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0
  LICENSE,LF 行尾 + .gitattributes

已知:3 个测试包在源仓同一提交上同样失败(client/upstream、
service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

282 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package imei
import (
"context"
"strings"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// setupDB builds a minimal card table mirroring the columns ApplyBatch writes.
func setupDB(t *testing.T, ctx context.Context) {
t.Helper()
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = g.DB().Close(ctx) })
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, `CREATE TABLE t_sim_card_info (
id INTEGER PRIMARY KEY,
imei TEXT,
imei_sync_time DATETIME,
imei_update_time DATETIME
)`); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, `INSERT INTO t_sim_card_info (id, imei, imei_sync_time, imei_update_time) VALUES
(1,'OLD-IMEI-1',NULL,NULL),
(2,'OLD-IMEI-2',NULL,NULL),
(3,'OLD-IMEI-3',NULL,NULL),
(4,'',NULL,NULL)`); err != nil {
t.Fatal(err)
}
}
// readCard returns the stored imei and both timestamps for one card.
func readCard(t *testing.T, ctx context.Context, id int64) (imei string, syncTime, updateTime *string) {
t.Helper()
var row struct {
Imei string `orm:"imei"`
ImeiSyncTime *string `orm:"imei_sync_time"`
ImeiUpdateTime *string `orm:"imei_update_time"`
}
if err := g.DB().Model("t_sim_card_info").Where("id", id).Scan(&row); err != nil {
t.Fatal(err)
}
return row.Imei, row.ImeiSyncTime, row.ImeiUpdateTime
}
// TestApplyBatchValueChangeWritesAllThreeColumns 值变更时三列同时写。
func TestApplyBatchValueChangeWritesAllThreeColumns(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: true, ValueChanged: true, Imei: "865466070817587"},
}); err != nil {
t.Fatalf("ApplyBatch: %v", err)
}
imei, syncTime, updateTime := readCard(t, ctx, 1)
if imei != "865466070817587" {
t.Fatalf("imei = %q", imei)
}
if syncTime == nil || updateTime == nil {
t.Fatalf("值变更应同时写两个时间戳: sync=%v update=%v", syncTime, updateTime)
}
}
// TestApplyBatchHeartbeatDoesNotTouchValue 心跳只写 imei_sync_time,
// 绝不改动 imei 与 imei_update_time。这是 §5.2 的核心保证。
func TestApplyBatchHeartbeatDoesNotTouchValue(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: true, ValueChanged: false},
}); err != nil {
t.Fatalf("ApplyBatch: %v", err)
}
imei, syncTime, updateTime := readCard(t, ctx, 1)
if imei != "OLD-IMEI-1" {
t.Fatalf("心跳不得改动 imei: %q", imei)
}
if syncTime == nil {
t.Fatal("心跳应写 imei_sync_time")
}
if updateTime != nil {
t.Fatalf("心跳不得写 imei_update_time: %v", *updateTime)
}
}
// TestApplyBatchUnsyncedIsSkipped 未同步(查询失败)的卡完全不入库。
func TestApplyBatchUnsyncedIsSkipped(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: false},
{CardID: 2, Synced: false, ValueChanged: true, Imei: "999999999999999"},
}); err != nil {
t.Fatalf("ApplyBatch: %v", err)
}
for _, id := range []int64{1, 2} {
imei, syncTime, updateTime := readCard(t, ctx, id)
if syncTime != nil || updateTime != nil {
t.Fatalf("card %d: 未同步不应写时间戳: sync=%v update=%v", id, syncTime, updateTime)
}
_ = imei
}
}
// TestApplyBatchMixedGroups 混合批次:值变更与心跳分组写入,互不干扰。
func TestApplyBatchMixedGroups(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: true, ValueChanged: true, Imei: "865466070817587"}, // 值变更
{CardID: 2, Synced: true, ValueChanged: false}, // 心跳
{CardID: 3, Synced: false}, // 失败跳过
{CardID: 4, Synced: true, ValueChanged: true, Imei: "111222333444555"}, // 从空到有值
}); err != nil {
t.Fatalf("ApplyBatch: %v", err)
}
if imei, syncTime, updateTime := readCard(t, ctx, 1); imei != "865466070817587" || syncTime == nil || updateTime == nil {
t.Fatalf("card 1 应值变更: imei=%q sync=%v update=%v", imei, syncTime, updateTime)
}
if imei, syncTime, updateTime := readCard(t, ctx, 2); imei != "OLD-IMEI-2" || syncTime == nil || updateTime != nil {
t.Fatalf("card 2 应仅心跳: imei=%q sync=%v update=%v", imei, syncTime, updateTime)
}
if imei, syncTime, updateTime := readCard(t, ctx, 3); imei != "OLD-IMEI-3" || syncTime != nil || updateTime != nil {
t.Fatalf("card 3 应完全不动: imei=%q sync=%v update=%v", imei, syncTime, updateTime)
}
if imei, syncTime, updateTime := readCard(t, ctx, 4); imei != "111222333444555" || syncTime == nil || updateTime == nil {
t.Fatalf("card 4 应从空到有值: imei=%q sync=%v update=%v", imei, syncTime, updateTime)
}
}
// TestApplyBatchRejectsInvalidImeiValue 非数字值必须被拒绝,
// 因为 CASE WHEN 是拼接 SQL,值本身没有绑定参数保护。
func TestApplyBatchRejectsInvalidImeiValue(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
malicious := []string{
"865466070817587'", // 引号逃逸
"865466070817587'; DROP TABLE x; --", // 注入尝试
"865466070817587 OR 1=1", // 逻辑注入
"abc", // 非数字
"865466070817587;999999999999999", // 分号拼接(本链路不支持多值)
strings.Repeat("9", 33), // 超长
}
for _, value := range malicious {
err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: true, ValueChanged: true, Imei: value},
})
if err == nil {
t.Fatalf("非法值 %q 应被拒绝", value)
}
}
// 拒绝后数据必须保持原样
if imei, _, _ := readCard(t, ctx, 1); imei != "OLD-IMEI-1" {
t.Fatalf("被拒绝的批次不得改动数据: %q", imei)
}
}
// TestApplyBatchAcceptsBoundaryLengths 15/17 位正常 IMEI 与列宽边界值应通过。
func TestApplyBatchAcceptsBoundaryLengths(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
values := []string{
"865466070817587", // 15 位
"86546607081758712", // 17 位
strings.Repeat("9", 32), // varchar(32) 上限
}
for i, value := range values {
cardID := int64(i + 1)
if err := repo.ApplyBatch(ctx, []Result{
{CardID: cardID, Synced: true, ValueChanged: true, Imei: value},
}); err != nil {
t.Fatalf("合法值 %q 应被接受: %v", value, err)
}
if imei, _, _ := readCard(t, ctx, cardID); imei != value {
t.Fatalf("card %d: imei = %q, want %q", cardID, imei, value)
}
}
}
// TestApplyBatchRejectsInvalidCardID 非法卡 ID 拒绝整批。
func TestApplyBatchRejectsInvalidCardID(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: true, ValueChanged: true, Imei: "865466070817587"},
{CardID: 0, Synced: true, ValueChanged: true, Imei: "111222333444555"},
}); err == nil {
t.Fatal("非法 cardId 应被拒绝")
}
}
// TestApplyBatchRejectsOversizedBatch 超大批次拒绝,防止单条 SQL 无限增长。
func TestApplyBatchRejectsOversizedBatch(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
results := make([]Result, 0, maxImeiSyncBatch+1)
for i := 0; i <= maxImeiSyncBatch; i++ {
results = append(results, Result{CardID: int64(i + 1), Synced: true})
}
if err := repo.ApplyBatch(ctx, results); err == nil {
t.Fatal("超大批次应被拒绝")
}
}
// TestApplyBatchEmptyIsNoop 空批次不发 SQL。
func TestApplyBatchEmptyIsNoop(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, nil); err != nil {
t.Fatalf("空批次应无操作: %v", err)
}
if err := repo.ApplyBatch(ctx, []Result{}); err != nil {
t.Fatalf("空切片应无操作: %v", err)
}
}
// TestApplyBatchAllUnsyncedIssuesNoSQL 全部未同步时不应发任何 UPDATE。
func TestApplyBatchAllUnsyncedIssuesNoSQL(t *testing.T) {
ctx := context.Background()
setupDB(t, ctx)
repo := NewRepository()
if err := repo.ApplyBatch(ctx, []Result{
{CardID: 1, Synced: false},
{CardID: 2, Synced: false},
}); err != nil {
t.Fatalf("ApplyBatch: %v", err)
}
for _, id := range []int64{1, 2} {
if _, syncTime, updateTime := readCard(t, ctx, id); syncTime != nil || updateTime != nil {
t.Fatalf("card %d 不应被改动", id)
}
}
}
// TestImeiValuePattern documents the accepted value grammar.
func TestImeiValuePattern(t *testing.T) {
valid := []string{"1", "865466070817587", strings.Repeat("9", 32)}
invalid := []string{"", "abc", "865466070817587'", "865-466", " 865466070817587", strings.Repeat("9", 33), "865466070817587;1"}
for _, value := range valid {
if !imeiValuePattern.MatchString(value) {
t.Fatalf("%q 应被接受", value)
}
}
for _, value := range invalid {
if imeiValuePattern.MatchString(value) {
t.Fatalf("%q 应被拒绝", value)
}
}
}