309 lines
15 KiB
Go
309 lines
15 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"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 LiveReportHandlerTestSuite struct {
|
|
suite.Suite
|
|
db *gorm.DB
|
|
handler *LiveReportHandler
|
|
router *gin.Engine
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) SetupSuite() {
|
|
s.db, _ = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
s.db.AutoMigrate(&model.Account{}, &model.Team{}, &model.Conversation{}, &model.Message{}, &model.ReportingEventsRollup{})
|
|
|
|
anSvc := service.NewAnalyticsService(
|
|
repository.NewReportingEventRepo(s.db),
|
|
repository.NewReportingEventsRollupRepo(s.db),
|
|
)
|
|
s.handler = NewLiveReportHandler(anSvc)
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
r.GET("/api/v1/accounts/:account_id/live_reports/conversation_metrics", s.handler.ConversationMetrics)
|
|
r.GET("/api/v1/accounts/:account_id/live_reports/grouped_conversation_metrics", s.handler.GroupedConversationMetrics)
|
|
r.GET("/api/v2/accounts/:account_id/live_reports/conversation_metrics", s.handler.ConversationMetrics)
|
|
r.GET("/api/v2/accounts/:account_id/live_reports/grouped_conversation_metrics", s.handler.GroupedConversationMetrics)
|
|
s.router = r
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) SetupTest() {
|
|
s.db.Exec("DELETE FROM messages")
|
|
s.db.Exec("DELETE FROM conversations")
|
|
s.db.Exec("DELETE FROM teams")
|
|
}
|
|
|
|
func TestLiveReportHandlerTestSuite(t *testing.T) {
|
|
suite.Run(t, new(LiveReportHandlerTestSuite))
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestConversationMetrics_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/live_reports/conversation_metrics", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestConversationMetrics_Success() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/live_reports/conversation_metrics", 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.NotContains(body, "success")
|
|
s.Equal(float64(0), body["open"])
|
|
s.Equal(float64(0), body["unattended"])
|
|
s.Equal(float64(0), body["unassigned"])
|
|
s.Equal(float64(0), body["pending"])
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestAPIV2LiveReports_ChatwootFrontendPayloadShapes() {
|
|
team := model.Team{AccountID: 1, Name: "Support"}
|
|
otherTeam := model.Team{AccountID: 1, Name: "Other"}
|
|
s.Require().NoError(s.db.Create(&team).Error)
|
|
s.Require().NoError(s.db.Create(&otherTeam).Error)
|
|
agentID := uint(7)
|
|
otherAgentID := uint(8)
|
|
firstReply := int64(1760000000)
|
|
waitingSince := int64(1760000100)
|
|
teamOpen := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &agentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
teamReplied := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &agentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", FirstReplyCreatedAt: &firstReply}
|
|
teamWaiting := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &otherAgentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", FirstReplyCreatedAt: &firstReply, WaitingSince: &waitingSince}
|
|
teamPending := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &agentID, Status: string(model.ConversationStatusPending), ChannelType: "web_widget", Channel: "web_widget"}
|
|
unassigned := model.Conversation{AccountID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
otherTeamOpen := model.Conversation{AccountID: 1, TeamID: &otherTeam.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
otherAccountOpen := model.Conversation{AccountID: 2, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(&teamOpen).Error)
|
|
s.Require().NoError(s.db.Create(&teamReplied).Error)
|
|
s.Require().NoError(s.db.Create(&teamWaiting).Error)
|
|
s.Require().NoError(s.db.Create(&teamPending).Error)
|
|
s.Require().NoError(s.db.Create(&unassigned).Error)
|
|
s.Require().NoError(s.db.Create(&otherTeamOpen).Error)
|
|
s.Require().NoError(s.db.Create(&otherAccountOpen).Error)
|
|
s.Require().NoError(s.db.Create(&model.Message{AccountID: 1, ConversationID: teamReplied.ID, MessageType: string(model.MessageTypeOutgoing), Content: "already handled"}).Error)
|
|
|
|
metrics := httptest.NewRecorder()
|
|
s.router.ServeHTTP(metrics, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/conversation_metrics", nil))
|
|
s.Equal(http.StatusOK, metrics.Code)
|
|
metricsBody := decodeLiveReportObject(s.T(), metrics.Body.String())
|
|
assertLiveReportConversationMetricsShape(s.T(), metricsBody)
|
|
s.Equal(float64(5), metricsBody["open"])
|
|
s.Equal(float64(4), metricsBody["unattended"])
|
|
s.Equal(float64(2), metricsBody["unassigned"])
|
|
s.Equal(float64(1), metricsBody["pending"])
|
|
s.NotContains(metricsBody, "success")
|
|
|
|
teamMetrics := httptest.NewRecorder()
|
|
s.router.ServeHTTP(teamMetrics, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/conversation_metrics?team_id="+strconv.FormatUint(uint64(team.ID), 10), nil))
|
|
s.Equal(http.StatusOK, teamMetrics.Code)
|
|
teamMetricsBody := decodeLiveReportObject(s.T(), teamMetrics.Body.String())
|
|
assertLiveReportConversationMetricsShape(s.T(), teamMetricsBody)
|
|
s.Equal(float64(3), teamMetricsBody["open"])
|
|
s.Equal(float64(2), teamMetricsBody["unattended"])
|
|
s.Equal(float64(0), teamMetricsBody["unassigned"])
|
|
s.Equal(float64(1), teamMetricsBody["pending"])
|
|
|
|
groupedByAssignee := httptest.NewRecorder()
|
|
s.router.ServeHTTP(groupedByAssignee, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/grouped_conversation_metrics?group_by=assignee_id", nil))
|
|
s.Equal(http.StatusOK, groupedByAssignee.Code)
|
|
assigneeRows := decodeLiveReportArray(s.T(), groupedByAssignee.Body.String())
|
|
s.Require().Len(assigneeRows, 3)
|
|
assertLiveReportGroupedMetricShape(s.T(), assigneeRows[0], "assignee_id")
|
|
s.Nil(assigneeRows[0]["assignee_id"])
|
|
s.Equal(float64(2), assigneeRows[0]["open"])
|
|
s.Equal(float64(2), assigneeRows[0]["unattended"])
|
|
s.Equal(float64(2), assigneeRows[0]["unassigned"])
|
|
s.Equal(float64(agentID), assigneeRows[1]["assignee_id"])
|
|
s.Equal(float64(2), assigneeRows[1]["open"])
|
|
s.Equal(float64(1), assigneeRows[1]["unattended"])
|
|
s.Equal(float64(otherAgentID), assigneeRows[2]["assignee_id"])
|
|
s.Equal(float64(1), assigneeRows[2]["open"])
|
|
s.Equal(float64(1), assigneeRows[2]["unattended"])
|
|
|
|
groupedByTeam := httptest.NewRecorder()
|
|
s.router.ServeHTTP(groupedByTeam, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/grouped_conversation_metrics?group_by=team_id", nil))
|
|
s.Equal(http.StatusOK, groupedByTeam.Code)
|
|
teamRows := decodeLiveReportArray(s.T(), groupedByTeam.Body.String())
|
|
s.Require().Len(teamRows, 3)
|
|
assertLiveReportGroupedMetricShape(s.T(), teamRows[0], "team_id")
|
|
s.Nil(teamRows[0]["team_id"])
|
|
s.Equal(float64(1), teamRows[0]["open"])
|
|
s.Equal(float64(1), teamRows[0]["unattended"])
|
|
s.Equal(float64(team.ID), teamRows[1]["team_id"])
|
|
s.Equal(float64(3), teamRows[1]["open"])
|
|
s.Equal(float64(2), teamRows[1]["unattended"])
|
|
s.Equal(float64(otherTeam.ID), teamRows[2]["team_id"])
|
|
s.Equal(float64(1), teamRows[2]["open"])
|
|
|
|
invalidGroup := httptest.NewRecorder()
|
|
s.router.ServeHTTP(invalidGroup, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/grouped_conversation_metrics?group_by=bad", nil))
|
|
s.Equal(http.StatusUnprocessableEntity, invalidGroup.Code)
|
|
errorBody := decodeLiveReportObject(s.T(), invalidGroup.Body.String())
|
|
s.Equal("invalid group_by", errorBody["error"])
|
|
s.NotContains(errorBody, "success")
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestAPIV2LiveReports_StoreRefreshSequenceMatchesChatwootFrontend() {
|
|
team := model.Team{AccountID: 1, Name: "Store Team"}
|
|
s.Require().NoError(s.db.Create(&team).Error)
|
|
agentID := uint(11)
|
|
teamOpen := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &agentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
unassigned := model.Conversation{AccountID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
pending := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &agentID, Status: string(model.ConversationStatusPending), ChannelType: "web_widget", Channel: "web_widget"}
|
|
otherAccount := model.Conversation{AccountID: 2, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(&teamOpen).Error)
|
|
s.Require().NoError(s.db.Create(&unassigned).Error)
|
|
s.Require().NoError(s.db.Create(&pending).Error)
|
|
s.Require().NoError(s.db.Create(&otherAccount).Error)
|
|
|
|
accountMetric := httptest.NewRecorder()
|
|
s.router.ServeHTTP(accountMetric, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/conversation_metrics", nil))
|
|
s.Equal(http.StatusOK, accountMetric.Code)
|
|
accountPayload := decodeLiveReportObject(s.T(), accountMetric.Body.String())
|
|
s.Equal(float64(2), accountPayload["open"])
|
|
s.Equal(float64(2), accountPayload["unattended"])
|
|
s.Equal(float64(1), accountPayload["unassigned"])
|
|
s.Equal(float64(1), accountPayload["pending"])
|
|
s.NotContains(accountPayload, "payload")
|
|
s.NotContains(accountPayload, "success")
|
|
|
|
agentMetric := httptest.NewRecorder()
|
|
s.router.ServeHTTP(agentMetric, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/grouped_conversation_metrics?group_by=assignee_id", nil))
|
|
s.Equal(http.StatusOK, agentMetric.Code)
|
|
agentRows := decodeLiveReportArray(s.T(), agentMetric.Body.String())
|
|
s.Require().Len(agentRows, 2)
|
|
s.Nil(agentRows[0]["assignee_id"])
|
|
s.Equal(float64(1), agentRows[0]["open"])
|
|
s.Equal(float64(agentID), agentRows[1]["assignee_id"])
|
|
s.Equal(float64(1), agentRows[1]["open"])
|
|
s.Equal(float64(1), agentRows[1]["unattended"])
|
|
s.Equal(float64(0), agentRows[1]["unassigned"])
|
|
|
|
teamMetric := httptest.NewRecorder()
|
|
s.router.ServeHTTP(teamMetric, httptest.NewRequest(http.MethodGet, "/api/v2/accounts/1/live_reports/grouped_conversation_metrics?group_by=team_id", nil))
|
|
s.Equal(http.StatusOK, teamMetric.Code)
|
|
teamRows := decodeLiveReportArray(s.T(), teamMetric.Body.String())
|
|
s.Require().Len(teamRows, 2)
|
|
s.Nil(teamRows[0]["team_id"])
|
|
s.Equal(float64(1), teamRows[0]["open"])
|
|
s.Equal(float64(team.ID), teamRows[1]["team_id"])
|
|
s.Equal(float64(1), teamRows[1]["open"])
|
|
s.Equal(float64(1), teamRows[1]["unattended"])
|
|
s.Equal(float64(0), teamRows[1]["unassigned"])
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestGroupedConversationMetrics_InvalidGroupByReturnsChatwootError() {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/live_reports/grouped_conversation_metrics?group_by=invalid_param", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
var body map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
|
s.Equal("invalid group_by", body["error"])
|
|
s.NotContains(body, "success")
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestGroupedConversationMetrics_ByAssignee() {
|
|
agentID := uint(42)
|
|
firstReply := int64(1760000000)
|
|
assigned := model.Conversation{AccountID: 1, AssigneeID: &agentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
replied := model.Conversation{AccountID: 1, AssigneeID: &agentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", FirstReplyCreatedAt: &firstReply}
|
|
unassigned := model.Conversation{AccountID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(&assigned).Error)
|
|
s.Require().NoError(s.db.Create(&replied).Error)
|
|
s.Require().NoError(s.db.Create(&unassigned).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/live_reports/grouped_conversation_metrics?group_by=assignee_id", 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.Len(body, 2)
|
|
s.Nil(body[0]["assignee_id"])
|
|
s.Equal(float64(1), body[0]["open"])
|
|
s.Equal(float64(1), body[0]["unattended"])
|
|
s.Equal(float64(1), body[0]["unassigned"])
|
|
s.Equal(float64(agentID), body[1]["assignee_id"])
|
|
s.Equal(float64(2), body[1]["open"])
|
|
s.Equal(float64(1), body[1]["unattended"])
|
|
s.Equal(float64(0), body[1]["unassigned"])
|
|
}
|
|
|
|
func (s *LiveReportHandlerTestSuite) TestGroupedConversationMetrics_FiltersByTeamID() {
|
|
team := model.Team{AccountID: 1, Name: "Support"}
|
|
otherTeam := model.Team{AccountID: 1, Name: "Other"}
|
|
s.Require().NoError(s.db.Create(&team).Error)
|
|
s.Require().NoError(s.db.Create(&otherTeam).Error)
|
|
agentID := uint(7)
|
|
teamConversation := model.Conversation{AccountID: 1, TeamID: &team.ID, AssigneeID: &agentID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
otherConversation := model.Conversation{AccountID: 1, TeamID: &otherTeam.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(&teamConversation).Error)
|
|
s.Require().NoError(s.db.Create(&otherConversation).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/live_reports/grouped_conversation_metrics?group_by=assignee_id&team_id="+strconv.FormatUint(uint64(team.ID), 10), 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.Len(body, 1)
|
|
s.Equal(float64(agentID), body[0]["assignee_id"])
|
|
s.Equal(float64(1), body[0]["open"])
|
|
s.Equal(float64(1), body[0]["unattended"])
|
|
s.Equal(float64(0), body[0]["unassigned"])
|
|
}
|
|
|
|
func decodeLiveReportObject(t *testing.T, body string) map[string]interface{} {
|
|
t.Helper()
|
|
payload := map[string]interface{}{}
|
|
if err := json.Unmarshal([]byte(body), &payload); err != nil {
|
|
t.Fatalf("expected JSON object: %v\n%s", err, body)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func decodeLiveReportArray(t *testing.T, body string) []map[string]interface{} {
|
|
t.Helper()
|
|
payload := []map[string]interface{}{}
|
|
if err := json.Unmarshal([]byte(body), &payload); err != nil {
|
|
t.Fatalf("expected JSON array: %v\n%s", err, body)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func assertLiveReportConversationMetricsShape(t *testing.T, payload map[string]interface{}) {
|
|
t.Helper()
|
|
for _, key := range []string{"open", "unattended", "unassigned", "pending"} {
|
|
if _, ok := payload[key]; !ok {
|
|
t.Fatalf("expected live conversation metrics to include %q, got %#v", key, payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertLiveReportGroupedMetricShape(t *testing.T, payload map[string]interface{}, groupKey string) {
|
|
t.Helper()
|
|
for _, key := range []string{groupKey, "open", "unattended", "unassigned"} {
|
|
if _, ok := payload[key]; !ok {
|
|
t.Fatalf("expected live grouped metric to include %q, got %#v", key, payload)
|
|
}
|
|
}
|
|
}
|