feat(conversations): align direct upload routes
This commit is contained in:
@@ -118,3 +118,56 @@ func (h *UploadHandler) AccountDirectUpload(c *gin.Context) {
|
||||
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// ConversationDirectUpload handles Chatwoot's nested conversation direct upload
|
||||
// endpoint used by the reused dashboard message composer.
|
||||
// Reference: POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads
|
||||
func (h *UploadHandler) ConversationDirectUpload(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "account_id is required")
|
||||
return
|
||||
}
|
||||
conversationID, err := parseUintParam(c, "conversation_id")
|
||||
if err != nil || conversationID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(c.GetHeader("Content-Type"), "application/json") {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "invalid direct upload metadata")
|
||||
return
|
||||
}
|
||||
|
||||
var req service.ActiveStorageDirectUploadRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "invalid direct upload metadata")
|
||||
return
|
||||
}
|
||||
result, svcErr := h.svc.CreateConversationDirectUpload(c.Request.Context(), accountID, conversationID, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) CompleteConversationDirectUpload(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "account_id is required")
|
||||
return
|
||||
}
|
||||
conversationID, err := parseUintParam(c, "conversation_id")
|
||||
if err != nil || conversationID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
|
||||
return
|
||||
}
|
||||
|
||||
result, svcErr := h.svc.CompleteConversationDirectUpload(c.Request.Context(), accountID, conversationID, c.Param("upload_uuid"), c.Request.Body)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -37,6 +38,8 @@ func setupUploadHandlerRouter(h *UploadHandler) *gin.Engine {
|
||||
|
||||
// Account direct upload route
|
||||
api.POST("/direct_uploads", h.AccountDirectUpload)
|
||||
api.POST("/conversations/:conversation_id/direct_uploads", h.ConversationDirectUpload)
|
||||
api.PUT("/conversations/:conversation_id/direct_uploads/:upload_uuid", h.CompleteConversationDirectUpload)
|
||||
|
||||
// Widget direct upload route
|
||||
widget := r.Group("/widget")
|
||||
@@ -163,6 +166,84 @@ func TestUploadHandler_WidgetActiveStorageDirectUploadFlow(t *testing.T) {
|
||||
assert.Equal(t, []byte("hello image"), storedBytes)
|
||||
}
|
||||
|
||||
func TestUploadHandler_ConversationActiveStorageDirectUploadFlow(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(
|
||||
&model.Account{},
|
||||
&model.Inbox{},
|
||||
&model.Contact{},
|
||||
&model.Conversation{},
|
||||
&model.DirectUpload{},
|
||||
))
|
||||
|
||||
account := &model.Account{Name: "Conversation Upload Org", Status: "active"}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
inbox := &model.Inbox{AccountID: account.ID, Name: "Conversation Upload Inbox", ChannelType: "web_widget", Enabled: true}
|
||||
require.NoError(t, db.Create(inbox).Error)
|
||||
contact := &model.Contact{AccountID: account.ID, Name: "Composer"}
|
||||
require.NoError(t, db.Create(contact).Error)
|
||||
displayID := uint(44)
|
||||
conversation := &model.Conversation{
|
||||
AccountID: account.ID,
|
||||
InboxID: inbox.ID,
|
||||
ContactID: contact.ID,
|
||||
DisplayID: &displayID,
|
||||
Status: "open",
|
||||
ChannelType: "web_widget",
|
||||
Channel: "web_widget",
|
||||
}
|
||||
require.NoError(t, db.Create(conversation).Error)
|
||||
|
||||
uploadSvc := service.NewUploadService(repository.NewDirectUploadRepo(db), &config.Config{
|
||||
Storage: config.StorageConfig{LocalPath: tmpDir, MaxFileSize: 50 << 20},
|
||||
}).WithConversationRepo(repository.NewConversationRepo(db))
|
||||
router := setupUploadHandlerRouter(NewUploadHandler(uploadSvc))
|
||||
|
||||
metadataBody, err := json.Marshal(map[string]any{
|
||||
"blob": map[string]any{
|
||||
"filename": "agent-note.pdf",
|
||||
"byte_size": 12,
|
||||
"checksum": "pdf-checksum",
|
||||
"content_type": "application/pdf",
|
||||
"metadata": map[string]any{"identified": true},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
createPath := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) + "/conversations/44/direct_uploads"
|
||||
wCreate := httptest.NewRecorder()
|
||||
reqCreate, _ := http.NewRequest("POST", createPath, bytes.NewReader(metadataBody))
|
||||
reqCreate.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(wCreate, reqCreate)
|
||||
require.Equal(t, http.StatusOK, wCreate.Code)
|
||||
|
||||
var createResp map[string]any
|
||||
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
|
||||
signedID, ok := createResp["signed_id"].(string)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, signedID)
|
||||
assert.Equal(t, "agent-note.pdf", createResp["filename"])
|
||||
directUpload := createResp["direct_upload"].(map[string]any)
|
||||
assert.Equal(t, createPath+"/"+signedID, directUpload["url"])
|
||||
|
||||
wPut := httptest.NewRecorder()
|
||||
reqPut, _ := http.NewRequest("PUT", directUpload["url"].(string), bytes.NewReader([]byte("hello report")))
|
||||
reqPut.Header.Set("Content-Type", "application/pdf")
|
||||
router.ServeHTTP(wPut, reqPut)
|
||||
require.Equal(t, http.StatusOK, wPut.Code)
|
||||
|
||||
var upload model.DirectUpload
|
||||
require.NoError(t, db.Where("upload_uuid = ?", signedID).First(&upload).Error)
|
||||
assert.Equal(t, account.ID, upload.AccountID)
|
||||
assert.Equal(t, model.DirectUploadSourceAccount, upload.Source)
|
||||
storedBytes, err := os.ReadFile(filepath.Join(tmpDir, "account", strconv.FormatUint(uint64(account.ID), 10), signedID+".pdf"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("hello report"), storedBytes)
|
||||
}
|
||||
|
||||
func TestUploadHandler_AccountDirectUpload_NoFile(t *testing.T) {
|
||||
// Create handler with nil service — we only test validation before service call
|
||||
h := &UploadHandler{svc: nil}
|
||||
|
||||
Reference in New Issue
Block a user