Align GoChat with Chatwoot frontend contracts

This commit is contained in:
2026-06-13 22:13:32 +08:00
parent 71abf58636
commit d884fdda0a
162 changed files with 9825 additions and 528 deletions
@@ -143,6 +143,7 @@ func (s *ConversationHandlerTestSuite) SetupSuite() {
conversations.GET("/:conversation_id/attachments", handler.ListAttachments)
conversations.GET("/:conversation_id/reporting_events", handler.ReportingEvents)
conversations.POST("/:conversation_id/toggle_typing", handler.ToggleTyping)
conversations.POST("/:conversation_id/toggle_typing_status", handler.ToggleTyping)
conversations.POST("/:conversation_id/update_last_seen", handler.UpdateLastSeen)
conversations.DELETE("/:conversation_id", handler.Delete)
}
@@ -316,6 +317,47 @@ func (s *ConversationHandlerTestSuite) TestUnread_Success() {
assert.Equal(s.T(), s.testConv.ID, resp.ID)
}
func (s *ConversationHandlerTestSuite) TestUnread_DisplayIDRouteSetsLastSeenBeforeIncoming() {
displayID := uint(909)
s.Require().NoError(s.db.Model(s.testConv).Update("display_id", displayID).Error)
incomingCreatedAt := time.Now().Add(-10 * time.Minute)
oldSeen := incomingCreatedAt.Add(5 * time.Minute).Unix()
s.Require().NoError(s.db.Model(s.testConv).Updates(map[string]any{
"agent_last_seen_at": oldSeen,
"assignee_last_seen_at": oldSeen,
}).Error)
message := &model.Message{
AccountID: s.testAccount.ID,
InboxID: s.testInbox.ID,
ConversationID: s.testConv.ID,
MessageType: string(model.MessageTypeIncoming),
Content: "unread transition",
ContentType: "text",
SenderType: "contact",
}
s.Require().NoError(s.db.Create(message).Error)
s.Require().NoError(s.db.Model(message).Updates(map[string]any{"created_at": incomingCreatedAt, "updated_at": incomingCreatedAt}).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/"+strconv.FormatUint(uint64(displayID), 10)+"/unread", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
var resp map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(s.T(), float64(displayID), resp["id"])
assert.Equal(s.T(), incomingCreatedAt.Add(-time.Second).Unix(), int64(resp["agent_last_seen_at"].(float64)))
assert.Equal(s.T(), incomingCreatedAt.Add(-time.Second).Unix(), int64(resp["assignee_last_seen_at"].(float64)))
assert.NotContains(s.T(), resp, "success")
var stored model.Conversation
s.Require().NoError(s.db.First(&stored, s.testConv.ID).Error)
s.Require().NotNil(stored.AgentLastSeenAt)
s.Require().NotNil(stored.AssigneeLastSeenAt)
assert.Equal(s.T(), incomingCreatedAt.Add(-time.Second).Unix(), *stored.AgentLastSeenAt)
assert.Equal(s.T(), incomingCreatedAt.Add(-time.Second).Unix(), *stored.AssigneeLastSeenAt)
}
func (s *ConversationHandlerTestSuite) TestUnread_InvalidAccountID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/1/unread", nil)
@@ -799,6 +841,15 @@ func (s *ConversationHandlerTestSuite) TestToggleTyping_MissingFieldsIsNoopSucce
assert.Empty(s.T(), w.Body.String())
}
func (s *ConversationHandlerTestSuite) TestToggleTypingStatus_ChatwootRouteSuccess() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/toggle_typing_status", s.testAccount.ID, s.testConv.ID), bytes.NewBufferString(`{"typing_status":"typing_on","is_private":false}`))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Empty(s.T(), w.Body.String())
}
func (s *ConversationHandlerTestSuite) TestToggleTyping_EmptyBodyIsNoopSuccess() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/toggle_typing", s.testAccount.ID, s.testConv.ID), nil)
@@ -839,6 +890,33 @@ func (s *ConversationHandlerTestSuite) TestUpdateLastSeen_SuccessMarksNotificati
assert.NotNil(s.T(), updatedNotification.ReadAt)
}
func (s *ConversationHandlerTestSuite) TestUpdateLastSeen_DisplayIDRouteMarksNotificationRead() {
displayID := uint(910)
s.Require().NoError(s.db.Model(s.testConv).Update("display_id", displayID).Error)
oldSeen := time.Now().Add(-2 * time.Hour).Unix()
s.Require().NoError(s.db.Model(s.testConv).Update("agent_last_seen_at", oldSeen).Error)
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, MessageType: string(model.MessageTypeIncoming), Content: "new display-id message"}).Error)
accountID := s.testAccount.ID
notification := &model.Notification{UserID: s.testUser.ID, AccountID: &accountID, NotificationType: "assigned_conversation_new_message", PrimaryActorType: "Conversation", PrimaryActorID: s.testConv.ID}
s.Require().NoError(s.db.Create(notification).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/update_last_seen", s.testAccount.ID, displayID), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Empty(s.T(), w.Body.String())
var conversation model.Conversation
s.Require().NoError(s.db.First(&conversation, s.testConv.ID).Error)
s.Require().NotNil(conversation.AgentLastSeenAt)
assert.Greater(s.T(), *conversation.AgentLastSeenAt, oldSeen)
var updatedNotification model.Notification
s.Require().NoError(s.db.First(&updatedNotification, notification.ID).Error)
assert.NotNil(s.T(), updatedNotification.ReadAt)
}
func (s *ConversationHandlerTestSuite) TestUpdateLastSeen_InvalidAccountID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations/1/update_last_seen", nil)