* H-16: align takeover with channel AI workflow (#2) * feat(conversations): complete manual AI takeover * fix(conversations): align AI takeover flow with channel AI * fix(conversations): close takeover review gaps --------- Co-authored-by: Rogee <rogee@ipao.vip> * feat(shangwutong): sync customer names back to channel (#3) Co-authored-by: Rogee <rogee@ipao.vip> * fix(shangwutong): close contact sync review gaps (#4) Co-authored-by: Rogee <rogee@ipao.vip> * H-28: harden Shangwutong CID sync (#5) * fix(shangwutong): close contact sync review gaps * fix(shangwutong): harden CID sync boundaries --------- Co-authored-by: Rogee <rogee@ipao.vip> * fix(conversations): sync AI takeover exit in realtime (#6) Co-authored-by: Rogee <rogee@ipao.vip> * test(shangwutong): cover CID rename reliability (#7) Co-authored-by: Rogee <rogee@ipao.vip> * H-43: fix WEB Captain takeover E2E flow (#8) * test(shangwutong): cover CID rename reliability * H-43: fix WEB Captain takeover flow * H-48: preserve compatible provider model * H-49: make Captain takeover atomic * H-50: prevent duplicate widget initialization --------- Co-authored-by: Rogee <rogee@ipao.vip> * H-55: make Captain bindings atomic (#9) Co-authored-by: Rogee <rogee@ipao.vip> * H-60: harden Captain migration rollback and concurrency * chore(agent): baseline — uncommitted work from the local directory * H-335: add safe Captain skills and user deactivation * H-338: close auth and Captain review blockers * H-338: close assignment and session races * H-338: close assignment and websocket invalidation gaps * H-338: enforce assignment write invariants --------- Co-authored-by: Rogee <rogee@ipao.vip>
169 lines
6.2 KiB
Go
169 lines
6.2 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/service"
|
|
)
|
|
|
|
func setupAgentBulkRouter(handler *AgentBulkHandler) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
r.POST("/api/v1/accounts/:account_id/agents/bulk_assign", handler.BulkAssign)
|
|
r.POST("/api/v1/accounts/:account_id/agents/bulk_unassign", handler.BulkUnassign)
|
|
return r
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkAssign_BadJSON(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/agents/bulk_assign", bytes.NewReader([]byte("invalid json")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkAssign_EmptyConversationIDs(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
body := map[string]interface{}{
|
|
"conversation_ids": []uint{},
|
|
"agent_id": 10,
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/agents/bulk_assign", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkAssign_MissingAgentID(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
body := map[string]interface{}{
|
|
"conversation_ids": []uint{1, 2, 3},
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/agents/bulk_assign", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkAssign_InvalidAccountID(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
body := map[string]interface{}{
|
|
"conversation_ids": []uint{1, 2},
|
|
"agent_id": 10,
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/agents/bulk_assign", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestAgentBulkHandlerBulkAssignRejectsInactiveAgent(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}))
|
|
account := &model.Account{Name: "Account"}
|
|
agent := &model.User{Name: "Inactive", Email: "inactive@example.com", Password: "hash", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
require.NoError(t, db.Create(agent).Error)
|
|
require.NoError(t, db.Model(agent).Update("active", false).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: agent.ID, Role: "agent"}).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget)}
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
require.NoError(t, db.Create(contact).Error)
|
|
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
|
require.NoError(t, db.Create(conversation).Error)
|
|
conversationService := service.NewConversationService(
|
|
repository.NewConversationRepo(db), repository.NewMessageRepo(db), channel.NewDispatcher(), nil,
|
|
repository.NewAccountUserRepo(db), nil, nil,
|
|
)
|
|
router := setupAgentBulkRouter(NewAgentBulkHandler(conversationService))
|
|
body, err := json.Marshal(map[string]any{"conversation_ids": []uint{conversation.ID}, "agent_id": agent.ID})
|
|
require.NoError(t, err)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/"+strconv.FormatUint(uint64(account.ID), 10)+"/agents/bulk_assign", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
require.Contains(t, w.Body.String(), "not an agent or administrator")
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkUnassign_BadJSON(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/agents/bulk_unassign", bytes.NewReader([]byte("invalid json")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkUnassign_EmptyConversationIDs(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
body := map[string]interface{}{
|
|
"conversation_ids": []uint{},
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/agents/bulk_unassign", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestAgentBulkHandler_BulkUnassign_InvalidAccountID(t *testing.T) {
|
|
handler := NewAgentBulkHandler(&service.ConversationService{})
|
|
router := setupAgentBulkRouter(handler)
|
|
|
|
body := map[string]interface{}{
|
|
"conversation_ids": []uint{1, 2},
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/agents/bulk_unassign", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|