Publish current project state
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
@@ -100,7 +101,7 @@ func RegisterAuditRoutes(rg *gin.RouterGroup, h *AuditHandler) {
|
||||
|
||||
func isAuditAdmin(c *gin.Context) bool {
|
||||
role := getRole(c)
|
||||
return role == "administrator" || role == "super_admin"
|
||||
return role == auth.RoleAdministrator || role == auth.RoleSuperAdmin
|
||||
}
|
||||
|
||||
func parseAuditPage(raw string) int {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
@@ -247,7 +248,7 @@ func (h *CaptainCustomToolHandler) ensureCustomToolsEnabled(c *gin.Context, acco
|
||||
|
||||
func (h *CaptainCustomToolHandler) ensureCustomToolAdmin(c *gin.Context) bool {
|
||||
if role, exists := c.Get("role"); exists {
|
||||
if role == "administrator" || role == "super_admin" {
|
||||
if role == auth.RoleAdministrator || role == auth.RoleSuperAdmin {
|
||||
return true
|
||||
}
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to do this action"})
|
||||
@@ -293,5 +294,5 @@ func captainCustomToolPayload(c *gin.Context, tool *model.CaptainCustomTool) gin
|
||||
|
||||
func captainCustomToolShowAuthConfig(c *gin.Context) bool {
|
||||
role, exists := c.Get("role")
|
||||
return exists && (role == "administrator" || role == "super_admin")
|
||||
return exists && (role == auth.RoleAdministrator || role == auth.RoleSuperAdmin)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
@@ -112,5 +113,5 @@ func (h *CaptainPreferenceHandler) Delete(c *gin.Context) {
|
||||
|
||||
func captainPreferencesCanUpdate(c *gin.Context) bool {
|
||||
role := getRole(c)
|
||||
return role == "administrator" || role == "super_admin"
|
||||
return role == auth.RoleAdministrator || role == auth.RoleSuperAdmin
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/llm"
|
||||
"github.com/gochat/gochat/internal/middleware"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
@@ -470,7 +471,7 @@ func (h *ConversationHandler) UpdateLabels(c *gin.Context) {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": gin.H{"conversationId": strconv.FormatUint(uint64(conversation.ID), 10), "labels": labelList(conversation.Labels)}})
|
||||
c.JSON(http.StatusOK, gin.H{"payload": labelList(conversation.Labels)})
|
||||
}
|
||||
|
||||
// GetLabels returns the labels assigned to a conversation.
|
||||
@@ -1145,7 +1146,7 @@ func (h *ConversationHandler) canManageConversationInbox(ctx context.Context, ac
|
||||
if err := h.conversationSvc.DB().WithContext(ctx).Where("account_id = ? AND user_id = ?", accountID, userID).First(&accountUser).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
if accountUser.Role == "administrator" {
|
||||
if accountUser.Role == auth.RoleAdministrator {
|
||||
return true
|
||||
}
|
||||
var count int64
|
||||
|
||||
@@ -1080,9 +1080,7 @@ func (s *ConversationCrudTestSuite) TestUpdateLabels_Success() {
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
payload := resp["payload"].(map[string]interface{})
|
||||
assert.Equal(s.T(), strconv.FormatUint(uint64(s.testConv.ID), 10), payload["conversationId"])
|
||||
assert.ElementsMatch(s.T(), []interface{}{"support", "bug"}, payload["labels"])
|
||||
assert.ElementsMatch(s.T(), []interface{}{"support", "bug"}, resp["payload"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestChatwootFrontendConversationLabelsRuntimeRoutes() {
|
||||
@@ -1097,9 +1095,7 @@ func (s *ConversationCrudTestSuite) TestChatwootFrontendConversationLabelsRuntim
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
|
||||
var updateResp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &updateResp))
|
||||
updatePayload := updateResp["payload"].(map[string]interface{})
|
||||
assert.Equal(s.T(), strconv.FormatUint(uint64(s.testConv.ID), 10), updatePayload["conversationId"])
|
||||
assert.ElementsMatch(s.T(), []interface{}{"customer-success", "on-hold"}, updatePayload["labels"])
|
||||
assert.ElementsMatch(s.T(), []interface{}{"customer-success", "on-hold"}, updateResp["payload"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", s.convURL(s.testConv.ID)+"/labels", nil)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
@@ -721,7 +722,7 @@ func serializeUser(user *model.User, accountID uint) map[string]any {
|
||||
"provider": nonEmpty(user.Provider, "email"),
|
||||
"available_name": nonEmpty(user.DisplayName, user.Name),
|
||||
"name": user.Name,
|
||||
"role": nonEmpty(user.Role, "agent"),
|
||||
"role": nonEmpty(user.Role, auth.RoleAgent),
|
||||
"thumbnail": user.AvatarURL,
|
||||
"type": "user",
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -93,10 +95,39 @@ func serializeCompanyContact(ctx context.Context, db *gorm.DB, contact *model.Co
|
||||
}
|
||||
|
||||
func serializeContactInbox(contactInbox *model.ContactInbox) map[string]any {
|
||||
return map[string]any{
|
||||
payload := map[string]any{
|
||||
"source_id": contactInbox.SourceID,
|
||||
"inbox": serializeInboxSlim(&contactInbox.Inbox),
|
||||
}
|
||||
if contactInbox.Inbox.ChannelType == "shangwutong" {
|
||||
if status := shangwutongContactNameStatus(contactInbox.ChannelMetadata); status != nil {
|
||||
status["contact_inbox_id"] = contactInbox.ID
|
||||
status["source_id"] = contactInbox.SourceID
|
||||
payload["shangwutong_contact_name_operation"] = status
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func shangwutongContactNameStatus(metadata datatypes.JSON) map[string]any {
|
||||
var values map[string]any
|
||||
if len(metadata) == 0 || json.Unmarshal(metadata, &values) != nil {
|
||||
return nil
|
||||
}
|
||||
state, ok := values["swt_contact_name_operation"].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
status := map[string]any{}
|
||||
for _, key := range []string{"event_id", "operation", "status", "error_code", "error_message", "updated_at"} {
|
||||
if value, exists := state[key]; exists {
|
||||
status[key] = value
|
||||
}
|
||||
}
|
||||
if len(status) == 0 {
|
||||
return nil
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func serializeContactInboxShell(ctx context.Context, db *gorm.DB, contactInbox *model.ContactInbox) map[string]any {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/internal/ws"
|
||||
@@ -234,7 +235,7 @@ func RegisterCustomRoleRoutes(rg *gin.RouterGroup, h *CustomRoleHandler) {
|
||||
|
||||
func isCustomRoleAdmin(c *gin.Context) bool {
|
||||
role := getRole(c)
|
||||
return role == "administrator" || role == "super_admin"
|
||||
return role == auth.RoleAdministrator || role == auth.RoleSuperAdmin
|
||||
}
|
||||
|
||||
func serializeCustomRoles(roles []model.CustomRole) []gin.H {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
@@ -67,7 +68,7 @@ func (h *InboxHandler) List(c *gin.Context) {
|
||||
inboxes []model.Inbox
|
||||
svcErr error
|
||||
)
|
||||
if getRole(c) == "administrator" {
|
||||
if getRole(c) == auth.RoleAdministrator {
|
||||
inboxes, _, svcErr = h.svc.ListByAccount(c.Request.Context(), accountID, offset, perPage)
|
||||
} else {
|
||||
userID := getUserID(c)
|
||||
@@ -995,5 +996,5 @@ func (h *InboxHandler) ResetSecret(c *gin.Context) {
|
||||
}
|
||||
|
||||
func inboxSerializationAdmin(c *gin.Context) bool {
|
||||
return getRole(c) == "administrator"
|
||||
return getRole(c) == auth.RoleAdministrator
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
)
|
||||
@@ -263,7 +264,7 @@ func serializeInboxMemberAgent(member model.InboxMember) gin.H {
|
||||
role = member.Role
|
||||
}
|
||||
if role == "" {
|
||||
role = "agent"
|
||||
role = auth.RoleAgent
|
||||
}
|
||||
return gin.H{
|
||||
"id": user.ID,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/automation"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
@@ -100,7 +101,7 @@ func (h *MacroHandler) Create(c *gin.Context) {
|
||||
// set_visibility: Chatwoot forces visibility=personal if user role is agent
|
||||
// Reference: Chatwoot macro.rb set_visibility — self.visibility = :personal if user.agent?
|
||||
role := getRole(c)
|
||||
if role == "agent" {
|
||||
if role == auth.RoleAgent {
|
||||
macro.Visibility = automation.MacroVisibilityPersonal
|
||||
}
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@ import (
|
||||
"github.com/gochat/gochat/internal/config"
|
||||
"github.com/gochat/gochat/internal/middleware"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
|
||||
// OIDCHandler handles OIDC/OAuth2 enterprise authentication HTTP endpoints.
|
||||
type OIDCHandler struct {
|
||||
oidcService *auth.OIDCService
|
||||
ssoMiddleware *auth.SSOMiddleware
|
||||
jwtService *auth.JWTService
|
||||
refreshStore *auth.RefreshTokenStore
|
||||
oidcCfg *config.OIDCConfig
|
||||
oidcService *auth.OIDCService
|
||||
ssoMiddleware *auth.SSOMiddleware
|
||||
jwtService *auth.JWTService
|
||||
refreshStore *auth.RefreshTokenStore
|
||||
oidcCfg *config.OIDCConfig
|
||||
}
|
||||
|
||||
// NewOIDCHandler creates an OIDC handler with service dependencies.
|
||||
@@ -240,12 +240,12 @@ func (h *OIDCHandler) Callback(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.APIResponse{
|
||||
Success: true,
|
||||
Data: gin.H{
|
||||
"access_token": accessToken,
|
||||
"user_id": result.UserID,
|
||||
"account_id": result.AccountID,
|
||||
"role": result.Role,
|
||||
"provider": string(result.Provider),
|
||||
"session_id": sessionID,
|
||||
"access_token": accessToken,
|
||||
"user_id": result.UserID,
|
||||
"account_id": result.AccountID,
|
||||
"role": result.Role,
|
||||
"provider": string(result.Provider),
|
||||
"session_id": sessionID,
|
||||
"auto_provisioned": result.AutoProvision,
|
||||
},
|
||||
})
|
||||
@@ -531,10 +531,10 @@ func RegisterOIDCRoutes(rg *gin.RouterGroup, handler *OIDCHandler, authMiddlewar
|
||||
|
||||
// Admin-only config management routes (require auth + administrator role)
|
||||
configGroup := oidcGroup.Group("/config")
|
||||
configGroup.Use(authMiddleware, middleware.RoleCheck("administrator"))
|
||||
configGroup.Use(authMiddleware, middleware.RoleCheck(auth.RoleAdministrator))
|
||||
{
|
||||
configGroup.GET("", handler.GetConfig)
|
||||
configGroup.PUT("", handler.UpdateConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/search"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
@@ -424,7 +425,7 @@ func serializeSearchConversationAgentModel(conv *model.Conversation) map[string]
|
||||
"available_name": nonEmpty(conv.Assignee.DisplayName, conv.Assignee.Name),
|
||||
"email": conv.Assignee.Email,
|
||||
"name": conv.Assignee.Name,
|
||||
"role": nonEmpty(conv.Assignee.Role, "agent"),
|
||||
"role": nonEmpty(conv.Assignee.Role, auth.RoleAgent),
|
||||
}
|
||||
}
|
||||
return map[string]any{}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -181,6 +181,140 @@ func TestShangwutongClassificationUpdateAcceptsConversationDisplayID(t *testing.
|
||||
require.Contains(t, string(job.Payload), fmt.Sprintf(`"conversation_id":%d`, conversation.ID))
|
||||
}
|
||||
|
||||
func TestShangwutongConnectorContactOperationStatusIsIdempotentAndStaleSafe(t *testing.T) {
|
||||
router, db, token, inbox, _ := setupShangwutongConnectorAPI(t)
|
||||
contact := &model.Contact{AccountID: inbox.AccountID, Name: "Visitor"}
|
||||
require.NoError(t, db.Create(contact).Error)
|
||||
ci := &model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, SourceID: "visitor", ChannelMetadata: datatypes.JSON([]byte(`{"cid":"cid-1","swt_contact_name_operation":{"event_id":"contact-event-1","operation":"change_contact_name","status":"pending","account_id":1,"inbox_id":1,"contact_inbox_id":1,"source_id":"visitor","contact_id":1,"name":"Renamed"}}`))}
|
||||
require.NoError(t, db.Create(ci).Error)
|
||||
path := fmt.Sprintf("/api/v1/connector/shangwutong/inboxes/%d/contacts/visitor/status", inbox.ID)
|
||||
body := map[string]any{"event_id": "contact-event-1", "operation": "change_contact_name", "contact_id": contact.ID, "name": "Renamed", "status": "succeeded"}
|
||||
request := func(payload map[string]any) *httptest.ResponseRecorder {
|
||||
encoded, err := json.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPut, path, bytes.NewReader(encoded))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
eventID, _ := payload["event_id"].(string)
|
||||
req.Header.Set("Idempotency-Key", fmt.Sprintf("swt-contact-operation:%d:%s", inbox.ID, eventID))
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, req)
|
||||
return response
|
||||
}
|
||||
updated := request(body)
|
||||
require.Equal(t, http.StatusOK, updated.Code, updated.Body.String())
|
||||
replayed := request(body)
|
||||
require.Equal(t, http.StatusOK, replayed.Code, replayed.Body.String())
|
||||
require.Contains(t, replayed.Body.String(), `"updated":false`)
|
||||
body["status"] = "failed"
|
||||
body["error_code"] = "remote_rejected"
|
||||
conflict := request(body)
|
||||
require.Equal(t, http.StatusConflict, conflict.Code, conflict.Body.String())
|
||||
body["event_id"] = "old-event"
|
||||
body["status"] = "failed"
|
||||
stale := request(body)
|
||||
require.Equal(t, http.StatusOK, stale.Code, stale.Body.String())
|
||||
require.Contains(t, stale.Body.String(), `"stale":true`)
|
||||
|
||||
require.NoError(t, db.First(ci, ci.ID).Error)
|
||||
var metadata map[string]any
|
||||
require.NoError(t, json.Unmarshal(ci.ChannelMetadata, &metadata))
|
||||
state := metadata["swt_contact_name_operation"].(map[string]any)
|
||||
require.Equal(t, "succeeded", state["status"])
|
||||
}
|
||||
|
||||
func TestShangwutongClassificationCallbackIsScopedAndStaleSafe(t *testing.T) {
|
||||
router, db, token, inbox, _ := setupShangwutongConnectorAPI(t)
|
||||
contact := &model.Contact{AccountID: inbox.AccountID, Name: "Visitor"}
|
||||
require.NoError(t, db.Create(contact).Error)
|
||||
contactInbox := &model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, SourceID: "visitor", ChannelMetadata: datatypes.JSON([]byte(`{"cid":"cid-1"}`))}
|
||||
otherContactInbox := &model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, SourceID: "visitor-other", ChannelMetadata: datatypes.JSON([]byte(`{"cid":"cid-2"}`))}
|
||||
sameCIDContact := &model.Contact{AccountID: inbox.AccountID, Name: "Merged visitor"}
|
||||
require.NoError(t, db.Create(contactInbox).Error)
|
||||
require.NoError(t, db.Create(otherContactInbox).Error)
|
||||
require.NoError(t, db.Create(sameCIDContact).Error)
|
||||
sameCIDContactInbox := &model.ContactInbox{ContactID: sameCIDContact.ID, InboxID: inbox.ID, SourceID: "visitor-same-cid", ChannelMetadata: datatypes.JSON([]byte(`{"cid":"cid-1"}`))}
|
||||
require.NoError(t, db.Create(sameCIDContactInbox).Error)
|
||||
conversation := &model.Conversation{AccountID: inbox.AccountID, InboxID: inbox.ID, ContactID: contact.ID, ContactInboxID: &contactInbox.ID, Status: "open"}
|
||||
peer := &model.Conversation{AccountID: inbox.AccountID, InboxID: inbox.ID, ContactID: contact.ID, ContactInboxID: &contactInbox.ID, Status: "open", AdditionalAttributes: datatypes.JSON([]byte(`{"keep":"yes"}`))}
|
||||
otherConversation := &model.Conversation{AccountID: inbox.AccountID, InboxID: inbox.ID, ContactID: contact.ID, ContactInboxID: &otherContactInbox.ID, Status: "open"}
|
||||
sameCIDConversation := &model.Conversation{AccountID: inbox.AccountID, InboxID: inbox.ID, ContactID: sameCIDContact.ID, ContactInboxID: &sameCIDContactInbox.ID, Status: "open"}
|
||||
require.NoError(t, db.Create(conversation).Error)
|
||||
require.NoError(t, db.Create(peer).Error)
|
||||
require.NoError(t, db.Create(otherConversation).Error)
|
||||
require.NoError(t, db.Create(sameCIDConversation).Error)
|
||||
require.NoError(t, db.Create(&model.ShangwutongClassificationCache{
|
||||
InboxID: inbox.ID, ConversationKinds: datatypes.JSON([]byte(`[]`)), CustomerColorKinds: datatypes.JSON([]byte(`[{"id":"color-1","name":"VIP"},{"id":"color-2","name":"New"}]`)), SyncStatus: "succeeded",
|
||||
}).Error)
|
||||
changePath := fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/shangwutong-classifications", inbox.AccountID, conversation.ID)
|
||||
accepted := connectorRequest(t, router, "", http.MethodPatch, changePath, map[string]string{"customer_color_id": "color-1"})
|
||||
require.Equal(t, http.StatusAccepted, accepted.Code, accepted.Body.String())
|
||||
var queued struct {
|
||||
SyncID string `json:"sync_id"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(accepted.Body.Bytes(), &queued))
|
||||
require.NotEmpty(t, queued.SyncID)
|
||||
callbackPath := fmt.Sprintf("/api/v1/connector/shangwutong/inboxes/%d/conversations/%d/classifications/status", inbox.ID, conversation.ID)
|
||||
callback := func(eventID string, body map[string]any) *httptest.ResponseRecorder {
|
||||
encoded, err := json.Marshal(body)
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPut, callbackPath, bytes.NewReader(encoded))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Idempotency-Key", fmt.Sprintf("swt-classification-operation:%d:%s", inbox.ID, eventID))
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, req)
|
||||
return response
|
||||
}
|
||||
result := map[string]any{"event_id": queued.SyncID, "operation": "set_customer_color", "status": "succeeded", "customer_color_id": "color-1"}
|
||||
response := callback(queued.SyncID, result)
|
||||
require.Equal(t, http.StatusOK, response.Code, response.Body.String())
|
||||
replayed := callback(queued.SyncID, result)
|
||||
require.Equal(t, http.StatusOK, replayed.Code, replayed.Body.String())
|
||||
require.Contains(t, replayed.Body.String(), `"updated":false`)
|
||||
conflicting := map[string]any{"event_id": queued.SyncID, "operation": "set_customer_color", "status": "succeeded", "customer_color_id": "color-2"}
|
||||
conflict := callback(queued.SyncID, conflicting)
|
||||
require.Equal(t, http.StatusConflict, conflict.Code, conflict.Body.String())
|
||||
require.Contains(t, conflict.Body.String(), `"code":"idempotency_conflict"`)
|
||||
var updatedConversation, updatedPeer, updatedOther, updatedSameCID model.Conversation
|
||||
require.NoError(t, db.First(&updatedConversation, conversation.ID).Error)
|
||||
require.NoError(t, db.First(&updatedPeer, peer.ID).Error)
|
||||
require.NoError(t, db.First(&updatedOther, otherConversation.ID).Error)
|
||||
require.NoError(t, db.First(&updatedSameCID, sameCIDConversation.ID).Error)
|
||||
var conversationAttributes, peerAttributes, sameCIDAttributes map[string]any
|
||||
require.NoError(t, json.Unmarshal(updatedConversation.AdditionalAttributes, &conversationAttributes))
|
||||
require.NoError(t, json.Unmarshal(updatedPeer.AdditionalAttributes, &peerAttributes))
|
||||
require.NoError(t, json.Unmarshal(updatedSameCID.AdditionalAttributes, &sameCIDAttributes))
|
||||
require.Equal(t, "color-1", conversationAttributes["swt_label_color"])
|
||||
require.Equal(t, "color-1", peerAttributes["swt_label_color"])
|
||||
require.Equal(t, "color-1", sameCIDAttributes["swt_label_color"])
|
||||
require.Equal(t, "yes", peerAttributes["keep"])
|
||||
require.NotContains(t, string(updatedOther.AdditionalAttributes), "swt_label_color")
|
||||
|
||||
accepted = connectorRequest(t, router, "", http.MethodPatch, changePath, map[string]string{"customer_color_id": "color-2"})
|
||||
require.Equal(t, http.StatusAccepted, accepted.Code, accepted.Body.String())
|
||||
var next struct {
|
||||
SyncID string `json:"sync_id"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(accepted.Body.Bytes(), &next))
|
||||
stale := callback(queued.SyncID, result)
|
||||
require.Equal(t, http.StatusOK, stale.Code, stale.Body.String())
|
||||
require.Contains(t, stale.Body.String(), `"stale":true`)
|
||||
|
||||
var pendingConversation model.Conversation
|
||||
require.NoError(t, db.First(&pendingConversation, conversation.ID).Error)
|
||||
var pendingAttributes map[string]any
|
||||
require.NoError(t, json.Unmarshal(pendingConversation.AdditionalAttributes, &pendingAttributes))
|
||||
pendingState := pendingAttributes["swt_classification_operations"].(map[string]any)["set_customer_color"].(map[string]any)
|
||||
pendingState["status"], pendingState["error_code"], pendingState["error_message"] = "uncertain", "connector_delivery_uncertain", "delivery acknowledgement was not received"
|
||||
pendingEncoded, err := json.Marshal(pendingAttributes)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Model(&pendingConversation).Update("additional_attributes", datatypes.JSON(pendingEncoded)).Error)
|
||||
resolved := callback(next.SyncID, map[string]any{"event_id": next.SyncID, "operation": "set_customer_color", "status": "succeeded", "customer_color_id": "color-2"})
|
||||
require.Equal(t, http.StatusOK, resolved.Code, resolved.Body.String())
|
||||
require.Contains(t, resolved.Body.String(), `"updated":true`)
|
||||
}
|
||||
|
||||
func uintPointer(value uint) *uint { return &value }
|
||||
|
||||
func setupShangwutongConnectorAPI(t *testing.T) (*gin.Engine, *gorm.DB, string, *model.Inbox, *model.Inbox) {
|
||||
@@ -235,6 +369,8 @@ func setupShangwutongConnectorAPI(t *testing.T) (*gin.Engine, *gorm.DB, string,
|
||||
group.PUT("/inboxes/:inbox_id/status", handler.UpdateInboxStatus)
|
||||
group.PUT("/inboxes/:inbox_id/messages/:message_id/status", handler.UpdateMessageStatus)
|
||||
group.PATCH("/inboxes/:inbox_id/contacts/:source_id", handler.UpdateContactMetadata)
|
||||
group.PUT("/inboxes/:inbox_id/contacts/:source_id/status", handler.UpdateContactOperationStatus)
|
||||
group.PUT("/inboxes/:inbox_id/conversations/:conversation_id/classifications/status", handler.UpdateClassificationStatus)
|
||||
router.PATCH("/api/v1/accounts/:account_id/conversations/:conversation_id/shangwutong-classifications", handler.UpdateConversationClassification)
|
||||
return router, db, token, inboxes[0], inboxes[1]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/auth"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
@@ -37,7 +38,7 @@ func (h *WhatsAppCallHandler) Index(c *gin.Context) {
|
||||
inboxID, _ := strconv.ParseUint(c.Query("inbox_id"), 10, 64)
|
||||
agentID, _ := strconv.ParseUint(c.Query("agent_id"), 10, 64)
|
||||
role := c.GetString("role")
|
||||
filter := service.AccountCallListFilter{Page: page, UserID: getUserID(c), CustomRoleID: getCustomRoleID(c), AccountWide: role == "administrator" || role == "super_admin", Status: c.Query("status"), Direction: c.Query("direction"), InboxID: uint(inboxID), AgentID: uint(agentID)}
|
||||
filter := service.AccountCallListFilter{Page: page, UserID: getUserID(c), CustomRoleID: getCustomRoleID(c), AccountWide: role == auth.RoleAdministrator || role == auth.RoleSuperAdmin, Status: c.Query("status"), Direction: c.Query("direction"), InboxID: uint(inboxID), AgentID: uint(agentID)}
|
||||
if c.Query("since") != "" && c.Query("until") != "" {
|
||||
since, errSince := parseChatwootReportTime(c.Query("since"))
|
||||
until, errUntil := parseChatwootReportTime(c.Query("until"))
|
||||
|
||||
Reference in New Issue
Block a user