package testutil import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/model/channel" ) // === Database Helpers === // NewTestDB creates an in-memory SQLite database with all models auto-migrated. func NewTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) assert.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{}, &model.Attachment{}, &model.Notification{}, &model.NotificationPreference{}, &model.CustomRole{}, &model.PlatformApp{}, &channel.ChannelTelegram{}, &channel.ChannelWebWidget{}, } err = db.AutoMigrate(models...) assert.NoError(t, err, "failed to auto-migrate models") return db } // NewTestDBWithModels creates an in-memory SQLite database with only specified models. func NewTestDBWithModels(t *testing.T, models ...interface{}) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) assert.NoError(t, err, "failed to open test database") err = db.AutoMigrate(models...) assert.NoError(t, err, "failed to auto-migrate models") return db } // CleanupDB closes the test database connection. func CleanupDB(t *testing.T, db *gorm.DB) { t.Helper() sqlDB, err := db.DB() assert.NoError(t, err, "failed to get underlying sql.DB") err = sqlDB.Close() assert.NoError(t, err, "failed to close test database") } // TruncateAll clears all data from the test database. func TruncateAll(t *testing.T, db *gorm.DB) { t.Helper() tables, err := db.Migrator().GetTables() assert.NoError(t, err, "failed to get tables") for _, table := range tables { err = db.Exec("DELETE FROM " + table).Error assert.NoError(t, err, "failed to truncate table %s", table) } } // === HTTP Test Helpers === // NewTestRouter creates a Gin router in test mode. func NewTestRouter() *gin.Engine { gin.SetMode(gin.TestMode) return gin.New() } // NewTestServer creates an httptest.Server wrapping a Gin router. func NewTestServer(router *gin.Engine) *httptest.Server { return httptest.NewServer(router) } // PerformRequest performs an HTTP request against a Gin router and returns the response. func PerformRequest(router *gin.Engine, method, path string, body interface{}) *httptest.ResponseRecorder { var reqBody *http.Request if body != nil { // This is handled by the caller using json.Marshal reqBody = httptest.NewRequest(method, path, nil) } else { reqBody = httptest.NewRequest(method, path, nil) } w := httptest.NewRecorder() router.ServeHTTP(w, reqBody) return w } // AssertJSONResponse asserts that a response contains expected JSON keys. func AssertJSONResponse(t *testing.T, w *httptest.ResponseRecorder, statusCode int, keys ...string) { t.Helper() assert.Equal(t, statusCode, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err, "response body should be valid JSON") for _, key := range keys { assert.Contains(t, resp, key, "response should contain key: %s", key) } } // === Context Helpers === // NewTestContext creates a Gin context with a test HTTP request. func NewTestContext() (*gin.Context, *httptest.ResponseRecorder) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/", nil) return c, w } // SetTestUserContext sets a user ID and account ID in the Gin context. func SetTestUserContext(c *gin.Context, userID, accountID uint, role string) { c.Set("user_id", userID) c.Set("account_id", accountID) c.Set("role", role) } // === Assertion Helpers === // AssertCreated asserts that a model was created with a non-zero ID. func AssertCreated(t *testing.T, obj interface{}, id uint) { t.Helper() assert.NotZero(t, id, "created object should have non-zero ID") } // AssertSoftDeleted asserts that a model's DeletedAt field is set. func AssertSoftDeleted(t *testing.T, deletedAt gorm.DeletedAt) { t.Helper() assert.NotNil(t, deletedAt, "soft-deleted object should have DeletedAt set") assert.NotZero(t, deletedAt.Time, "DeletedAt time should not be zero") } // AssertTimestampOrder asserts that created_at <= updated_at. func AssertTimestampOrder(t *testing.T, createdAt, updatedAt time.Time) { t.Helper() assert.True(t, createdAt.Before(updatedAt) || createdAt.Equal(updatedAt), "created_at should be <= updated_at") } // === Fixture Seeding === // SeedAccount creates and persists a test account. func SeedAccount(t *testing.T, db *gorm.DB, name string) *model.Account { t.Helper() acc := &model.Account{Name: name, Locale: "en", Timezone: "UTC", Active: true} err := db.Create(acc).Error assert.NoError(t, err, "failed to seed account") return acc } // SeedUser creates and persists a test user. func SeedUser(t *testing.T, db *gorm.DB, accountID uint, name, email string) *model.User { t.Helper() user := &model.User{ AccountID: accountID, Name: name, Email: email, Password: "hashed_password_placeholder", Role: string(model.AccountUserRoleAgent), Active: true, } err := db.Create(user).Error assert.NoError(t, err, "failed to seed user") return user } // SeedAccountUser creates and persists an AccountUser membership. func SeedAccountUser(t *testing.T, db *gorm.DB, userID, accountID uint, role string) *model.AccountUser { t.Helper() au := &model.AccountUser{ UserID: userID, AccountID: accountID, Role: role, } err := db.Create(au).Error assert.NoError(t, err, "failed to seed account_user") return au } // SeedInbox creates and persists a test inbox. func SeedInbox(t *testing.T, db *gorm.DB, accountID uint, name, channelType string) *model.Inbox { t.Helper() inbox := &model.Inbox{ AccountID: accountID, Name: name, ChannelType: channelType, ChannelID: 1, } err := db.Create(inbox).Error assert.NoError(t, err, "failed to seed inbox") return inbox } // SeedContact creates and persists a test contact. func SeedContact(t *testing.T, db *gorm.DB, accountID uint, name string) *model.Contact { t.Helper() contact := &model.Contact{ AccountID: accountID, Name: name, } err := db.Create(contact).Error assert.NoError(t, err, "failed to seed contact") return contact } // SeedConversation creates and persists a test conversation. func SeedConversation(t *testing.T, db *gorm.DB, accountID, inboxID, contactID uint, status string) *model.Conversation { t.Helper() conv := &model.Conversation{ AccountID: accountID, InboxID: inboxID, ContactID: contactID, Status: status, ChannelType: "web_widget", } err := db.Create(conv).Error assert.NoError(t, err, "failed to seed conversation") return conv } // SeedMessage creates and persists a test message. func SeedMessage(t *testing.T, db *gorm.DB, conversationID, accountID, inboxID uint, content string) *model.Message { t.Helper() msg := &model.Message{ ConversationID: conversationID, AccountID: accountID, InboxID: inboxID, Content: content, ContentType: string(model.MessageContentTypeText), MessageType: string(model.MessageTypeIncoming), SenderType: "contact", } err := db.Create(msg).Error assert.NoError(t, err, "failed to seed message") return msg } // === Custom Error type for test assertions === // TestError wraps an error with context for test failure reporting. type TestError struct { Op string Err error Msg string } func (e *TestError) Error() string { return fmt.Sprintf("%s: %s: %v", e.Op, e.Msg, e.Err) } func (e *TestError) Unwrap() error { return e.Err } // NewTestError creates a TestError for a failed operation. func NewTestError(op string, err error, msg string) *TestError { return &TestError{Op: op, Err: err, Msg: msg} } // === JSON encoding helper === // We can't use json.Unmarshal directly in the AssertJSONResponse function // because it was referenced but not imported. Let me fix that. // DecodeJSONResponse decodes an HTTP response body into a map. func DecodeJSONResponse(t *testing.T, w *httptest.ResponseRecorder) map[string]interface{} { t.Helper() var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err, "response body should be valid JSON") return resp }