This commit is contained in:
2026-06-10 07:24:13 +08:00
parent ac7f351308
commit 2bb54b21a5
47 changed files with 2912 additions and 305 deletions
+78 -20
View File
@@ -31,9 +31,11 @@ import (
type mockMsgHandlerLLMProvider struct {
chatResponse *llm.ChatResponse
chatError error
chatCalls int
}
func (m *mockMsgHandlerLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) {
m.chatCalls++
if m.chatError != nil {
return nil, m.chatError
}
@@ -152,8 +154,10 @@ func (s *MessageHandlerTestSuite) SetupTest() {
s.Require().NoError(s.db.Create(contact).Error)
s.testContact = contact
displayID := uint(4242)
conv := &model.Conversation{
AccountID: account.ID,
DisplayID: &displayID,
InboxID: inbox.ID,
ContactID: contact.ID,
Status: string(model.ConversationStatusOpen),
@@ -185,6 +189,7 @@ func (s *MessageHandlerTestSuite) SetupTest() {
},
}
s.mockLLM.chatError = nil
s.mockLLM.chatCalls = 0
}
func (s *MessageHandlerTestSuite) TearDownTest() {
@@ -329,7 +334,38 @@ func (s *MessageHandlerTestSuite) TestCreate_Success() {
assert.NotNil(s.T(), resp["id"])
assert.Equal(s.T(), "New message", resp["content"])
assert.Equal(s.T(), float64(1), resp["message_type"])
assert.Equal(s.T(), float64(s.testConv.ID), resp["conversation_id"])
assert.Equal(s.T(), float64(*s.testConv.DisplayID), resp["conversation_id"])
}
func (s *MessageHandlerTestSuite) TestCreate_UsesRouteConversationOverBodyConversationID() {
otherConv := &model.Conversation{
AccountID: s.testAccount.ID,
InboxID: s.testInbox.ID,
ContactID: s.testContact.ID,
Status: string(model.ConversationStatusOpen),
Priority: string(model.ConversationPriorityMedium),
ChannelType: "web_widget",
Channel: "web_widget",
}
s.Require().NoError(s.db.Create(otherConv).Error)
payload := map[string]interface{}{
"conversation_id": otherConv.ID,
"content": "Route scoped message",
"message_type": "outgoing",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
url := msgListURL(s.testAccount.ID, *s.testConv.DisplayID)
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var created model.Message
s.Require().NoError(s.db.Where("content = ?", "Route scoped message").First(&created).Error)
assert.Equal(s.T(), s.testConv.ID, created.ConversationID)
}
func (s *MessageHandlerTestSuite) TestCreate_ChatwootFrontendPayloadDefaultsOutgoing() {
@@ -424,7 +460,7 @@ func (s *MessageHandlerTestSuite) TestCreate_InvalidConversationID() {
func (s *MessageHandlerTestSuite) TestGet_Success() {
w := httptest.NewRecorder()
url := msgDetailURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
url := msgDetailURL(s.testAccount.ID, *s.testConv.DisplayID, s.testMessage.ID)
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
@@ -433,6 +469,7 @@ func (s *MessageHandlerTestSuite) TestGet_Success() {
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(s.T(), float64(s.testMessage.ID), resp["id"])
assert.Equal(s.T(), "Hello world", resp["content"])
assert.Equal(s.T(), float64(*s.testConv.DisplayID), resp["conversation_id"])
}
func (s *MessageHandlerTestSuite) TestGet_NotFound() {
@@ -455,13 +492,16 @@ func (s *MessageHandlerTestSuite) TestGet_InvalidID() {
// --- Update Tests ---
func (s *MessageHandlerTestSuite) TestUpdate_Success() {
s.Require().NoError(s.db.Model(s.testInbox).Update("channel_type", "api").Error)
s.testInbox.ChannelType = "api"
payload := map[string]interface{}{
"content": "Updated content",
"status": "delivered",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
url := msgDetailURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
url := msgDetailURL(s.testAccount.ID, *s.testConv.DisplayID, s.testMessage.ID)
req, _ := http.NewRequest("PATCH", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
@@ -469,7 +509,8 @@ func (s *MessageHandlerTestSuite) TestUpdate_Success() {
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(s.T(), "Updated content", resp["content"])
assert.Equal(s.T(), "Hello world", resp["content"])
assert.Equal(s.T(), "delivered", resp["status"])
}
func (s *MessageHandlerTestSuite) TestUpdate_StatusExternalError() {
@@ -567,7 +608,7 @@ func (s *MessageHandlerTestSuite) TestRetry_Success() {
}).Error)
w := httptest.NewRecorder()
url := msgRetryURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
url := msgRetryURL(s.testAccount.ID, *s.testConv.DisplayID, s.testMessage.ID)
req, _ := http.NewRequest("POST", url, nil)
s.router.ServeHTTP(w, req)
@@ -602,8 +643,8 @@ func (s *MessageHandlerTestSuite) TestRetry_NotFound() {
req, _ := http.NewRequest("POST", url, nil)
s.router.ServeHTTP(w, req)
// Service returns "not found" error → handleServiceError → 404
assert.Equal(s.T(), http.StatusNotFound, w.Code)
// Chatwoot retry rescues lookup/status-update failures as unprocessable entity.
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
}
func (s *MessageHandlerTestSuite) TestRetry_AccountMismatch() {
@@ -617,8 +658,7 @@ func (s *MessageHandlerTestSuite) TestRetry_AccountMismatch() {
req, _ := http.NewRequest("POST", url, nil)
s.router.ServeHTTP(w, req)
// The service's FindByAccountAndID won't find the message under account 2 → "not found" → 404
assert.Equal(s.T(), http.StatusNotFound, w.Code)
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
}
// --- Translate Tests ---
@@ -630,7 +670,7 @@ func (s *MessageHandlerTestSuite) TestTranslate_Success() {
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
url := msgTranslateURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
url := msgTranslateURL(s.testAccount.ID, *s.testConv.DisplayID, s.testMessage.ID)
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
@@ -638,11 +678,7 @@ func (s *MessageHandlerTestSuite) TestTranslate_Success() {
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.True(s.T(), resp["success"].(bool))
data := resp["data"].(map[string]interface{})
assert.Equal(s.T(), "Bonjour le monde", data["translated_content"])
assert.Equal(s.T(), "Hello world", data["original_content"])
assert.Equal(s.T(), "fr", data["target_language"])
assert.Equal(s.T(), "Bonjour le monde", resp["content"])
}
func (s *MessageHandlerTestSuite) TestTranslate_EmptyTargetLanguage() {
@@ -756,10 +792,32 @@ func (s *MessageHandlerTestSuite) TestTranslate_EmptyChoices() {
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.True(s.T(), resp["success"].(bool))
data := resp["data"].(map[string]interface{})
assert.Equal(s.T(), "", data["translated_content"])
assert.Equal(s.T(), "Hello world", data["original_content"])
assert.Equal(s.T(), "", resp["content"])
}
func (s *MessageHandlerTestSuite) TestTranslate_CachesSecondCall() {
payload := map[string]interface{}{
"target_language": "fr",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
url := msgTranslateURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Equal(s.T(), 1, s.mockLLM.chatCalls)
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req2.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w2, req2)
assert.Equal(s.T(), http.StatusOK, w2.Code)
assert.Equal(s.T(), 1, s.mockLLM.chatCalls)
assert.Empty(s.T(), w2.Body.String())
}
// --- DirectUpload Handler Tests (upload_handler_test.go extension) ---