diff --git a/internal/data/migrations_test.go b/internal/data/migrations_test.go new file mode 100644 index 0000000..807173b --- /dev/null +++ b/internal/data/migrations_test.go @@ -0,0 +1,100 @@ +package data + +import ( + "testing" +) + +func TestMigrateLegacyAuthorityAPIsWithoutDeletedAt(t *testing.T) { + db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared") + if err != nil { + t.Fatal(err) + } + sqlDB, err := db.DB() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = sqlDB.Close() }) + + for _, statement := range []string{ + `CREATE TABLE sys_apis (id integer primary key, path text, method text)`, + `CREATE TABLE sys_authority_apis (authority_id integer, api_id integer)`, + `INSERT INTO sys_apis (id, path, method) VALUES (1, '/legacy', 'GET')`, + `INSERT INTO sys_authority_apis (authority_id, api_id) VALUES (888, 1)`, + } { + if err = db.Exec(statement).Error; err != nil { + t.Fatal(err) + } + } + if err = db.AutoMigrate(&casbinRulePO{}, &errorRecordPO{}); err != nil { + t.Fatal(err) + } + legacyError := errorRecordPO{Status: "未解决"} + if err = db.Create(&legacyError).Error; err != nil { + t.Fatal(err) + } + if err = migrateLegacyAuthorityAPIsToCasbinRules(db); err != nil { + t.Fatalf("legacy migration failed without sys_apis.deleted_at: %v", err) + } + if err = normalizeErrorRecordStatuses(db); err != nil { + t.Fatalf("following migration inherited the legacy table alias: %v", err) + } + + var count int64 + if err = db.Model(&casbinRulePO{}). + Where("ptype = ? AND v0 = ? AND v1 = ? AND v2 = ?", "p", "888", "/legacy", "GET"). + Count(&count).Error; err != nil { + t.Fatal(err) + } + if count != 1 { + t.Fatalf("migrated policy count = %d, want 1", count) + } + if err = db.First(&legacyError, legacyError.ID).Error; err != nil { + t.Fatal(err) + } + if legacyError.Status != "未处理" { + t.Fatalf("normalized status = %q, want 未处理", legacyError.Status) + } +} + +func TestReconcileRootAuthorityAPIsUsesIndependentQueries(t *testing.T) { + db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared") + if err != nil { + t.Fatal(err) + } + sqlDB, err := db.DB() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = sqlDB.Close() }) + + if err = db.AutoMigrate(&authorityPO{}, &apiPO{}, &ignoredAPIPO{}, &casbinRulePO{}); err != nil { + t.Fatal(err) + } + if err = db.Create(&authorityPO{AuthorityID: 888, AuthorityName: "root"}).Error; err != nil { + t.Fatal(err) + } + if err = db.Create(&apiPO{Path: "/allowed", Method: "GET"}).Error; err != nil { + t.Fatal(err) + } + if err = db.Create(&apiPO{Path: "/ignored", Method: "POST"}).Error; err != nil { + t.Fatal(err) + } + if err = db.Create(&ignoredAPIPO{Path: "/ignored", Method: "POST"}).Error; err != nil { + t.Fatal(err) + } + + // Deliberately pass a handle carrying an unrelated model and predicate. + // Migration queries must not inherit either state. + dirty := db.Model(&authorityPO{}).Where("authority_id = ?", 999) + if err = reconcileRootAuthorityAPIs(dirty); err != nil { + t.Fatalf("reconcile root policies with dirty DB state: %v", err) + } + + var rules []casbinRulePO + if err = db.Where("ptype = ? AND v0 = ?", "p", "888").Find(&rules).Error; err != nil { + t.Fatal(err) + } + if len(rules) != 1 || rules[0].V1 != "/allowed" || rules[0].V2 != "GET" { + t.Fatalf("root rules = %#v, want only GET /allowed", rules) + } +} diff --git a/internal/worker/task_scheduler_test.go b/internal/worker/task_scheduler_test.go new file mode 100644 index 0000000..c2f1da6 --- /dev/null +++ b/internal/worker/task_scheduler_test.go @@ -0,0 +1,25 @@ +package worker + +import "testing" + +func TestCloseSubscribersClosesEveryAlertStream(t *testing.T) { + scheduler := &TaskScheduler{subscribers: map[uint]map[chan []byte]struct{}{}} + first := scheduler.Subscribe(1) + second := scheduler.Subscribe(1) + third := scheduler.Subscribe(2) + + scheduler.closeSubscribers() + + for _, ch := range []chan []byte{first, second, third} { + if _, open := <-ch; open { + t.Fatal("subscriber channel remains open after scheduler shutdown") + } + } + if len(scheduler.subscribers) != 0 { + t.Fatalf("subscriber registry was not cleared: %#v", scheduler.subscribers) + } + + // Handlers defer Unsubscribe. It must remain safe after shutdown has already + // closed and removed every channel. + scheduler.Unsubscribe(1, first) +}