Files

298 lines
10 KiB
Go
Raw Permalink 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 query
import (
"context"
"encoding/json"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
"github.com/gogf/gf/v2/util/gconv"
// 触发 service.RegisterIotTSimCardInfo,否则 loadSimBatchCards 会 panic
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/openapiiot"
)
// setupImeiBatchDB 建一张最小可用的卡表并灌入覆盖各分支的样本数据。
//
// 样本设计(对齐 §7 测试场景):
// CARD-A 有 imei → 正常命中
// CARD-B imei 为空 → 卡存在但上游未上报值
// CARD-C 有 imei → 正常命中(入库带的值)
// CARD-D 属于另一个部门 → 数据权限过滤后视为 notFound
func setupImeiBatchDB(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)
}
statements := []string{
`CREATE TABLE t_sim_card_info (
id INTEGER PRIMARY KEY,
iccid TEXT, msisdn TEXT, imsi TEXT, imei TEXT,
boss_card_status INTEGER, used_flow_mb REAL,
imei_sync_time DATETIME, imei_update_time DATETIME,
dept_id INTEGER, tenant_id TEXT
)`,
// FilterAuth 的 GetDeptAndSub 会查 sys_dept(本部门及以下数据权限)
`CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, ancestors TEXT)`,
`INSERT INTO t_sim_card_info
(id, iccid, msisdn, imsi, imei, boss_card_status, used_flow_mb, imei_sync_time, imei_update_time, dept_id, tenant_id)
VALUES
(1,'CARD-A','13800000001','460000000000001','865466070817587',2,10.5,'2026-10-12 04:31:07','2026-10-12 04:31:07',7,'A'),
(2,'CARD-B','13800000002','460000000000002','', 2,20.0,'2026-10-12 04:31:09',NULL, 7,'A'),
(3,'CARD-C','13800000003','460000000000003','123456789012345',2,30.0,NULL, '2026-09-01 08:00:00',7,'A'),
(4,'CARD-D','13800000004','460000000000004','999888777666555',2,40.0,'2026-10-12 04:31:11','2026-10-12 04:31:11',8,'A')`,
}
for _, statement := range statements {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
}
// apiDataScopeRoleId 对齐 middleware.go:28 的 apiDataScopeRoleId(包私有,此处复刻值)。
const apiDataScopeRoleId int64 = 17
// imeiBatchCtx 复现 ApiCtx 中间件注入的身份:API 客户固定 RoleId=17 +
// SysRoleDataScopeDeptAndBelow(对齐 middleware.go:99),使数据权限过滤真实生效。
func imeiBatchCtx() context.Context {
ctx := contexts.Set(context.Background(), &model.Context{
User: &model.Identity{
BaseClaims: model.BaseClaims{
ID: 0,
TenantId: "A",
DeptId: 7,
Roles: []model.Role{
{RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow},
},
},
},
AgentUser: &model.BaseClaimsApi{DeptId: 7, TenantId: "A"},
})
contexts.SetDataValue(ctx, "currentRoleId", gconv.String(apiDataScopeRoleId))
contexts.SetDataValue(ctx, "currentRoleDataScope", consts.SysRoleDataScopeDeptAndBelow)
return ctx
}
// TestQuerySimImeiBatchSemantics 覆盖 §7 T11/T12/T16 的核心语义:
// - 有值的卡返回 imei
// - imei 为空但卡存在 → matched=true 且 imei="",不算 notFound(T16)
// - 不存在的卡 → matched=false + CARD_NOT_FOUND(T12)
// - 跨部门的卡 → 数据权限过滤后视为 notFound(T13)
func TestQuerySimImeiBatchSemantics(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{
{Iccid: "CARD-A"},
{Iccid: "CARD-B"},
{Iccid: "CARD-C"},
{Iccid: "CARD-D"},
{Iccid: "CARD-MISSING"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if output.Total != 5 {
t.Fatalf("total = %d, want 5", output.Total)
}
// A/B/C 命中,D(跨部门)与 MISSING 未命中
if output.MatchedCount != 3 || output.NotFoundCount != 2 {
t.Fatalf("matched=%d notFound=%d, want 3/2", output.MatchedCount, output.NotFoundCount)
}
if len(output.Items) != 5 {
t.Fatalf("items = %d, want 5 (按输入顺序逐卡返回)", len(output.Items))
}
// 顺序必须与输入一致(契约:按输入顺序逐卡返回)。
// 注意:未命中的 item 不返回 iccid(omitempty),所以必须用下标访问,
// 不能用 iccid 建索引,否则未命中项会取到零值。
ordered := []string{"CARD-A", "CARD-B", "CARD-C", "CARD-D", "CARD-MISSING"}
for i, wantIccid := range ordered {
if i == 3 || i == 4 {
continue // D 与 MISSING 未命中,不返回 iccid
}
if output.Items[i].Iccid != wantIccid {
t.Fatalf("items[%d].iccid = %q, want %q(必须保持输入顺序)", i, output.Items[i].Iccid, wantIccid)
}
}
a, b, c := output.Items[0], output.Items[1], output.Items[2]
d, missing := output.Items[3], output.Items[4]
if !a.Matched || a.Imei != "865466070817587" {
t.Fatalf("CARD-A: %+v", a)
}
// CARD-B:imei 为空但 matched=true(关键语义,不算 notFound)
if !b.Matched {
t.Fatalf("CARD-B 应为 matched=true(卡存在,只是上游没给值): %+v", b)
}
if b.Imei != "" {
t.Fatalf("CARD-B imei 应为空串: %q", b.Imei)
}
// CARD-C:入库就带值
if !c.Matched || c.Imei != "123456789012345" {
t.Fatalf("CARD-C: %+v", c)
}
// CARD-D / MISSING:未命中,且不泄露 imei
for _, tc := range []struct {
name string
item model.QuerySimImeiBatchItem
}{{"CARD-D(跳部门)", d}, {"CARD-MISSING(不存在)", missing}} {
item := tc.item
if item.Matched {
t.Fatalf("%s 不应命中: %+v", tc.name, item)
}
if item.ErrorCode != "CARD_NOT_FOUND" {
t.Fatalf("%s errorCode = %q, want CARD_NOT_FOUND", tc.name, item.ErrorCode)
}
if item.Imei != "" {
t.Fatalf("%s 未命中时不应返回 imei: %q", tc.name, item.Imei)
}
}
}
// TestQuerySimImeiBatchValidation 覆盖 §7 T14/T15 的参数校验。
func TestQuerySimImeiBatchValidation(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
svc := NewQuerySim()
cases := []struct {
name string
in *model.QuerySimImeiBatchParam
}{
{"nil 参数", nil},
{"appId 为空", &model.QuerySimImeiBatchParam{Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}}}},
{"cards 为空", &model.QuerySimImeiBatchParam{AppId: "app"}},
}
for _, tc := range cases {
if _, err := svc.QuerySimImeiBatch(ctx, tc.in); err == nil {
t.Fatalf("%s: 应返回错误", tc.name)
}
}
// 超过 1000 张(T14)
tooMany := make([]model.QuerySimBatchCard, 0, querySimBatchMaxCards+1)
for i := 0; i <= querySimBatchMaxCards; i++ {
tooMany = append(tooMany, model.QuerySimBatchCard{Iccid: "CARD-" + string(rune('0'+i%10)) + string(rune('0'+i/10%10)) + string(rune('0'+i/100%10)) + string(rune('0'+i/1000%10))})
}
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{AppId: "app", Cards: tooMany}); err == nil {
t.Fatal("超过 1000 张应被拒绝")
}
// 重复标识(parseSimBatchIdentifiers 既有逻辑)
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}, {Iccid: "CARD-A"}},
}); err == nil {
t.Fatal("重复标识应被拒绝")
}
// 单标识超长(simBatchIdentifierMaxLen)
longIccid := make([]byte, simBatchIdentifierMaxLen+1)
for i := range longIccid {
longIccid[i] = '9'
}
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: string(longIccid)}},
}); err == nil {
t.Fatal("超长标识应被拒绝")
}
// 多标识互相矛盾:ICCID 属于 CARD-A 但 MSISDN 属于 CARD-B(既有校验逻辑)
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A", Msisdn: "13800000002"}},
}); err != nil {
t.Fatalf("多标识查询本身应合法: %v", err)
}
// 矛盾组合应返回 notFound 而非错误(simBatchCardMatchesIdentifier 拦下)
conflict, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A", Msisdn: "13800000002"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conflict.NotFoundCount != 1 || conflict.Items[0].Matched {
t.Fatalf("矛盾的 iccid+msisdn 组合应判为 notFound: %+v", conflict.Items[0])
}
}
// TestQuerySimImeiBatchByMsisdnAndImsi 验证三种标识都能定位到卡。
func TestQuerySimImeiBatchByMsisdnAndImsi(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{
{Msisdn: "13800000001"},
{Imsi: "460000000000003"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if output.MatchedCount != 2 {
t.Fatalf("matched = %d, want 2", output.MatchedCount)
}
for _, item := range output.Items {
if !item.Matched || item.Iccid == "" {
t.Fatalf("应通过 msisdn/imsi 定位到卡并回填 iccid: %+v", item)
}
}
}
// TestQuerySimImeiBatchItemJSONShape 锁死对外字段集:只允许
// matched / iccid / imei / errorCode / errorMessage,任何额外字段(例如
// imeiSyncTime、imeiUpdateTime)都会让该用例失败,避免内部字段泄露给客户。
func TestQuerySimImeiBatchItemJSONShape(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}, {Iccid: "CARD-MISSING"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
allowed := map[string]bool{
"matched": true, "iccid": true, "imei": true,
"errorCode": true, "errorMessage": true,
}
for i, item := range output.Items {
raw, marshalErr := json.Marshal(item)
if marshalErr != nil {
t.Fatalf("items[%d] 序列化失败: %v", i, marshalErr)
}
var fields map[string]any
if err := json.Unmarshal(raw, &fields); err != nil {
t.Fatalf("items[%d] 反序列化失败: %v", i, err)
}
for name := range fields {
if !allowed[name] {
t.Fatalf("items[%d] 出现非预期对外字段 %q: %s", i, name, raw)
}
}
}
}