package upstreamapi import ( "context" "path/filepath" "testing" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/channelconfig" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) type normalGateTestChannel struct { model.SimCardChannelI } type normalGateTestTemplate struct { calls int lastConfig *model.SimChannelConfigItemCache } func (f *normalGateTestTemplate) NewTemp(_ context.Context, cfg *model.SimChannelConfigItemCache) model.SimCardChannelI { f.calls++ f.lastConfig = cfg return &normalGateTestChannel{} } // Exercise real ORM decoding and config versioning as well as both creation paths. // All SQL runs against a temporary SQLite file, never a configured environment DB. func TestChannelNormalCreationGate(t *testing.T) { ctx := context.Background() if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{ Type: "sqlite", Name: filepath.Join(t.TempDir(), "channels.db"), }}); err != nil { t.Fatal(err) } db := g.DB() t.Cleanup(func() { _ = db.Close(ctx) }) execSQL := func(query string, args ...any) { t.Helper() if _, err := db.Exec(ctx, query, args...); err != nil { t.Fatal(err) } } execSQL("CREATE TABLE t_sim_channels (id INTEGER PRIMARY KEY, chan_key TEXT, temp_key TEXT, chan_name TEXT, is_normal INTEGER NULL)") execSQL("CREATE TABLE t_sim_channel_template (id INTEGER PRIMARY KEY, temp_key TEXT, temp_model TEXT)") execSQL("INSERT INTO t_sim_channel_template (id, temp_key, temp_model) VALUES (1, 'gate_test', 'direct')") oldConfig := service.ChannelConfig() t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) }) for _, path := range []string{"loop", "refresh"} { t.Run(path, func(t *testing.T) { for _, tc := range []struct { name string value any eligible bool }{ {"yes_is_zero", 0, true}, {"no_is_one", 1, false}, {"null", nil, false}, {"unexpected", 2, false}, } { t.Run(tc.name, func(t *testing.T) { execSQL("DELETE FROM t_sim_channels") execSQL("INSERT INTO t_sim_channels (id, chan_key, temp_key, is_normal) VALUES (1, 'gate_channel', 'gate_test', ?)", tc.value) config := channelconfig.NewChannelConfig() service.RegisterChannelConfig(config) api := NewUpstreamAPI() factory := &normalGateTestTemplate{} api.RegisterTemp("gate_test", factory) reload := func() { t.Helper() var err error if path == "loop" { err = config.LoadAllChannelConfig(ctx) if err == nil { err = api.loop(ctx) } } else { err = api.RefreshChannel(ctx, "gate_channel") } if err != nil { t.Fatalf("refresh must succeed: %v", err) } } reload() cfg, ok := config.GetChannel("gate_channel") if !ok || cfg.CanCreateInstance() != tc.eligible { t.Fatalf("wrong eligibility after ORM scan: %+v", cfg) } if tc.value == nil && cfg.IsNormal != nil { t.Fatalf("SQL NULL decoded as non-nil: %v", *cfg.IsNormal) } if cfg.Channel.ChanKey != "gate_channel" { t.Fatal("embedded channel fields were not scanned") } _, exists := api.GetChan("gate_channel") if exists != tc.eligible || (factory.calls == 1) != tc.eligible { t.Fatalf("instance exists=%v factory calls=%d eligible=%v", exists, factory.calls, tc.eligible) } initialVersion := cfg.Version // Only is_normal changes: the configuration cache must notice it. execSQL("UPDATE t_sim_channels SET is_normal=0") reload() current, ok := api.GetChan("gate_channel") if !ok || current.Instance == nil { t.Fatal("eligible channel was not instantiated") } cfg, _ = config.GetChannel("gate_channel") if !tc.eligible && cfg.Version == initialVersion { t.Fatal("changing only is_normal did not change config version") } oldCalls := factory.calls // Becoming ineligible must remove the old runtime instance. execSQL("UPDATE t_sim_channels SET is_normal=1, chan_name='changed while disabled', temp_key='missing_template'") reload() if _, exists := api.GetChan("gate_channel"); exists || factory.calls != oldCalls { t.Fatal("ineligible channel retained or rebuilt its runtime instance") } cfg, _ = config.GetChannel("gate_channel") if cfg.CanCreateInstance() { t.Fatal("ineligible config must remain ineligible") } // Repeat the disabled refresh, as a retry would. reload() if factory.calls != oldCalls { t.Fatal("retry created an ineligible instance") } // A process restart cannot recreate the retained disabled instance. restarted := NewUpstreamAPI() restarted.RegisterTemp("gate_test", factory) if err := restarted.loop(ctx); err != nil { t.Fatal(err) } if _, ok := restarted.GetChan("gate_channel"); ok { t.Fatal("restart recreated disabled instance") } execSQL("UPDATE t_sim_channels SET is_normal=0, temp_key='gate_test'") reload() restored, _ := api.GetChan("gate_channel") if restored == current || factory.calls != oldCalls+1 || factory.lastConfig.Channel.ChanName != "changed while disabled" { t.Fatal("restored channel did not rebuild using the latest configuration") } }) } }) } }