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