Files
iotlcm/internal/logic/upstreamapi/pack_chan_key_test.go
T
2026-10-29 14:59:32 +08:00

230 lines
7.8 KiB
Go

package upstreamapi
import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
)
type packChanKeyRedis struct {
gredis.Adapter
gredis.IGroupString
values map[string]string
mgetCalls int
setKeys []string
}
func (r *packChanKeyRedis) GroupGeneric() gredis.IGroupGeneric { return nil }
func (r *packChanKeyRedis) GroupHash() gredis.IGroupHash { return nil }
func (r *packChanKeyRedis) GroupList() gredis.IGroupList { return nil }
func (r *packChanKeyRedis) GroupPubSub() gredis.IGroupPubSub { return nil }
func (r *packChanKeyRedis) GroupScript() gredis.IGroupScript { return nil }
func (r *packChanKeyRedis) GroupSet() gredis.IGroupSet { return nil }
func (r *packChanKeyRedis) GroupSortedSet() gredis.IGroupSortedSet { return nil }
func (r *packChanKeyRedis) GroupString() gredis.IGroupString { return r }
func (r *packChanKeyRedis) MGet(_ context.Context, keys ...string) (map[string]*gvar.Var, error) {
r.mgetCalls++
values := make(map[string]*gvar.Var, len(keys))
for _, key := range keys {
values[key] = gvar.New(r.values[key])
}
return values, nil
}
func (r *packChanKeyRedis) Set(_ context.Context, key string, value any, _ ...gredis.SetOption) (*gvar.Var, error) {
r.values[key] = gvar.New(value).String()
r.setKeys = append(r.setKeys, key)
return gvar.New("OK"), nil
}
func TestPackChanKeyPreservesIdentifiersAcrossLookupBranches(t *testing.T) {
// 子进程隔离 GoFrame 缓存的默认 DB/Redis 实例,所有查询只使用临时 SQLite 和内存适配器。
const childEnv = "PACK_CHAN_KEY_TEST_CHILD"
if os.Getenv(childEnv) != "1" {
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
command := exec.CommandContext(t.Context(), executable, "-test.run=^TestPackChanKeyPreservesIdentifiersAcrossLookupBranches$", "-test.v")
command.Env = append(os.Environ(), childEnv+"=1")
output, err := command.CombinedOutput()
t.Logf("%s", output)
if err != nil {
t.Fatalf("隔离回归失败:%v", err)
}
return
}
ctx := context.Background()
cache := &packChanKeyRedis{}
gredis.RegisterAdapterFunc(func(*gredis.Config) gredis.Adapter { return cache })
gredis.SetConfig(&gredis.Config{Address: "in-memory-test"})
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{
Type: "sqlite", Name: filepath.Join(t.TempDir(), "pack-chan-key.db"),
}}); err != nil {
t.Fatal(err)
}
db := g.DB()
t.Cleanup(func() { _ = db.Close(ctx) })
if _, err := db.Exec(ctx, "CREATE TABLE t_sim_card_info (iccid TEXT PRIMARY KEY, chan_key TEXT)"); err != nil {
t.Fatal(err)
}
api := NewUpstreamAPI()
for _, branch := range []string{"redis", "database", "unmatched"} {
t.Run(branch, func(t *testing.T) {
cache.values = make(map[string]string)
cache.mgetCalls = 0
cache.setKeys = nil
if _, err := db.Exec(ctx, "DELETE FROM t_sim_card_info"); err != nil {
t.Fatal(err)
}
first := &model.SimKey{ICCID: "iccid-duplicate", MSISDN: "msisdn-first", IMSI: "imsi-first"}
second := &model.SimKey{ICCID: first.ICCID, MSISDN: "msisdn-second", IMSI: "imsi-second"}
other := &model.SimKey{ICCID: "iccid-other", MSISDN: "msisdn-other", IMSI: "imsi-other"}
inputs := []*model.SimKey{first, second, first, other}
wantChannel := ""
if branch != "unmatched" {
wantChannel = branch + "-channel"
for _, key := range []*model.SimKey{first, other} {
if branch == "redis" {
cache.values["sim:chan:iccid:"+key.ICCID] = wantChannel
} else if _, err := db.Exec(ctx, "INSERT INTO t_sim_card_info (iccid, chan_key) VALUES (?, ?)", key.ICCID, wantChannel); err != nil {
t.Fatal(err)
}
}
}
before := make([]model.SimKey, len(inputs))
for i, key := range inputs {
before[i] = *key
}
got, err := api.packChanKey(ctx, inputs)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || len(got[wantChannel]) != len(inputs) {
t.Fatalf("分组数量错误:%+v", got)
}
assertPackedSimKeys(t, got[wantChannel], inputs, before, wantChannel)
if cache.mgetCalls != 1 {
t.Fatalf("MGet 调用次数=%d,期望1", cache.mgetCalls)
}
wantSets := 0
if branch == "database" {
wantSets = 2
}
if len(cache.setKeys) != wantSets {
t.Fatalf("缓存回填数量=%d,期望%d", len(cache.setKeys), wantSets)
}
})
}
t.Run("mixed_with_explicit_channel", func(t *testing.T) {
cache.values = map[string]string{"sim:chan:iccid:redis-card": "redis-channel"}
if _, err := db.Exec(ctx, "DELETE FROM t_sim_card_info"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(ctx, "INSERT INTO t_sim_card_info (iccid, chan_key) VALUES ('database-card', 'database-channel')"); err != nil {
t.Fatal(err)
}
explicit := &model.SimKey{ChanKey: "explicit-channel", ICCID: "redis-card", MSISDN: "msisdn-explicit", IMSI: "imsi-explicit"}
inputs := []*model.SimKey{explicit}
wanted := map[string][]*model.SimKey{}
for _, tc := range []struct{ iccid, channel string }{
{"redis-card", "redis-channel"}, {"database-card", "database-channel"}, {"missing-card", ""},
} {
first := &model.SimKey{ICCID: tc.iccid, MSISDN: tc.iccid + "-msisdn-first", IMSI: tc.iccid + "-imsi-first"}
second := &model.SimKey{ICCID: tc.iccid, MSISDN: tc.iccid + "-msisdn-second", IMSI: tc.iccid + "-imsi-second"}
inputs = append(inputs, first, second)
wanted[tc.channel] = []*model.SimKey{first, second}
}
before := make(map[*model.SimKey]model.SimKey, len(inputs))
for _, key := range inputs {
before[key] = *key
}
got, err := api.packChanKey(ctx, inputs)
if err != nil {
t.Fatal(err)
}
if len(got) != 4 || len(got[explicit.ChanKey]) != 1 || got[explicit.ChanKey][0] != explicit {
t.Fatalf("显式通道分组被改变:%+v", got)
}
for channel, originals := range wanted {
assertPackedSimKeys(t, got[channel], originals, []model.SimKey{before[originals[0]], before[originals[1]]}, channel)
}
if *explicit != before[explicit] {
t.Fatal("显式通道原始输入被修改")
}
})
}
func assertPackedSimKeys(t *testing.T, got, originals []*model.SimKey, before []model.SimKey, channel string) {
t.Helper()
if len(got) != len(originals) {
t.Fatalf("输出数量=%d,期望%d", len(got), len(originals))
}
want := make(map[model.SimKey]int, len(originals))
for i, original := range originals {
if *original != before[i] {
t.Fatalf("原始输入被修改:%+v", original)
}
key := before[i]
key.ChanKey = channel
want[key]++
}
seen := make(map[*model.SimKey]bool, len(got))
for _, key := range got {
if key == nil || seen[key] {
t.Fatal("输出为空或复用了同一指针")
}
seen[key] = true
for _, original := range originals {
if key == original {
t.Fatal("输出复用了原始输入指针")
}
}
if want[*key] == 0 {
t.Fatalf("卡标识丢失或被另一条重复ICCID输入覆盖:%+v", key)
}
want[*key]--
}
for _, remaining := range want {
if remaining != 0 {
t.Fatal("输出未保留每条原始输入")
}
}
if len(got) > 1 {
other := *got[1]
got[0].MSISDN = "changed-output"
got[0].IMSI = "changed-output"
if *got[1] != other {
t.Fatal("修改一条输出影响了另一条输出")
}
}
for i, original := range originals {
if *original != before[i] {
t.Fatalf("修改输出影响了原始输入:%+v", original)
}
}
}
func TestPackChanKeyEmptyInput(t *testing.T) {
for _, inputs := range [][]*model.SimKey{nil, {}} {
got, err := NewUpstreamAPI().packChanKey(context.Background(), inputs)
if err != nil || got == nil || len(got) != 0 {
t.Fatalf("空输入结果=%+v,错误=%v", got, err)
}
}
}