296 lines
8.4 KiB
Go
296 lines
8.4 KiB
Go
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
|
|
} |