package v1 import ( "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/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) type AuditHandlerTestSuite struct { suite.Suite db *gorm.DB handler *AuditHandler account *model.Account } func (s *AuditHandlerTestSuite) 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.Audit{})) s.db = db repo := repository.NewAuditRepo(db) svc := service.NewAuditService(repo) s.handler = NewAuditHandler(svc) s.account = &model.Account{Name: "test-audit-account"} s.Require().NoError(db.Create(s.account).Error) } func (s *AuditHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func TestAuditHandlerSuite(t *testing.T) { suite.Run(t, new(AuditHandlerTestSuite)) } func (s *AuditHandlerTestSuite) SetupTest() { s.Require().NoError(s.db.Exec("DELETE FROM audits").Error) } func (s *AuditHandlerTestSuite) TestList_ChatwootPayloadFiltersAndSerializer() { r := gin.New() r.GET("/api/v1/accounts/:account_id/audit_logs", withAuditRole("administrator", s.handler.List)) createdAt := time.Unix(1710000000, 0).UTC() userID := uint(42) associatedID := s.account.ID version := 3 audit := &model.Audit{ AccountID: &s.account.ID, UserID: &userID, AuditableType: "Conversation", AuditableID: 777, Action: "update", AuditedChanges: json.RawMessage(`{"status":["open","resolved"]}`), AssociatedType: "Account", AssociatedID: &associatedID, Username: "admin@example.com", RemoteAddress: "203.0.113.10", RequestUUID: "req-123", Version: &version, Comment: "status changed", UserType: "User", CreatedAt: createdAt, } s.Require().NoError(s.db.Create(audit).Error) otherAccount := &model.Account{Name: "other-audit-account"} s.Require().NoError(s.db.Create(otherAccount).Error) s.Require().NoError(s.db.Create(&model.Audit{ AccountID: &otherAccount.ID, AuditableType: "Conversation", AuditableID: 999, Action: "update", AuditedChanges: json.RawMessage(`{}`), }).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/audit_logs?page=1&per_page=100&action=update&auditable_type=Conversation", s.account.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)) assert.NotContains(s.T(), body, "success") assert.Equal(s.T(), float64(25), body["per_page"]) assert.Equal(s.T(), float64(1), body["current_page"]) assert.Equal(s.T(), float64(1), body["total_entries"]) logs := body["audit_logs"].([]any) s.Require().Len(logs, 1) log := logs[0].(map[string]any) assert.Equal(s.T(), float64(audit.ID), log["id"]) assert.Equal(s.T(), float64(777), log["auditable_id"]) assert.Equal(s.T(), "Conversation", log["auditable_type"]) assert.Contains(s.T(), log, "auditable") assert.Nil(s.T(), log["auditable"]) assert.Equal(s.T(), float64(s.account.ID), log["associated_id"]) assert.Equal(s.T(), "Account", log["associated_type"]) assert.Equal(s.T(), float64(userID), log["user_id"]) assert.Equal(s.T(), "User", log["user_type"]) assert.Equal(s.T(), "admin@example.com", log["username"]) assert.Equal(s.T(), "update", log["action"]) assert.Equal(s.T(), float64(version), log["version"]) assert.Equal(s.T(), "status changed", log["comment"]) assert.Equal(s.T(), "req-123", log["request_uuid"]) assert.Equal(s.T(), float64(createdAt.Unix()), log["created_at"]) assert.Equal(s.T(), "203.0.113.10", log["remote_address"]) } func (s *AuditHandlerTestSuite) TestList_AssociatedAccountScopeAndFixedPageSize() { r := gin.New() r.GET("/api/v1/accounts/:account_id/audit_logs", withAuditRole("administrator", s.handler.List)) associatedID := s.account.ID for i := 0; i < 26; i++ { s.Require().NoError(s.db.Create(&model.Audit{ AuditableType: "Conversation", AuditableID: uint(i + 1), Action: "update", AuditedChanges: json.RawMessage(`{}`), AssociatedType: "Account", AssociatedID: &associatedID, CreatedAt: time.Unix(int64(1710000000+i), 0), }).Error) } w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/audit_logs?page=2&per_page=1", s.account.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)) assert.Equal(s.T(), float64(25), body["per_page"]) assert.Equal(s.T(), float64(2), body["current_page"]) assert.Equal(s.T(), float64(26), body["total_entries"]) assert.Len(s.T(), body["audit_logs"].([]any), 1) } func (s *AuditHandlerTestSuite) TestList_ChatwootPaginationMetaAndDescendingOrder() { r := gin.New() r.GET("/api/v1/accounts/:account_id/audit_logs", withAuditRole("administrator", s.handler.List)) associatedID := s.account.ID createdAt := time.Unix(1710000000, 0).UTC() for i := 0; i < 30; i++ { s.Require().NoError(s.db.Create(&model.Audit{ AuditableType: "Conversation", AuditableID: uint(i + 1), Action: "update", AuditedChanges: json.RawMessage(`{}`), AssociatedType: "Account", AssociatedID: &associatedID, CreatedAt: createdAt.Add(time.Duration(i) * time.Minute), }).Error) } w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/audit_logs?page=2", s.account.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)) assert.Equal(s.T(), float64(25), body["per_page"]) assert.Equal(s.T(), float64(2), body["current_page"]) assert.Equal(s.T(), float64(30), body["total_entries"]) logs := body["audit_logs"].([]any) s.Require().Len(logs, 5) for index, item := range logs { log := item.(map[string]any) expectedAuditableID := float64(5 - index) assert.Equal(s.T(), expectedAuditableID, log["auditable_id"]) assert.Equal(s.T(), float64(createdAt.Add(time.Duration(4-index)*time.Minute).Unix()), log["created_at"]) } } func (s *AuditHandlerTestSuite) TestList_UnauthorizedForAgent() { r := gin.New() r.GET("/api/v1/accounts/:account_id/audit_logs", withAuditRole("agent", s.handler.List)) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/audit_logs", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusUnauthorized, w.Code) } func (s *AuditHandlerTestSuite) TestGet_BadRequest_InvalidID() { r := gin.New() r.GET("/api/v1/accounts/:account_id/audit_logs/:id", withAuditRole("administrator", s.handler.Get)) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/audit_logs/abc", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func withAuditRole(role string, h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { c.Set("role", role) h(c) } }