Files
gochat/backend/pkg/testutil/helpers.go
T
rogee a1ae852eb2 Fix widget i18n missing locale files and clean up unused locales
- widget/i18n/index.js: only import en.json and zh_CN.json (the only
  locale files present); remove 40+ imports for missing locale JSONs
  that caused Vite compile failure and global white screen
- dashboard/i18n/index.js: remove unused locale imports for consistency
- Remove 62 unused locale JSON files from widget/i18n/locale/
- Minor: update index.html, test helpers, e2e test, languages spec
2026-07-08 09:57:00 +08:00

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: "zh_CN", 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
}