package iot import ( "context" "encoding/json" "errors" "strings" "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-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" ) func channelDisplayTestContext(tenant string, dept int64) context.Context { return contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ ID: 1, TenantId: tenant, DeptId: dept, Roles: []model.Role{{RoleId: consts.SuperAdminRoleId, DataScope: consts.SysRoleDataScopeAll}}, }}}) } func TestChannelDisplayScope(t *testing.T) { for _, tc := range []struct { name string dept *entity.SysDept err error want bool }{ {name: "top organization", dept: &entity.SysDept{DeptId: 100, ParentId: 0}, want: true}, {name: "top organization legacy ancestors", dept: &entity.SysDept{DeptId: 100, ParentId: 1, Ancestors: "0"}, want: true}, {name: "downstream super admin still uses alias", dept: &entity.SysDept{DeptId: 100, ParentId: 50, Ancestors: "0,50"}}, {name: "missing organization"}, {name: "different organization", dept: &entity.SysDept{DeptId: 999, ParentId: 0}}, {name: "lookup failure", err: errors.New("lookup failed")}, } { t.Run(tc.name, func(t *testing.T) { ctx := channelDisplayTestContext("tenant-A", 100) got := resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { return tc.dept, tc.err }) if got != tc.want { t.Fatalf("full name = %v, want %v", got, tc.want) } if got := canViewChannelFullName(ctx); got != tc.want { t.Fatalf("cached scope = %v", got) } }) } for _, ctx := range []context.Context{context.Background(), channelDisplayTestContext("", 100), channelDisplayTestContext("tenant-A", 0)} { if resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { t.Fatal("invalid identity should not query organizations") return nil, nil }) { t.Fatal("invalid identity got full names") } } } func TestChannelDisplayScopeCacheIdentity(t *testing.T) { ctx := channelDisplayTestContext("tenant-A", 100) calls := 0 load := func() (*entity.SysDept, error) { calls++ return &entity.SysDept{DeptId: contexts.GetDeptId(ctx), ParentId: 0}, nil } if !resolveChannelDisplayScope(ctx, load) || !resolveChannelDisplayScope(ctx, load) || calls != 1 { t.Fatal("scope not cached for same identity") } contexts.GetUser(ctx).DeptId = 200 resolveChannelDisplayScope(ctx, load) contexts.GetUser(ctx).TenantId = "tenant-B" resolveChannelDisplayScope(ctx, load) if calls != 3 { t.Fatalf("scope reused across organization or tenant: %d calls", calls) } } func TestChannelDisplayAliasAndExport(t *testing.T) { ctx := channelDisplayTestContext("tenant-A", 200) resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { return &entity.SysDept{DeptId: 200, ParentId: 100, Ancestors: "0,100"}, nil }) list := []*model.TSimCardInfoListModel{ {ChanName: "PRIVATE UPSTREAM NAME", ChanAlias: " 中国移动223 "}, {ChanName: "PRIVATE UPSTREAM NAME", ChanAlias: " "}, nil, } applyCardChannelDisplayNames(ctx, list) if list[0].ChanName != "中国移动223" || list[1].ChanName != "--" { t.Fatalf("name leak: %+v %+v", list[0], list[1]) } for _, card := range list[:2] { row := cancellationExportRow(&model.TSimCardCancellationListModel{ChanName: card.ChanName}) if row[7] != card.ChanName { t.Fatalf("export name differs: %v", row[7]) } } if channelDisplayName(true, "full name", "alias") != "full name" { t.Fatal("top organization name changed") } } func TestChannelDisplaySQL(t *testing.T) { db, err := gdb.New(gdb.ConfigNode{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close(context.Background()) }) if _, err = db.Exec(context.Background(), "CREATE TABLE channels(chan_name TEXT,chan_alias TEXT)"); err != nil { t.Fatal(err) } for _, alias := range []interface{}{"中国移动223", "", " ", nil} { if _, err = db.Exec(context.Background(), "INSERT INTO channels VALUES(?,?)", "PRIVATE UPSTREAM NAME", alias); err != nil { t.Fatal(err) } } for _, top := range []bool{false, true} { ctx := channelDisplayTestContext("tenant-A", 100) parent := int64(50) if top { parent = 0 } resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { return &entity.SysDept{DeptId: 100, ParentId: parent, Ancestors: "0,50"}, nil }) values, err := db.GetArray(ctx, "SELECT "+channelDisplayNameSQL(ctx, "channels")+" FROM channels ORDER BY rowid") if err != nil { t.Fatal(err) } for i, v := range values { want := "--" if i == 0 { want = "中国移动223" } if top { want = "PRIVATE UPSTREAM NAME" } if v.String() != want { t.Fatalf("top=%v row=%d got=%q want=%q", top, i, v.String(), want) } } } } // Exercise the real list query and export formatter against an isolated database. func TestChannelDisplayCancellationList(t *testing.T) { previous := gdb.GetConfig("default") if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil { t.Fatal(err) } db := g.DB() t.Cleanup(func() { _ = db.Close(context.Background()); _ = gdb.SetConfig(gdb.Config{"default": previous}) }) statements := []string{ "CREATE TABLE sys_dept(dept_id INTEGER,tenant_id TEXT,parent_id INTEGER,ancestors TEXT,dept_name TEXT,deleted_at TEXT)", "INSERT INTO sys_dept VALUES(100,'tenant-A',0,'0','Top',NULL),(200,'tenant-A',100,'0,100','Customer',NULL)", "CREATE TABLE t_sim_channels(id INTEGER,temp_key TEXT,chan_key TEXT,tenant_id TEXT,chan_name TEXT,chan_alias TEXT)", "CREATE TABLE t_sim_channel_template(temp_key TEXT,temp_name TEXT,temp_model TEXT)", "INSERT INTO t_sim_channels(id,chan_key,tenant_id,chan_name,chan_alias) VALUES(1,'SHARED','tenant-A','PRIVATE NAME A','中国移动223'),(2,'SHARED','tenant-B','PRIVATE NAME B','Other tenant alias')", "CREATE TABLE t_sim_flow_package_info(package_code TEXT,package_name TEXT,tenant_id TEXT)", "INSERT INTO t_sim_flow_package_info VALUES('PKG','套餐A','tenant-A'),('PKG','套餐B','tenant-B')", "CREATE TABLE t_sim_card_cancellation(id INTEGER,iccid TEXT,msisdn TEXT,imsi TEXT,imei TEXT,dept_id INTEGER,chan_key TEXT,cancel_batch_no TEXT,cancelled_at TEXT,boss_card_status INTEGER,current_package_code TEXT,inbound_batch_no TEXT,outbound_batch_no TEXT,allocation_batch_no TEXT,card_type INTEGER,net_model INTEGER,remark TEXT,created_by INTEGER,tenant_id TEXT)", "INSERT INTO t_sim_card_cancellation(id,iccid,dept_id,chan_key,created_by,tenant_id,current_package_code) VALUES(1,'CARD-A',200,'SHARED',0,'tenant-A','PKG'),(2,'CARD-B',200,'SHARED',0,'tenant-B','PKG')", } for _, sql := range statements { if _, err := db.Exec(context.Background(), sql); err != nil { t.Fatal(err) } } for _, dept := range []int64{200, 100} { ctx := channelDisplayTestContext("tenant-A", dept) contexts.GetUser(ctx).Roles = []model.Role{{RoleId: 99, DataScope: consts.SysRoleDataScopeAll}} in := &model.TSimCardCancellationListParam{} in.Page = 1 in.PageSize = 20 list, total, err := NewIotTSimCardCancellation().List(ctx, in) if err != nil { t.Fatal(err) } if total != 1 || len(list) != 1 || list[0].Iccid != "CARD-A" { t.Fatalf("cross tenant rows: total=%d list=%+v", total, list) } want := "中国移动223" if dept == 100 { want = "PRIVATE NAME A" } if list[0].PackageName != "套餐A" { t.Fatalf("cross tenant package: %q", list[0].PackageName) } if list[0].ChanName != want { t.Fatalf("dept=%d name=%q want=%q", dept, list[0].ChanName, want) } if row := cancellationExportRow(list[0]); row[7] != want { t.Fatalf("export name=%v", row[7]) } channelQuery := &model.TSimChannelsListParam{} channelQuery.Page = 1 channelQuery.PageSize = 20 channelQuery.ChanName = want channels, count, err := NewIotTSimChannels().List(ctx, channelQuery) if err != nil { t.Fatal(err) } if count != 1 || len(channels) != 1 || channels[0].ChanName != want { t.Fatalf("channel options: count=%d rows=%+v", count, channels) } stockNames, err := loadStockChannelNames(ctx, []string{"SHARED"}) if err != nil || stockNames["SHARED"] != want { t.Fatalf("stock channel name=%v err=%v", stockNames, err) } channelView, err := NewIotTSimChannels().View(ctx, &model.TSimChannelsViewParam{Id: 1}) if err != nil || channelView == nil || channelView.ChanName != want { t.Fatalf("channel view=%+v err=%v", channelView, err) } } } func TestChannelDisplayCancellationMissingAlias(t *testing.T) { ctx := channelDisplayTestContext("tenant-A", 200) resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { return &entity.SysDept{DeptId: 200, ParentId: 100}, nil }) for _, alias := range []string{"", " "} { name := channelDisplayName(canViewChannelFullName(ctx), "PRIVATE NAME", alias) encoded, err := json.Marshal(&model.TSimCardCancellationListModel{ChanName: name}) if err != nil { t.Fatal(err) } if strings.Contains(string(encoded), "PRIVATE NAME") || name != "--" { t.Fatalf("response exposes full name: %s", encoded) } } }