276 lines
9.2 KiB
Go
276 lines
9.2 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
)
|
|
|
|
type AnalyticsHandlerTestSuite struct {
|
|
suite.Suite
|
|
db *gorm.DB
|
|
svc *service.AnalyticsService
|
|
handler *AnalyticsHandler
|
|
router *gin.Engine
|
|
accountID uint
|
|
}
|
|
|
|
func TestAnalyticsHandlerTestSuite(t *testing.T) {
|
|
suite.Run(t, new(AnalyticsHandlerTestSuite))
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) SetupSuite() {
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
s.Require().NoError(err)
|
|
|
|
s.db = db
|
|
s.Require().NoError(db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.Conversation{},
|
|
&model.ReportingEvent{},
|
|
&model.ReportingEventsRollup{},
|
|
))
|
|
|
|
// Create test account
|
|
acct := model.Account{Name: "TestAccount"}
|
|
s.Require().NoError(db.Create(&acct).Error)
|
|
s.accountID = acct.ID
|
|
|
|
eventRepo := repository.NewReportingEventRepo(db)
|
|
rollupRepo := repository.NewReportingEventsRollupRepo(db)
|
|
s.svc = service.NewAnalyticsService(eventRepo, rollupRepo)
|
|
s.handler = NewAnalyticsHandler(s.svc)
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
accounts := r.Group("/api/v1/accounts/:account_id")
|
|
accounts.GET("/reports", s.handler.Index)
|
|
accounts.GET("/reports/summary", s.handler.Summary)
|
|
accounts.GET("/reports/agents", s.handler.AgentMetrics)
|
|
accounts.GET("/reports/inboxes", s.handler.InboxMetrics)
|
|
accounts.GET("/reports/labels", s.handler.LabelMetrics)
|
|
accounts.GET("/reports/teams", s.handler.TeamMetrics)
|
|
accounts.GET("/reports/conversation_traffic", s.handler.ConversationTraffic)
|
|
s.router = r
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TearDownSuite() {
|
|
sqlDB, _ := s.db.DB()
|
|
sqlDB.Close()
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) SetupTest() {
|
|
s.db.Exec("DELETE FROM reporting_events")
|
|
s.db.Exec("DELETE FROM reporting_events_rollups")
|
|
s.db.Exec("DELETE FROM conversations")
|
|
}
|
|
|
|
// ========== parseAccountID / parseDateRange edge cases ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/abc/reports/summary?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_MissingSinceParam() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/summary?until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_MissingUntilParam() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/summary?since=2025-01-01T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_InvalidSinceFormat() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/summary?since=not-a-date&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_InvalidUntilFormat() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/summary?since=2025-01-01T00:00:00Z&until=not-a-date", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// ========== Summary ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_EmptyData() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/summary?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var body map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
|
s.True(body["success"].(bool))
|
|
data := body["data"].(map[string]interface{})
|
|
s.NotNil(data["metrics"])
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestSummary_WithData() {
|
|
// Seed a reporting event
|
|
ev := model.ReportingEvent{
|
|
AccountID: s.accountID,
|
|
Name: "first_response",
|
|
Value: 120.5,
|
|
ValueInBusinessHours: 60.0,
|
|
EventStartTime: parseTime("2025-01-15T10:00:00Z"),
|
|
EventEndTime: parseTime("2025-01-15T10:02:00Z"),
|
|
}
|
|
s.Require().NoError(s.db.Create(&ev).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/summary?since=2025-01-01T00:00:00Z&until=2025-02-01T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestIndex_TimeseriesWithData() {
|
|
conv := model.Conversation{AccountID: s.accountID, InboxID: 1, ContactID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(&conv).Error)
|
|
s.Require().NoError(s.db.Model(&conv).Updates(map[string]interface{}{"created_at": parseTime("2025-01-15T10:00:00Z")}).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports?metric=conversations_count&since=2025-01-01T00:00:00Z&until=2025-02-01T00:00:00Z&type=account&group_by=day", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
var body map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
|
data := body["data"].([]interface{})
|
|
s.Len(data, 1)
|
|
s.Equal(float64(1), data[0].(map[string]interface{})["value"])
|
|
}
|
|
|
|
// ========== AgentMetrics ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestAgentMetrics_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/abc/reports/agents?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestAgentMetrics_EmptyData() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/agents?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
}
|
|
|
|
// ========== InboxMetrics ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestInboxMetrics_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/abc/reports/inboxes?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestInboxMetrics_EmptyData() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/inboxes?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
}
|
|
|
|
// ========== LabelMetrics ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestLabelMetrics_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/abc/reports/labels?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestLabelMetrics_EmptyData() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/labels?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
}
|
|
|
|
// ========== TeamMetrics ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestTeamMetrics_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/abc/reports/teams?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestTeamMetrics_EmptyData() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/teams?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
}
|
|
|
|
// ========== ConversationTraffic ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestConversationTraffic_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/abc/reports/conversation_traffic?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestConversationTraffic_EmptyData() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/accounts/1/reports/conversation_traffic?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
}
|
|
|
|
// ========== Nil service guard ==========
|
|
|
|
func (s *AnalyticsHandlerTestSuite) TestNilService() {
|
|
h := NewAnalyticsHandler(nil)
|
|
// Handler with nil svc will panic on method calls — skip creating a router
|
|
// Instead just verify NewAnalyticsHandler(nil) doesn't panic at construction
|
|
s.NotNil(h)
|
|
}
|
|
|
|
func parseTime(s string) time.Time {
|
|
t, _ := time.Parse(time.RFC3339, s)
|
|
return t
|
|
}
|