From fad2631b23ea58f26b57d7b31201d55a53f142e5 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 18 Aug 2026 10:20:11 +0800 Subject: [PATCH] test(H-271): remove race-suite lifecycle blockers (#45) Co-authored-by: Rogee --- .../handler/api/v1/coverage16_test.go | 32 ++++++-- .../handler/api/v1/coverage18_test.go | 8 +- .../handler/api/v1/coverage19_test.go | 7 +- .../handler/api/v1/coverage20_test.go | 7 +- backend/internal/handler/ws/hub_extra_test.go | 42 +++-------- backend/internal/service/coverage_test.go | 74 ++++++++++++------- 6 files changed, 86 insertions(+), 84 deletions(-) diff --git a/backend/internal/handler/api/v1/coverage16_test.go b/backend/internal/handler/api/v1/coverage16_test.go index 9527da03..eae8d5d0 100644 --- a/backend/internal/handler/api/v1/coverage16_test.go +++ b/backend/internal/handler/api/v1/coverage16_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" "github.com/gin-gonic/gin" @@ -26,12 +27,32 @@ func init() { // ============ DB-Backed Test Helpers (Cov16) ============ +// ponytail: coverage tests are sequential; use per-test DBs if they adopt t.Parallel. +var ( + coverageTestDBMu sync.Mutex + coverageTestDBs = make(map[string]*gorm.DB) +) + +func sharedCoverageTestDB(t *testing.T, name string, models ...interface{}) *gorm.DB { + t.Helper() + coverageTestDBMu.Lock() + defer coverageTestDBMu.Unlock() + + db := coverageTestDBs[name] + if db == nil { + var err error + db, err = gorm.Open(sqlite.Open("file:"+name+"?mode=memory&cache=shared"), &gorm.Config{}) + require.NoError(t, err, "failed to open test database") + require.NoError(t, db.AutoMigrate(models...), "failed to auto-migrate models") + coverageTestDBs[name] = db + } + t.Cleanup(func() { testutil.TruncateAll(t, db) }) + return db +} + // newTestDB_Cov16 creates an in-memory SQLite DB with a broad set of models auto-migrated. func newTestDB_Cov16(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - require.NoError(t, err, "failed to open test database") - models := []interface{}{ &model.Account{}, &model.User{}, @@ -100,10 +121,7 @@ func newTestDB_Cov16(t *testing.T) *gorm.DB { &model.Audit{}, } - err = db.AutoMigrate(models...) - require.NoError(t, err, "failed to auto-migrate models") - - return db + return sharedCoverageTestDB(t, "coverage16", models...) } // seedAccount_Cov16 creates and persists a test account. diff --git a/backend/internal/handler/api/v1/coverage18_test.go b/backend/internal/handler/api/v1/coverage18_test.go index 318eb94d..713b6480 100644 --- a/backend/internal/handler/api/v1/coverage18_test.go +++ b/backend/internal/handler/api/v1/coverage18_test.go @@ -8,7 +8,6 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" @@ -18,8 +17,6 @@ import ( func newTestDB_Cov18(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - require.NoError(t, err) models := []interface{}{ &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, @@ -36,10 +33,7 @@ func newTestDB_Cov18(t *testing.T) *gorm.DB { &model.WorkingHour{}, &model.CaptainAssistant{}, &model.CaptainDocument{}, &model.CaptainPreference{}, } - for _, m := range models { - require.NoError(t, db.AutoMigrate(m)) - } - return db + return sharedCoverageTestDB(t, "coverage18", models...) } func seedAccount_Cov18(t *testing.T, db *gorm.DB) uint { diff --git a/backend/internal/handler/api/v1/coverage19_test.go b/backend/internal/handler/api/v1/coverage19_test.go index 8e5a9b6e..b7b7f87a 100644 --- a/backend/internal/handler/api/v1/coverage19_test.go +++ b/backend/internal/handler/api/v1/coverage19_test.go @@ -10,7 +10,6 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" @@ -22,9 +21,6 @@ import ( func newTestDB_Cov19(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - require.NoError(t, err, "failed to open test database") - models := []interface{}{ &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, @@ -42,8 +38,7 @@ func newTestDB_Cov19(t *testing.T) *gorm.DB { &model.ContactNote{}, &model.CaptainAssistant{}, &model.CaptainDocument{}, &model.CaptainPreference{}, } - require.NoError(t, db.AutoMigrate(models...), "failed to auto-migrate models") - return db + return sharedCoverageTestDB(t, "coverage19", models...) } func seedAccount_Cov19(t *testing.T, db *gorm.DB) *model.Account { diff --git a/backend/internal/handler/api/v1/coverage20_test.go b/backend/internal/handler/api/v1/coverage20_test.go index bb452d1d..31ad9f50 100644 --- a/backend/internal/handler/api/v1/coverage20_test.go +++ b/backend/internal/handler/api/v1/coverage20_test.go @@ -12,7 +12,6 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" @@ -24,9 +23,6 @@ import ( func newTestDB_Cov20(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - require.NoError(t, err, "failed to open test database") - models := []interface{}{ &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, @@ -44,8 +40,7 @@ func newTestDB_Cov20(t *testing.T) *gorm.DB { &model.WorkingHour{}, &model.ContactNote{}, &model.CaptainAssistant{}, &model.CaptainDocument{}, &model.CaptainPreference{}, &model.WidgetTest{}, } - require.NoError(t, db.AutoMigrate(models...), "failed to auto-migrate models") - return db + return sharedCoverageTestDB(t, "coverage20", models...) } func seedAccount_Cov20(t *testing.T, db *gorm.DB) *model.Account { diff --git a/backend/internal/handler/ws/hub_extra_test.go b/backend/internal/handler/ws/hub_extra_test.go index 722c484a..eefb1993 100644 --- a/backend/internal/handler/ws/hub_extra_test.go +++ b/backend/internal/handler/ws/hub_extra_test.go @@ -1162,18 +1162,16 @@ func TestWatermillZapAdapter_With(t *testing.T) { // Returns the server side and the client side. func newTestWSConn(t *testing.T) (*websocket.Conn, *websocket.Conn) { t.Helper() - // Create an in-process WebSocket server upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } + serverConn := make(chan *websocket.Conn, 1) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } - // Block forever — the client side will close - <-r.Context().Done() - conn.Close() + serverConn <- conn })) t.Cleanup(srv.Close) @@ -1182,36 +1180,14 @@ func newTestWSConn(t *testing.T) (*websocket.Conn, *websocket.Conn) { clientConn, _, err := dialer.Dial(wsURL, nil) require.NoError(t, err) - // Accept the server-side connection by making a second request - // Actually, httptest.NewServer with the upgrader already accepted it. - // We need to get the server conn. Let's use a different approach: - // Use net.Pipe-based websocket pair. - clientConn.Close() - srv.Close() - - // Use websocket.NewPreparedMessages approach — or just use a pipe-based approach - // Actually, simplest: use a real WS server that returns the server conn - var serverConn *websocket.Conn - srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - serverConn = conn - })) - t.Cleanup(srv2.Close) - - wsURL2 := "ws" + strings.TrimPrefix(srv2.URL, "http") - clientConn2, _, err := dialer.Dial(wsURL2, nil) - require.NoError(t, err) - - // Wait briefly for serverConn to be set - for i := 0; i < 100 && serverConn == nil; i++ { - time.Sleep(time.Millisecond) + select { + case conn := <-serverConn: + return conn, clientConn + case <-time.After(2 * time.Second): + clientConn.Close() + t.Fatal("server connection was not established") + return nil, nil } - require.NotNil(t, serverConn, "server connection was not established") - - return serverConn, clientConn2 } func TestHandlerHandleUnsubscribe_InvalidIdentifier(t *testing.T) { diff --git a/backend/internal/service/coverage_test.go b/backend/internal/service/coverage_test.go index 9cd59545..2063c8e1 100644 --- a/backend/internal/service/coverage_test.go +++ b/backend/internal/service/coverage_test.go @@ -2,11 +2,15 @@ package service import ( "context" + "fmt" + "reflect" + "sync" "testing" "time" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" + "github.com/gochat/gochat/pkg/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" @@ -14,33 +18,53 @@ import ( "gorm.io/gorm/logger" ) +// ponytail: coverage tests are sequential; use per-test DBs if they adopt t.Parallel. +var ( + simpleServiceTestDBMu sync.Mutex + simpleServiceDBs = make(map[string]*gorm.DB) +) + func newSimpleServiceTestDB(t *testing.T, models ...interface{}) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - require.NoError(t, err) - allModels := append([]interface{}{ - &model.Account{}, &model.User{}, &model.AccountUser{}, - &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, - &model.Conversation{}, &model.Message{}, &model.Attachment{}, - &model.Notification{}, &model.NotificationPreference{}, - &model.Tag{}, &model.ConversationLabel{}, &model.ContactLabel{}, - &model.Team{}, &model.TeamMember{}, - &model.Banner{}, &model.Note{}, &model.ContactNote{}, - &model.InstallationConfig{}, - &model.InboxLimit{}, &model.InboxMember{}, - &model.AgentBot{}, &model.AgentBotInbox{}, - &model.DeliveryStatus{}, - &model.ReportingEvent{}, - &model.SlaEvent{}, &model.SlaPolicy{}, &model.AppliedSLA{}, - &model.WidgetTest{}, - &model.PushToken{}, - &model.NotificationSubscription{}, - &model.EmailChannelMigration{}, - &model.Article{}, - &model.Folder{}, - &model.CustomAttributeDefinition{}, - }, models...) - require.NoError(t, db.AutoMigrate(allModels...)) + simpleServiceTestDBMu.Lock() + defer simpleServiceTestDBMu.Unlock() + + key := "" + for _, model := range models { + key += "|" + reflect.TypeOf(model).String() + } + db := simpleServiceDBs[key] + if db == nil { + baseModels := []interface{}{ + &model.Account{}, &model.User{}, &model.AccountUser{}, + &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, + &model.Conversation{}, &model.Message{}, &model.Attachment{}, + &model.Notification{}, &model.NotificationPreference{}, + &model.Tag{}, &model.ConversationLabel{}, &model.ContactLabel{}, + &model.Team{}, &model.TeamMember{}, + &model.Banner{}, &model.Note{}, &model.ContactNote{}, + &model.InstallationConfig{}, + &model.InboxLimit{}, &model.InboxMember{}, + &model.AgentBot{}, &model.AgentBotInbox{}, + &model.DeliveryStatus{}, + &model.ReportingEvent{}, + &model.SlaEvent{}, &model.SlaPolicy{}, &model.AppliedSLA{}, + &model.WidgetTest{}, + &model.PushToken{}, + &model.NotificationSubscription{}, + &model.EmailChannelMigration{}, + &model.Article{}, + &model.Folder{}, + &model.CustomAttributeDefinition{}, + } + allModels := append(baseModels, models...) + var err error + db, err = gorm.Open(sqlite.Open(fmt.Sprintf("file:service-coverage-%d?mode=memory&cache=shared", len(simpleServiceDBs))), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + require.NoError(t, err) + require.NoError(t, db.AutoMigrate(allModels...)) + simpleServiceDBs[key] = db + } + t.Cleanup(func() { testutil.TruncateAll(t, db) }) return db }