88dfe5c640
iotlcm-openapi:对外 OpenAPI 网关(V3 接口 + 旧版兼容接口)。
- Go module:iotlcm-openapi → git.5iot.com/iotlcm-public/iotlcm-openapi(178 文件)
- 凭据与地址脱敏:JWT signingKey、MySQL DSN、Redis 口令、
RabbitMQ DSN 全部改为 ${*} 占位;私网 IP → 文档地址段
- 7 处真实手机号 → 合法 11 位编造号
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像,移除 GOPRIVATE 内网设置
- README 重写(原为 GoFrame 模板占位文案),补 Apache-2.0 LICENSE,
LF 行尾 + .gitattributes
299 lines
10 KiB
Go
299 lines
10 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|
||
}
|