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