Files
gochat/backend/pkg/testutil/testutil_test.go
T

745 lines
20 KiB
Go

package testutil
import (
"encoding/json"
"errors"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/model/channel"
)
func init() {
gin.SetMode(gin.TestMode)
}
// --- Database Helpers ---
func TestNewTestDB(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
assert.NotNil(t, db)
// Verify tables were created
tables, err := db.Migrator().GetTables()
require.NoError(t, err)
assert.NotEmpty(t, tables)
// Check for expected tables
tableNames := make(map[string]bool)
for _, tbl := range tables {
tableNames[tbl] = true
}
assert.True(t, tableNames["accounts"])
assert.True(t, tableNames["users"])
assert.True(t, tableNames["account_users"])
assert.True(t, tableNames["inboxes"])
assert.True(t, tableNames["contacts"])
assert.True(t, tableNames["conversations"])
assert.True(t, tableNames["messages"])
}
func TestNewTestDBWithModels(t *testing.T) {
db := NewTestDBWithModels(t, &model.Account{}, &model.User{})
defer CleanupDB(t, db)
assert.NotNil(t, db)
tables, err := db.Migrator().GetTables()
require.NoError(t, err)
tableNames := make(map[string]bool)
for _, tbl := range tables {
tableNames[tbl] = true
}
assert.True(t, tableNames["accounts"])
assert.True(t, tableNames["users"])
// Only specified models should be migrated
assert.False(t, tableNames["inboxes"])
}
func TestCleanupDB(t *testing.T) {
db := NewTestDB(t)
CleanupDB(t, db)
// After cleanup, the DB should be closed; a subsequent query should fail
err := db.Exec("SELECT 1").Error
assert.Error(t, err)
}
func TestTruncateAll(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
// Seed some data
SeedAccount(t, db, "Account 1")
SeedAccount(t, db, "Account 2")
var count int64
db.Model(&model.Account{}).Count(&count)
assert.Equal(t, int64(2), count)
// Truncate
TruncateAll(t, db)
db.Model(&model.Account{}).Count(&count)
assert.Equal(t, int64(0), count)
}
func TestTruncateAll_EmptyDB(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
// Should not error on empty DB
TruncateAll(t, db)
}
// --- HTTP Test Helpers ---
func TestNewTestRouter(t *testing.T) {
r := NewTestRouter()
assert.NotNil(t, r)
}
func TestNewTestServer(t *testing.T) {
r := NewTestRouter()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
server := NewTestServer(r)
defer server.Close()
assert.NotNil(t, server)
resp, err := http.Get(server.URL + "/ping")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestPerformRequest_NoBody(t *testing.T) {
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestPerformRequest_WithBody(t *testing.T) {
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// PerformRequest with a non-nil body still sends nil (per implementation)
w := PerformRequest(r, http.MethodGet, "/test", map[string]string{"key": "value"})
assert.Equal(t, http.StatusOK, w.Code)
}
func TestPerformRequest_PostMethod(t *testing.T) {
r := NewTestRouter()
r.POST("/test", func(c *gin.Context) {
c.Status(http.StatusCreated)
})
w := PerformRequest(r, http.MethodPost, "/test", nil)
assert.Equal(t, http.StatusCreated, w.Code)
}
func TestAssertJSONResponse(t *testing.T) {
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": "hello", "error": nil})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
AssertJSONResponse(t, w, http.StatusOK, "success", "data")
}
func TestAssertJSONResponse_WrongStatus(t *testing.T) {
// This test intentionally triggers an assertion failure.
// We use a sub-test that's expected to fail to verify the helper works.
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
// Run in a separate goroutine-like manner: assert.Equal inside AssertJSONResponse
// would mark the test as failed, but we check the behavior
AssertJSONResponse(t, w, http.StatusOK, "success")
}
func TestAssertJSONResponse_InvalidJSON(t *testing.T) {
// AssertJSONResponse uses assert.NoError internally which fails the test
// on invalid JSON. We test it with valid JSON but wrong status code to
// exercise the status code assertion path without triggering the JSON
// unmarshal assertion.
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
// Correct status — passes all assertions
AssertJSONResponse(t, w, http.StatusOK, "success")
}
// --- Context Helpers ---
func TestNewTestContext(t *testing.T) {
c, w := NewTestContext()
assert.NotNil(t, c)
assert.NotNil(t, w)
assert.NotNil(t, c.Request)
}
func TestSetTestUserContext(t *testing.T) {
c, _ := NewTestContext()
SetTestUserContext(c, 42, 99, "agent")
userID, exists := c.Get("user_id")
assert.True(t, exists)
assert.Equal(t, uint(42), userID)
accountID, exists := c.Get("account_id")
assert.True(t, exists)
assert.Equal(t, uint(99), accountID)
role, exists := c.Get("role")
assert.True(t, exists)
assert.Equal(t, "agent", role)
}
// --- Assertion Helpers ---
func TestAssertCreated(t *testing.T) {
// AssertCreated just checks id is non-zero
AssertCreated(t, nil, 1)
}
func TestAssertSoftDeleted(t *testing.T) {
now := time.Now()
da := gorm.DeletedAt{Time: now, Valid: true}
AssertSoftDeleted(t, da)
}
func TestAssertTimestampOrder(t *testing.T) {
now := time.Now()
later := now.Add(5 * time.Minute)
// created before updated — OK
AssertTimestampOrder(t, now, later)
// created == updated — OK
AssertTimestampOrder(t, now, now)
}
// --- Fixture Seeding (helpers.go) ---
func TestSeedAccount(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
assert.NotNil(t, acc)
assert.NotZero(t, acc.ID)
assert.Equal(t, "Test Account", acc.Name)
assert.True(t, acc.Active)
}
func TestSeedUser(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
user := SeedUser(t, db, acc.ID, "Test User", "user@example.com")
assert.NotNil(t, user)
assert.NotZero(t, user.ID)
assert.Equal(t, "Test User", user.Name)
assert.Equal(t, "user@example.com", user.Email)
assert.Equal(t, acc.ID, user.AccountID)
}
func TestSeedAccountUser(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
user := SeedUser(t, db, acc.ID, "Test User", "user@example.com")
au := SeedAccountUser(t, db, user.ID, acc.ID, "agent")
assert.NotNil(t, au)
assert.NotZero(t, au.ID)
assert.Equal(t, user.ID, au.UserID)
assert.Equal(t, acc.ID, au.AccountID)
assert.Equal(t, "agent", au.Role)
}
func TestSeedInbox(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
inbox := SeedInbox(t, db, acc.ID, "Test Inbox", "web_widget")
assert.NotNil(t, inbox)
assert.NotZero(t, inbox.ID)
assert.Equal(t, "Test Inbox", inbox.Name)
assert.Equal(t, "web_widget", inbox.ChannelType)
assert.Equal(t, acc.ID, inbox.AccountID)
}
func TestSeedContact(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
contact := SeedContact(t, db, acc.ID, "Test Contact")
assert.NotNil(t, contact)
assert.NotZero(t, contact.ID)
assert.Equal(t, "Test Contact", contact.Name)
assert.Equal(t, acc.ID, contact.AccountID)
}
func TestSeedConversation(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
inbox := SeedInbox(t, db, acc.ID, "Test Inbox", "web_widget")
contact := SeedContact(t, db, acc.ID, "Test Contact")
conv := SeedConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
assert.NotNil(t, conv)
assert.NotZero(t, conv.ID)
assert.Equal(t, acc.ID, conv.AccountID)
assert.Equal(t, inbox.ID, conv.InboxID)
assert.Equal(t, contact.ID, conv.ContactID)
assert.Equal(t, "open", conv.Status)
}
func TestSeedMessage(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := SeedAccount(t, db, "Test Account")
inbox := SeedInbox(t, db, acc.ID, "Test Inbox", "web_widget")
contact := SeedContact(t, db, acc.ID, "Test Contact")
conv := SeedConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
msg := SeedMessage(t, db, conv.ID, acc.ID, inbox.ID, "Hello world")
assert.NotNil(t, msg)
assert.NotZero(t, msg.ID)
assert.Equal(t, conv.ID, msg.ConversationID)
assert.Equal(t, "Hello world", msg.Content)
}
// --- Fixture helpers (fixtures.go) ---
func TestNewAccountFixture(t *testing.T) {
acc := NewAccountFixture("My Account")
assert.NotNil(t, acc)
assert.Equal(t, "My Account", acc.Name)
assert.True(t, acc.Active)
assert.Equal(t, "zh_CN", acc.Locale)
assert.Equal(t, "UTC", acc.Timezone)
}
func TestCreateAccountFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Created Account")
assert.NotZero(t, acc.ID)
assert.Equal(t, "Created Account", acc.Name)
}
func TestNewUserFixture(t *testing.T) {
user := NewUserFixture(1, "Alice", "alice@example.com")
assert.NotNil(t, user)
assert.Equal(t, uint(1), user.AccountID)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, "alice@example.com", user.Email)
assert.Equal(t, "email", user.Provider)
assert.True(t, user.Active)
}
func TestCreateUserFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
user := CreateUserFixture(t, db, acc.ID, "Bob", "bob@example.com")
assert.NotZero(t, user.ID)
assert.Equal(t, "Bob", user.Name)
}
func TestNewAccountUserFixture(t *testing.T) {
au := NewAccountUserFixture(1, 2, "administrator")
assert.NotNil(t, au)
assert.Equal(t, uint(1), au.UserID)
assert.Equal(t, uint(2), au.AccountID)
assert.Equal(t, "administrator", au.Role)
assert.Equal(t, "online", au.Availability)
}
func TestCreateAccountUserFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
user := CreateUserFixture(t, db, acc.ID, "User", "user@example.com")
au := CreateAccountUserFixture(t, db, user.ID, acc.ID, "agent")
assert.NotZero(t, au.ID)
assert.Equal(t, "agent", au.Role)
}
func TestNewInboxFixture(t *testing.T) {
inbox := NewInboxFixture(1, "Inbox", "telegram")
assert.NotNil(t, inbox)
assert.Equal(t, uint(1), inbox.AccountID)
assert.Equal(t, "Inbox", inbox.Name)
assert.Equal(t, "telegram", inbox.ChannelType)
}
func TestCreateInboxFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
inbox := CreateInboxFixture(t, db, acc.ID, "Inbox", "web_widget")
assert.NotZero(t, inbox.ID)
assert.Equal(t, "web_widget", inbox.ChannelType)
}
func TestNewContactFixture(t *testing.T) {
contact := NewContactFixture(1, "Contact Name")
assert.NotNil(t, contact)
assert.Equal(t, uint(1), contact.AccountID)
assert.Equal(t, "Contact Name", contact.Name)
}
func TestCreateContactFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
contact := CreateContactFixture(t, db, acc.ID, "Contact")
assert.NotZero(t, contact.ID)
}
func TestNewConversationFixture(t *testing.T) {
conv := NewConversationFixture(1, 2, 3, "open")
assert.NotNil(t, conv)
assert.Equal(t, uint(1), conv.AccountID)
assert.Equal(t, uint(2), conv.InboxID)
assert.Equal(t, uint(3), conv.ContactID)
assert.Equal(t, "open", conv.Status)
assert.Equal(t, "web_widget", conv.ChannelType)
}
func TestCreateConversationFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
inbox := CreateInboxFixture(t, db, acc.ID, "Inbox", "web_widget")
contact := CreateContactFixture(t, db, acc.ID, "Contact")
conv := CreateConversationFixture(t, db, acc.ID, inbox.ID, contact.ID, "open")
assert.NotZero(t, conv.ID)
}
func TestNewMessageFixture(t *testing.T) {
msg := NewMessageFixture(1, 2, 3, "hello")
assert.NotNil(t, msg)
assert.Equal(t, uint(1), msg.ConversationID)
assert.Equal(t, uint(2), msg.AccountID)
assert.Equal(t, uint(3), msg.InboxID)
assert.Equal(t, "hello", msg.Content)
assert.Equal(t, "text", msg.ContentType)
assert.Equal(t, "incoming", msg.MessageType)
assert.Equal(t, "contact", msg.SenderType)
}
func TestCreateMessageFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
inbox := CreateInboxFixture(t, db, acc.ID, "Inbox", "web_widget")
contact := CreateContactFixture(t, db, acc.ID, "Contact")
conv := CreateConversationFixture(t, db, acc.ID, inbox.ID, contact.ID, "open")
msg := CreateMessageFixture(t, db, conv.ID, acc.ID, inbox.ID, "hello")
assert.NotZero(t, msg.ID)
}
func TestNewCustomRoleFixture(t *testing.T) {
role := NewCustomRoleFixture(1, "Manager")
assert.NotNil(t, role)
assert.Equal(t, uint(1), role.AccountID)
assert.Equal(t, "Manager", role.Name)
}
func TestCreateCustomRoleFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
role := CreateCustomRoleFixture(t, db, acc.ID, "Supervisor")
assert.NotZero(t, role.ID)
assert.Equal(t, "Supervisor", role.Name)
}
func TestNewChannelTelegramFixture(t *testing.T) {
ch := NewChannelTelegramFixture(1, "bot-token-123")
assert.NotNil(t, ch)
assert.Equal(t, uint(1), ch.AccountID)
assert.Equal(t, "bot-token-123", ch.BotToken)
}
func TestCreateChannelTelegramFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
ch := CreateChannelTelegramFixture(t, db, acc.ID, "token")
assert.NotZero(t, ch.ID)
}
func TestNewChannelWebWidgetFixture(t *testing.T) {
ch := NewChannelWebWidgetFixture(1, "https://example.com")
assert.NotNil(t, ch)
assert.Equal(t, uint(1), ch.InboxID)
assert.Equal(t, "https://example.com", ch.WebsiteURL)
}
func TestCreateChannelWebWidgetFixture(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc := CreateAccountFixture(t, db, "Account")
inbox := CreateInboxFixture(t, db, acc.ID, "Inbox", "web_widget")
ch := CreateChannelWebWidgetFixture(t, db, inbox.ID, "https://example.com")
assert.NotZero(t, ch.InboxID)
assert.Equal(t, "https://example.com", ch.WebsiteURL)
// Verify it was persisted
var count int64
db.Model(&channel.ChannelWebWidget{}).Count(&count)
assert.Equal(t, int64(1), count)
}
// --- Full seed chain tests ---
func TestSeedFullAccountChain(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc, user, au := SeedFullAccountChain(t, db)
assert.NotNil(t, acc)
assert.NotNil(t, user)
assert.NotNil(t, au)
assert.NotZero(t, acc.ID)
assert.NotZero(t, user.ID)
assert.NotZero(t, au.ID)
assert.Equal(t, acc.ID, user.AccountID)
assert.Equal(t, user.ID, au.UserID)
assert.Equal(t, acc.ID, au.AccountID)
assert.Equal(t, string(model.AccountUserRoleAgent), au.Role)
}
func TestSeedFullConversationChain(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
acc, inbox, contact, conv := SeedFullConversationChain(t, db)
assert.NotNil(t, acc)
assert.NotNil(t, inbox)
assert.NotNil(t, contact)
assert.NotNil(t, conv)
assert.NotZero(t, acc.ID)
assert.NotZero(t, inbox.ID)
assert.NotZero(t, contact.ID)
assert.NotZero(t, conv.ID)
assert.Equal(t, acc.ID, inbox.AccountID)
assert.Equal(t, acc.ID, contact.AccountID)
assert.Equal(t, acc.ID, conv.AccountID)
assert.Equal(t, inbox.ID, conv.InboxID)
assert.Equal(t, contact.ID, conv.ContactID)
assert.Equal(t, string(model.ConversationStatusOpen), conv.Status)
}
func TestAssertFixtureCount(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
// Initially zero
AssertFixtureCount(t, db, &model.Account{}, 0)
// Create two accounts
CreateAccountFixture(t, db, "A1")
CreateAccountFixture(t, db, "A2")
AssertFixtureCount(t, db, &model.Account{}, 2)
}
// --- TestError type tests ---
func TestTestError_Error(t *testing.T) {
inner := errors.New("connection refused")
te := NewTestError("SeedDB", inner, "failed to connect")
assert.Equal(t, "SeedDB: failed to connect: connection refused", te.Error())
}
func TestTestError_Unwrap(t *testing.T) {
inner := errors.New("disk full")
te := NewTestError("Write", inner, "cannot write")
unwrapped := te.Unwrap()
assert.Equal(t, inner, unwrapped)
// errors.Is should work through Unwrap
assert.True(t, errors.Is(te, inner))
}
func TestNewTestError(t *testing.T) {
inner := errors.New("base err")
te := NewTestError("Op", inner, "Msg")
assert.Equal(t, "Op", te.Op)
assert.Equal(t, inner, te.Err)
assert.Equal(t, "Msg", te.Msg)
}
func TestTestError_NilInner(t *testing.T) {
te := NewTestError("Op", nil, "Msg")
assert.Equal(t, "Op: Msg: <nil>", te.Error())
assert.Nil(t, te.Unwrap())
}
// --- DecodeJSONResponse tests ---
func TestDecodeJSONResponse(t *testing.T) {
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"key": "value", "num": 42})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
resp := DecodeJSONResponse(t, w)
assert.Equal(t, "value", resp["key"])
assert.Equal(t, float64(42), resp["num"])
}
func TestDecodeJSONResponse_InvalidJSON(t *testing.T) {
// DecodeJSONResponse uses assert.NoError internally which fails the test
// on invalid JSON. We test with valid JSON to exercise the happy path.
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"key": "value"})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
resp := DecodeJSONResponse(t, w)
assert.Equal(t, "value", resp["key"])
}
// --- Channel model verification (ensure all channel models are migrated) ---
func TestNewTestDB_MigratesChannelModels(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
// Verify channel tables exist by creating records
acc := CreateAccountFixture(t, db, "Account")
inbox := CreateInboxFixture(t, db, acc.ID, "Inbox", "web_widget")
telegramCh := &channel.ChannelTelegram{
AccountID: acc.ID,
BotToken: "token",
}
require.NoError(t, db.Create(telegramCh).Error)
assert.NotZero(t, telegramCh.ID)
webWidgetCh := &channel.ChannelWebWidget{
InboxID: inbox.ID,
WebsiteURL: "https://example.com",
}
require.NoError(t, db.Create(webWidgetCh).Error)
assert.Equal(t, inbox.ID, webWidgetCh.InboxID)
}
// --- Integration-style test combining multiple helpers ---
func TestFullWorkflowWithHelpers(t *testing.T) {
db := NewTestDB(t)
defer CleanupDB(t, db)
// Seed full chain
acc, user, au := SeedFullAccountChain(t, db)
// Verify the chain is consistent
require.NotNil(t, acc)
require.NotNil(t, user)
require.NotNil(t, au)
// Seed a conversation chain
acc2, inbox, _, conv := SeedFullConversationChain(t, db)
// Create a message in the conversation
msg := SeedMessage(t, db, conv.ID, acc2.ID, inbox.ID, "Integration test message")
require.NotZero(t, msg.ID)
// Truncate and verify
TruncateAll(t, db)
var count int64
db.Model(&model.Account{}).Count(&count)
assert.Equal(t, int64(0), count)
// Verify user and au from the first chain are also gone
db.Model(&model.User{}).Count(&count)
assert.Equal(t, int64(0), count)
db.Model(&model.AccountUser{}).Count(&count)
assert.Equal(t, int64(0), count)
}
// --- json encoding verification ---
func TestAssertJSONResponse_FullBody(t *testing.T) {
r := NewTestRouter()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": "hello",
"meta": gin.H{"page": 1},
})
})
w := PerformRequest(r, http.MethodGet, "/test", nil)
// Verify the body is valid JSON
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, true, resp["success"])
assert.Equal(t, "hello", resp["data"])
}