feat(conversations): finish message mutation parity

This commit is contained in:
2026-06-05 02:10:42 +08:00
parent 5c44ca0c50
commit a465bbe09d
10 changed files with 420 additions and 42 deletions
+129 -5
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
@@ -80,6 +81,7 @@ func (s *MessageHandlerTestSuite) SetupSuite() {
&model.Conversation{},
&model.ConversationParticipant{},
&model.Message{},
&model.Attachment{},
&model.InboxMember{},
)
s.Require().NoError(err)
@@ -185,6 +187,7 @@ func (s *MessageHandlerTestSuite) SetupTest() {
}
func (s *MessageHandlerTestSuite) TearDownTest() {
s.db.Exec("DELETE FROM attachments")
s.db.Exec("DELETE FROM messages")
s.db.Exec("DELETE FROM conversation_participants")
s.db.Exec("DELETE FROM conversations")
@@ -253,6 +256,47 @@ func (s *MessageHandlerTestSuite) TestList_Empty() {
assert.Equal(s.T(), 0, len(data))
}
func (s *MessageHandlerTestSuite) TestList_BeforeAfterMessageFinder() {
s.db.Exec("DELETE FROM messages")
var ids []uint
for i := 0; i < 5; i++ {
msg := &model.Message{
ConversationID: s.testConv.ID,
AccountID: s.testAccount.ID,
InboxID: s.testInbox.ID,
Content: fmt.Sprintf("message-%d", i+1),
ContentType: "text",
MessageType: "incoming",
SenderType: "contact",
}
s.Require().NoError(s.db.Create(msg).Error)
ids = append(ids, msg.ID)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", msgListURL(s.testAccount.ID, s.testConv.ID)+fmt.Sprintf("?before=%d", ids[3]), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var beforeResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &beforeResp)
beforePayload := beforeResp["payload"].([]interface{})
assert.Len(s.T(), beforePayload, 3)
assert.Equal(s.T(), float64(ids[0]), beforePayload[0].(map[string]interface{})["id"])
assert.Equal(s.T(), float64(ids[2]), beforePayload[2].(map[string]interface{})["id"])
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", msgListURL(s.testAccount.ID, s.testConv.ID)+fmt.Sprintf("?after=%d", ids[2]), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var afterResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &afterResp)
afterPayload := afterResp["payload"].([]interface{})
assert.Len(s.T(), afterPayload, 2)
assert.Equal(s.T(), float64(ids[3]), afterPayload[0].(map[string]interface{})["id"])
}
func (s *MessageHandlerTestSuite) TestList_InvalidConversationID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/abc/messages/", nil)
@@ -313,6 +357,37 @@ func (s *MessageHandlerTestSuite) TestCreate_ChatwootFrontendPayloadDefaultsOutg
assert.NotNil(s.T(), resp["content_attributes"])
}
func (s *MessageHandlerTestSuite) TestCreate_MultipartAttachmentPersistsAndSerializes() {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
s.Require().NoError(writer.WriteField("content", "Attachment message"))
s.Require().NoError(writer.WriteField("private", "false"))
file, err := writer.CreateFormFile("attachments[]", "hello.txt")
s.Require().NoError(err)
_, err = file.Write([]byte("hello world"))
s.Require().NoError(err)
s.Require().NoError(writer.Close())
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", msgListURL(s.testAccount.ID, s.testConv.ID), &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
attachments, ok := resp["attachments"].([]interface{})
assert.True(s.T(), ok)
assert.Len(s.T(), attachments, 1)
attachment := attachments[0].(map[string]interface{})
assert.Equal(s.T(), "file", attachment["file_type"])
assert.Contains(s.T(), attachment["data_url"], "hello.txt")
var count int64
s.Require().NoError(s.db.Model(&model.Attachment{}).Where("message_id = ?", uint(resp["id"].(float64))).Count(&count).Error)
assert.Equal(s.T(), int64(1), count)
}
func (s *MessageHandlerTestSuite) TestCreate_MissingContent() {
payload := map[string]interface{}{
"content_type": "text",
@@ -355,10 +430,8 @@ func (s *MessageHandlerTestSuite) TestGet_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(), float64(s.testMessage.ID), data["id"])
assert.Equal(s.T(), "Hello world", data["content"])
assert.Equal(s.T(), float64(s.testMessage.ID), resp["id"])
assert.Equal(s.T(), "Hello world", resp["content"])
}
func (s *MessageHandlerTestSuite) TestGet_NotFound() {
@@ -398,6 +471,43 @@ func (s *MessageHandlerTestSuite) TestUpdate_Success() {
assert.Equal(s.T(), "Updated content", resp["content"])
}
func (s *MessageHandlerTestSuite) TestUpdate_StatusExternalError() {
s.Require().NoError(s.db.Model(s.testInbox).Update("channel_type", "api").Error)
s.testInbox.ChannelType = "api"
payload := map[string]interface{}{
"status": "failed",
"external_error": "provider rejected message",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
url := msgDetailURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
req, _ := http.NewRequest("PATCH", 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 resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(s.T(), "failed", resp["status"])
attrs := resp["content_attributes"].(map[string]interface{})
assert.Equal(s.T(), "provider rejected message", attrs["external_error"])
}
func (s *MessageHandlerTestSuite) TestUpdate_StatusForbiddenForNonAPIInbox() {
payload := map[string]interface{}{"status": "delivered"}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
url := msgDetailURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
req, _ := http.NewRequest("PATCH", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusForbidden, w.Code)
}
func (s *MessageHandlerTestSuite) TestUpdate_NotFound() {
payload := map[string]interface{}{
"content": "Updated content",
@@ -416,12 +526,26 @@ func (s *MessageHandlerTestSuite) TestUpdate_NotFound() {
// --- Delete Tests ---
func (s *MessageHandlerTestSuite) TestDelete_Success() {
attachment := &model.Attachment{MessageID: s.testMessage.ID, AccountID: s.testAccount.ID, FileType: "file", FileName: "delete.txt"}
s.Require().NoError(s.db.Create(attachment).Error)
w := httptest.NewRecorder()
url := msgDetailURL(s.testAccount.ID, s.testConv.ID, s.testMessage.ID)
req, _ := http.NewRequest("DELETE", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusNoContent, w.Code)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(s.T(), "This message was deleted", resp["content"])
attrs := resp["content_attributes"].(map[string]interface{})
assert.Equal(s.T(), true, attrs["deleted"])
_, hasAttachments := resp["attachments"]
assert.False(s.T(), hasAttachments)
var count int64
s.Require().NoError(s.db.Model(&model.Attachment{}).Where("message_id = ?", s.testMessage.ID).Count(&count).Error)
assert.Equal(s.T(), int64(0), count)
}
func (s *MessageHandlerTestSuite) TestDelete_NotFound() {