Files
gochat/internal/handler/api/v1/account_handler_test.go
T

493 lines
15 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
)
// --- Account Handler Test Suite ---
// Uses real SQLite DB + real repo + real service.
// AccountHandler stores *service.AccountService (concrete type), so mocks can't be injected.
type AccountHandlerTestSuite struct {
suite.Suite
router *gin.Engine
handler *AccountHandler
db *gorm.DB
testUserID uint
}
func (s *AccountHandlerTestSuite) SetupSuite() {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
s.Require().NoError(err)
s.db = db
err = db.AutoMigrate(
&model.Account{},
&model.User{},
&model.AccountUser{},
)
s.Require().NoError(err)
// Create a persistent test user for authentication
user := &model.User{
Name: "Test Auth User",
Email: "auth@test.com",
Password: "hashedpassword",
Role: "administrator",
Active: true,
}
s.Require().NoError(db.Create(user).Error)
s.testUserID = user.ID
// Create real repo + service
accountRepo := repository.NewAccountRepo(db)
accountSvc := service.NewAccountService(accountRepo)
// Create handler
s.handler = NewAccountHandler(accountSvc)
// Setup router with middleware that injects userID into context
// (simulating what the auth middleware does in production)
s.router = gin.New()
s.router.Use(func(c *gin.Context) {
c.Set("user_id", s.testUserID)
c.Next()
})
accountsGroup := s.router.Group("/api/v1/accounts")
{
accountsGroup.GET("/all", s.handler.GetAll)
accountsGroup.GET("", s.handler.List)
accountsGroup.GET("/:account_id", s.handler.Get)
accountsGroup.POST("", s.handler.Create)
accountsGroup.PUT("/:account_id", s.handler.Update)
accountsGroup.DELETE("/:account_id", s.handler.Delete)
accountsGroup.PUT("/:account_id/settings", s.handler.UpdateSettings)
accountsGroup.GET("/:account_id/agents", s.handler.GetAgents)
accountsGroup.GET("/:account_id/users", s.handler.ListUsers)
accountsGroup.POST("/:account_id/users", s.handler.AddUser)
accountsGroup.DELETE("/:account_id/users/:user_id", s.handler.RemoveUser)
// G8 extension routes
accountsGroup.POST("/:account_id/update_active_at", s.handler.UpdateActiveAt)
accountsGroup.GET("/:account_id/cache_keys", s.handler.CacheKeys)
}
}
func (s *AccountHandlerTestSuite) TearDownSuite() {
if s.db != nil {
sqlDB, _ := s.db.DB()
sqlDB.Close()
}
}
func (s *AccountHandlerTestSuite) SetupTest() {
// Clean tables before each test
s.db.Exec("DELETE FROM account_users")
s.db.Exec("DELETE FROM accounts")
s.db.Exec("DELETE FROM sqlite_sequence WHERE name IN ('account_users','accounts')")
}
func TestAccountHandlerSuite(t *testing.T) {
suite.Run(t, new(AccountHandlerTestSuite))
}
// --- Helper: create a test account directly in DB ---
func (s *AccountHandlerTestSuite) seedAccount(name string) *model.Account {
acc := &model.Account{Name: name, Active: true, Status: "active"}
s.Require().NoError(s.db.Create(acc).Error)
return acc
}
// --- Helper: create a test user directly in DB ---
func (s *AccountHandlerTestSuite) seedUser(email string) *model.User {
user := &model.User{
Name: "Seeded User",
Email: email,
Password: "hashedpassword",
Role: "agent",
Active: true,
}
s.Require().NoError(s.db.Create(user).Error)
return user
}
// --- Helper: link user to account in DB ---
func (s *AccountHandlerTestSuite) seedAccountUser(userID, accountID uint, role string) {
au := &model.AccountUser{
UserID: userID,
AccountID: accountID,
Role: role,
}
s.Require().NoError(s.db.Create(au).Error)
}
// --- Helper: unmarshal response ---
func (s *AccountHandlerTestSuite) unmarshalResponse(w *httptest.ResponseRecorder) map[string]interface{} {
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
return resp
}
// ====== List Accounts ======
func (s *AccountHandlerTestSuite) TestList_Success() {
// Seed accounts linked to the test user
acc1 := s.seedAccount("List Account 1")
acc2 := s.seedAccount("List Account 2")
s.seedAccountUser(s.testUserID, acc1.ID, "administrator")
s.seedAccountUser(s.testUserID, acc2.ID, "agent")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
assert.NotNil(s.T(), resp["data"])
assert.NotNil(s.T(), resp["meta"])
}
func (s *AccountHandlerTestSuite) TestList_Empty() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].([]interface{})
assert.Equal(s.T(), 0, len(data))
}
// ====== Get Account ======
func (s *AccountHandlerTestSuite) TestGet_Success() {
acc := s.seedAccount("Get Account")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
assert.NotContains(s.T(), resp, "success")
assert.Equal(s.T(), "Get Account", resp["name"])
assert.Contains(s.T(), resp, "settings")
assert.Contains(s.T(), resp, "features")
assert.Contains(s.T(), resp, "cache_keys")
}
func (s *AccountHandlerTestSuite) TestGet_NotFound() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/99999", nil)
s.router.ServeHTTP(w, req)
// Should return error status
assert.True(s.T(), w.Code >= 400)
}
func (s *AccountHandlerTestSuite) TestGet_InvalidID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
// ====== Create Account ======
func (s *AccountHandlerTestSuite) TestCreate_Success() {
body := `{"name":"New Test Account"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].(map[string]interface{})
assert.NotZero(s.T(), data["account_id"])
assert.NotContains(s.T(), resp, "success")
}
func (s *AccountHandlerTestSuite) TestCreate_ChatwootAccountName() {
body := `{"account_name":"Chatwoot Account"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].(map[string]interface{})
assert.NotZero(s.T(), data["account_id"])
}
func (s *AccountHandlerTestSuite) TestCreate_MissingName() {
body := `{}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
// Should fail with 400 when name is missing
assert.True(s.T(), w.Code >= 400)
}
// ====== Update Account ======
func (s *AccountHandlerTestSuite) TestUpdate_Success() {
acc := s.seedAccount("Before Update")
body := `{"name":"After Update"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10), bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
assert.NotContains(s.T(), resp, "success")
assert.Equal(s.T(), "After Update", resp["name"])
}
// ====== Delete Account ======
func (s *AccountHandlerTestSuite) TestDelete_Success() {
acc := s.seedAccount("Delete Me")
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusNoContent, w.Code)
}
// ====== Update Settings ======
func (s *AccountHandlerTestSuite) TestUpdateSettings() {
acc := s.seedAccount("Settings Account")
body := `{"auto_assignment":true,"custom_settings":"value"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10)+"/settings", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
assert.NotContains(s.T(), resp, "success")
assert.Contains(s.T(), resp, "settings")
}
// ====== List Users ======
func (s *AccountHandlerTestSuite) TestListUsers() {
acc := s.seedAccount("Users List Account")
user1 := s.seedUser("listu1@test.com")
user2 := s.seedUser("listu2@test.com")
s.seedAccountUser(user1.ID, acc.ID, "administrator")
s.seedAccountUser(user2.ID, acc.ID, "agent")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10)+"/users?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
assert.NotNil(s.T(), resp["data"])
}
// ====== Add User ======
func (s *AccountHandlerTestSuite) TestAddUser() {
acc := s.seedAccount("Add User Account")
newUser := s.seedUser("adduser@test.com")
body := `{"user_id":` + strconv.FormatUint(uint64(newUser.ID), 10) + `,"role":"agent"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10)+"/users", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.True(s.T(), w.Code == http.StatusOK || w.Code < 500)
}
// ====== Remove User ======
func (s *AccountHandlerTestSuite) TestRemoveUser() {
acc := s.seedAccount("Remove User Account")
existingUser := s.seedUser("rmuser@test.com")
s.seedAccountUser(existingUser.ID, acc.ID, "agent")
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10)+"/users/"+strconv.FormatUint(uint64(existingUser.ID), 10), nil)
s.router.ServeHTTP(w, req)
assert.True(s.T(), w.Code == http.StatusOK || w.Code == http.StatusNoContent || w.Code < 500)
}
// ====== GetAll ======
func (s *AccountHandlerTestSuite) TestGetAll_Success() {
s.seedAccount("All Account 1")
s.seedAccount("All Account 2")
s.seedAccount("All Account 3")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/all?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].([]interface{})
assert.Equal(s.T(), 3, len(data))
assert.NotNil(s.T(), resp["meta"])
}
func (s *AccountHandlerTestSuite) TestGetAll_Empty() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/all?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].([]interface{})
assert.Equal(s.T(), 0, len(data))
}
func (s *AccountHandlerTestSuite) TestGetAll_Pagination() {
s.seedAccount("Pag Account 1")
s.seedAccount("Pag Account 2")
s.seedAccount("Pag Account 3")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/all?page=1&per_page=2", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].([]interface{})
assert.Equal(s.T(), 2, len(data))
meta := resp["meta"].(map[string]interface{})
assert.Equal(s.T(), float64(3), meta["total_count"])
}
// ====== GetAgents ======
func (s *AccountHandlerTestSuite) TestGetAgents_Success() {
acc := s.seedAccount("Agents Account")
user1 := s.seedUser("agent1@test.com")
user2 := s.seedUser("agent2@test.com")
s.seedAccountUser(user1.ID, acc.ID, "agent")
s.seedAccountUser(user2.ID, acc.ID, "administrator")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10)+"/agents?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].([]interface{})
assert.Equal(s.T(), 2, len(data))
assert.NotNil(s.T(), resp["meta"])
}
func (s *AccountHandlerTestSuite) TestGetAgents_Empty() {
acc := s.seedAccount("Empty Agents Account")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10)+"/agents?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
data := resp["data"].([]interface{})
assert.Equal(s.T(), 0, len(data))
}
func (s *AccountHandlerTestSuite) TestGetAgents_InvalidID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/agents?page=1&per_page=25", nil)
s.router.ServeHTTP(w, req)
assert.True(s.T(), w.Code >= 400)
}
// --- G8 extension handler tests: UpdateActiveAt, CacheKeys ---
// Reference: Chatwoot accounts_controller.rb#update_active_at, #cache_keys
func (s *AccountHandlerTestSuite) TestUpdateActiveAt_Success() {
account := s.seedAccount("G8 ActiveAt Account")
// Use the suite's testUserID (set by middleware) and link it to the seeded account
s.seedAccountUser(s.testUserID, account.ID, "administrator")
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/update_active_at", account.ID), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Empty(s.T(), w.Body.String())
// Verify active_at was updated in DB
var au model.AccountUser
require.NoError(s.T(), s.db.Where("account_id = ? AND user_id = ?", account.ID, s.testUserID).First(&au).Error)
assert.NotNil(s.T(), au.ActiveAt)
}
func (s *AccountHandlerTestSuite) TestUpdateActiveAt_InvalidID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/update_active_at", nil)
s.router.ServeHTTP(w, req)
assert.True(s.T(), w.Code >= 400)
}
func (s *AccountHandlerTestSuite) TestCacheKeys_Success() {
account := s.seedAccount("G8 CacheKeys Account")
// Use the suite's testUserID (set by middleware) and link it to the seeded account
s.seedAccountUser(s.testUserID, account.ID, "administrator")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/cache_keys", account.ID), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
data := s.unmarshalResponse(w)
keys := data["cache_keys"].(map[string]interface{})
assert.Equal(s.T(), "0000000000", keys["label"])
assert.Equal(s.T(), "0000000000", keys["inbox"])
assert.Equal(s.T(), "0000000000", keys["team"])
assert.NotContains(s.T(), data, "success")
}
func (s *AccountHandlerTestSuite) TestCacheKeys_InvalidID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/cache_keys", nil)
s.router.ServeHTTP(w, req)
assert.True(s.T(), w.Code >= 400)
}