230 lines
7.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|