package v1 import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "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("/:id", s.handler.Get) accountsGroup.POST("", s.handler.Create) accountsGroup.PUT("/:id", s.handler.Update) accountsGroup.DELETE("/:id", s.handler.Delete) accountsGroup.PUT("/:id/settings", s.handler.UpdateSettings) accountsGroup.GET("/:id/agents", s.handler.GetAgents) accountsGroup.GET("/:id/users", s.handler.ListUsers) accountsGroup.POST("/:id/users", s.handler.AddUser) accountsGroup.DELETE("/:id/users/:user_id", s.handler.RemoveUser) } } 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) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "Get Account", data["name"]) } 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.StatusCreated, w.Code) resp := s.unmarshalResponse(w) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "New Test Account", data["name"]) } 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) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "After Update", data["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) // Should succeed or return acceptable status assert.True(s.T(), w.Code == http.StatusOK || w.Code == http.StatusNoContent || w.Code < 500) } // ====== 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) }