package v1 import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" "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" ) type InboxMemberHandlerTestSuite struct { suite.Suite db *gorm.DB handler *InboxMemberHandler account *model.Account inbox *model.Inbox } func (s *InboxMemberHandlerTestSuite) SetupSuite() { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) s.Require().NoError(err) s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.User{}, &model.InboxMember{})) s.db = db repo := repository.NewInboxMemberRepo(db) svc := service.NewInboxMemberService(repo) s.handler = NewInboxMemberHandler(svc) s.account = &model.Account{Name: "test-inbox-member-account"} s.Require().NoError(db.Create(s.account).Error) s.inbox = &model.Inbox{AccountID: s.account.ID, Name: "test-inbox", ChannelType: string(model.InboxChannelTypeWebWidget)} s.Require().NoError(db.Create(s.inbox).Error) } func (s *InboxMemberHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func TestInboxMemberHandlerSuite(t *testing.T) { suite.Run(t, new(InboxMemberHandlerTestSuite)) } func (s *InboxMemberHandlerTestSuite) TestListMembers_BadRequest_InvalidInboxID() { r := gin.New() r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/members", s.handler.ListMembers) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/inboxes/abc/members", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *InboxMemberHandlerTestSuite) TestListMembers_Success() { r := gin.New() r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/members", s.handler.ListMembers) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/members", s.account.ID, s.inbox.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var body map[string]any s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &body)) s.Require().Contains(body, "payload") s.Require().NotContains(body, "members") } func (s *InboxMemberHandlerTestSuite) TestAddMember_BadRequest_InvalidInboxID() { r := gin.New() r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/members", s.handler.AddMember) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/inboxes/abc/members", s.account.ID), bytes.NewBufferString(`{"user_ids":[1]}`)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *InboxMemberHandlerTestSuite) TestAccountScopedInboxMembers_ChatwootPayloadAndDiffUpdate() { s.db.Exec("DELETE FROM inbox_members") users := []*model.User{ {AccountID: s.account.ID, Name: "Agent One", DisplayName: "Agent 1", Email: "agent1@example.com", Password: "password", Provider: "email", Role: "agent", ConfirmedAt: ptrTimeNow()}, {AccountID: s.account.ID, Name: "Agent Two", Email: "agent2@example.com", Password: "password", Provider: "email", Role: "agent"}, {AccountID: s.account.ID, Name: "Agent Three", Email: "agent3@example.com", Password: "password", Provider: "email", Role: "agent"}, } for _, user := range users { s.Require().NoError(s.db.Create(user).Error) } s.Require().NoError(s.db.Create(&model.InboxMember{InboxID: s.inbox.ID, UserID: users[0].ID, Role: "agent", AvailabilityStatus: "online"}).Error) r := gin.New() r.GET("/api/v1/accounts/:account_id/inbox_members/:inbox_id", s.handler.ShowAccountScoped) r.POST("/api/v1/accounts/:account_id/inbox_members", s.handler.CreateAccountScoped) r.PATCH("/api/v1/accounts/:account_id/inbox_members", s.handler.UpdateAccountScoped) r.DELETE("/api/v1/accounts/:account_id/inbox_members", s.handler.DestroyAccountScoped) show := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/inbox_members/%d", s.account.ID, s.inbox.ID), nil) r.ServeHTTP(show, req) s.Require().Equal(http.StatusOK, show.Code, show.Body.String()) payload := inboxMemberPayload(s.T(), show) s.Require().Len(payload, 1) s.Require().Equal("Agent 1", payload[0]["available_name"]) s.Require().Equal("online", payload[0]["availability_status"]) s.Require().NotContains(payload[0], "inbox_id") create := httptest.NewRecorder() body := fmt.Sprintf(`{"inbox_id":%d,"user_ids":[%d,%d,%d]}`, s.inbox.ID, users[0].ID, users[1].ID, users[1].ID) req, _ = http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/inbox_members", s.account.ID), bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(create, req) s.Require().Equal(http.StatusOK, create.Code, create.Body.String()) payload = inboxMemberPayload(s.T(), create) s.Require().Len(payload, 2) update := httptest.NewRecorder() body = fmt.Sprintf(`{"inbox_id":%d,"user_ids":[%d]}`, s.inbox.ID, users[2].ID) req, _ = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/inbox_members", s.account.ID), bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(update, req) s.Require().Equal(http.StatusOK, update.Code, update.Body.String()) payload = inboxMemberPayload(s.T(), update) s.Require().Len(payload, 1) s.Require().Equal(float64(users[2].ID), payload[0]["id"]) destroy := httptest.NewRecorder() body = fmt.Sprintf(`{"inbox_id":%d,"user_ids":[%d]}`, s.inbox.ID, users[2].ID) req, _ = http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/inbox_members", s.account.ID), bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(destroy, req) s.Require().Equal(http.StatusOK, destroy.Code, destroy.Body.String()) var count int64 s.Require().NoError(s.db.Model(&model.InboxMember{}).Where("inbox_id = ?", s.inbox.ID).Count(&count).Error) s.Require().Zero(count) } func inboxMemberPayload(t *testing.T, response *httptest.ResponseRecorder) []map[string]any { t.Helper() var body struct { Payload []map[string]any `json:"payload"` } require.NoError(t, json.Unmarshal(response.Body.Bytes(), &body), response.Body.String()) return body.Payload } func ptrTimeNow() *time.Time { now := time.Now() return &now } func (s *InboxMemberHandlerTestSuite) TestRemoveMember_BadRequest_InvalidInboxID() { r := gin.New() r.DELETE("/api/v1/accounts/:account_id/inboxes/:inbox_id/members/:user_id", s.handler.RemoveMember) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/inboxes/abc/members/1", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) }