package repository import ( "os" "sync" "testing" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/canned" "github.com/gochat/gochat/internal/csat" "github.com/gochat/gochat/internal/model" ) // actualDB tracks whether we actually connected to PG or fell back to SQLite. // This is set once during the first setupTestDB call and used by skipIfSQLite. var ( actualDBIsPG bool actualDBChecked bool actualDBCheckMu sync.Mutex ) // wantPostgres returns true when GOCHAT_TEST_DB != "sqlite". func wantPostgres() bool { return os.Getenv("GOCHAT_TEST_DB") != "sqlite" } // isActuallyPG returns true if the test database is actually PostgreSQL // (not a SQLite fallback). Probes PG once and caches the result. func isActuallyPG() bool { actualDBCheckMu.Lock() defer actualDBCheckMu.Unlock() if actualDBChecked { return actualDBIsPG } if !wantPostgres() { actualDBIsPG = false actualDBChecked = true return false } // Probe PG connectivity dsn := pgDSN() db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) if err != nil { actualDBIsPG = false actualDBChecked = true return false } sqlDB, _ := db.DB() sqlDB.Close() actualDBIsPG = true actualDBChecked = true return true } // pgDSN returns the PostgreSQL connection string for testing. // Priority: GOCHAT_TEST_DB_URL env > default localhost DSN. func pgDSN() string { dsn := os.Getenv("GOCHAT_TEST_DB_URL") if dsn != "" { return dsn } return "host=localhost port=5432 user=postgres password=postgres dbname=gochat_test sslmode=disable" } // setupTestDB creates an isolated test database. // When PG is available, connects to PostgreSQL; otherwise uses SQLite in-memory. // The returned DB has all GoChat models auto-migrated. func setupTestDB(t *testing.T, models ...interface{}) *gorm.DB { t.Helper() allModels := append(defaultTestModels(), models...) if isActuallyPG() { db, err := gorm.Open(postgres.Open(pgDSN()), &gorm.Config{}) if err != nil { t.Fatalf("failed to connect to PostgreSQL test db: %v\nSet GOCHAT_TEST_DB=sqlite to use SQLite instead", err) } if err := db.AutoMigrate(allModels...); err != nil { t.Fatalf("failed to auto-migrate PG: %v", err) } // Cleanup: truncate all tables after test t.Cleanup(func() { for _, m := range allModels { db.Unscoped().Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(m) } sqlDB, _ := db.DB() sqlDB.Close() }) return db } return openSQLiteDB(t, allModels) } // setupBenchmarkDB creates an isolated database for benchmarks. func setupBenchmarkDB(b *testing.B, models ...interface{}) *gorm.DB { b.Helper() allModels := append(defaultTestModels(), models...) if isActuallyPG() { db, err := gorm.Open(postgres.Open(pgDSN()), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { b.Fatalf("failed to connect to PostgreSQL benchmark db: %v", err) } if err := db.AutoMigrate(allModels...); err != nil { b.Fatalf("failed to auto-migrate PG: %v", err) } b.Cleanup(func() { for _, m := range allModels { db.Unscoped().Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(m) } sqlDB, _ := db.DB() sqlDB.Close() }) return db } return openSQLiteDBBench(b, allModels) } // defaultTestModels returns the standard set of models needed for most tests. func defaultTestModels() []interface{} { return []interface{}{ &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, &model.Notification{}, &model.NotificationPreference{}, &model.Team{}, &model.TeamMember{}, &canned.CannedResponse{}, &csat.CsatSurveyResponse{}, &model.Tag{}, &model.ConversationLabel{}, &model.ContactLabel{}, &model.ContactExport{}, &model.DataImport{}, &model.PlatformApp{}, &model.Permissible{}, &model.AccessToken{}, &model.ConversationParticipant{}, &model.DraftMessage{}, &model.Audit{}, &model.CustomRole{}, &model.AgentCapacityPolicy{}, &model.InboxCapacityLimit{}, &model.Company{}, &model.CompanyNote{}, &model.Mention{}, &model.IntegrationHook{}, &model.IntegrationApp{}, } } func openSQLiteDB(t *testing.T, models []interface{}) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) if err != nil { t.Fatalf("failed to open SQLite test db: %v", err) } if err := db.AutoMigrate(models...); err != nil { t.Fatalf("failed to auto-migrate SQLite: %v", err) } return db } func openSQLiteDBBench(b *testing.B, models []interface{}) *gorm.DB { b.Helper() db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { b.Fatalf("failed to open SQLite benchmark db: %v", err) } if err := db.AutoMigrate(models...); err != nil { b.Fatalf("failed to auto-migrate SQLite: %v", err) } return db } // skipIfSQLite skips the test when the actual database is SQLite // (use for PG-only features like ILIKE, NOW(), NULLS LAST, etc.). // When PG is requested but unavailable, tests are also skipped since // the fallback SQLite won't support PG-specific syntax. func skipIfSQLite(t *testing.T) { t.Helper() if !isActuallyPG() { t.Skip("Skipping: this test requires PostgreSQL (ILIKE / NOW() / NULLS LAST etc.)") } }