969 lines
37 KiB
Go
969 lines
37 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/gochat/gochat/internal/channel"
|
||
"github.com/gochat/gochat/internal/llm"
|
||
"github.com/gochat/gochat/internal/model"
|
||
"github.com/gochat/gochat/internal/repository"
|
||
"github.com/gochat/gochat/internal/search"
|
||
"github.com/gochat/gochat/internal/worker"
|
||
)
|
||
|
||
// mockMessageLLMProvider implements llm.Provider for message service testing.
|
||
// (Separate name to avoid conflict with mockLLMProvider in captain_task_service_test.go)
|
||
type mockMessageLLMProvider struct {
|
||
chatResponse *llm.ChatResponse
|
||
chatErr error
|
||
chatCalls int
|
||
}
|
||
|
||
func (m *mockMessageLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) {
|
||
m.chatCalls++
|
||
if m.chatErr != nil {
|
||
return nil, m.chatErr
|
||
}
|
||
return m.chatResponse, nil
|
||
}
|
||
|
||
func (m *mockMessageLLMProvider) CreateEmbedding(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
|
||
return nil, fmt.Errorf("not implemented")
|
||
}
|
||
|
||
func (m *mockMessageLLMProvider) ChatCompletionStream(ctx context.Context, req llm.ChatRequest, onChunk func(llm.StreamChunk) error) error {
|
||
return fmt.Errorf("not implemented")
|
||
}
|
||
|
||
// mockRetryListener implements channel.EventListener for capturing dispatched events.
|
||
type mockRetryListener struct {
|
||
received bool
|
||
lastEventType channel.EventType
|
||
lastAccountID uint
|
||
lastInboxID uint
|
||
lastUserID uint
|
||
lastConvID uint
|
||
lastData map[string]interface{}
|
||
}
|
||
|
||
func (l *mockRetryListener) Name() string {
|
||
return "mock-retry-listener"
|
||
}
|
||
|
||
func (l *mockRetryListener) OnEvent(ctx context.Context, event *channel.ChannelEvent) error {
|
||
l.received = true
|
||
l.lastEventType = event.Type
|
||
l.lastAccountID = event.AccountID
|
||
l.lastInboxID = event.InboxID
|
||
l.lastUserID = event.UserID
|
||
l.lastConvID = event.ConversationID
|
||
l.lastData = event.Data
|
||
return nil
|
||
}
|
||
|
||
// setupMessageService creates MessageRepo + Dispatcher + mockLLM + MessageService test instances.
|
||
func setupMessageService(t *testing.T, mockLLM *mockMessageLLMProvider) (*gorm.DB, *repository.MessageRepo, *channel.Dispatcher, *MessageService) {
|
||
t.Helper()
|
||
db := setupServiceTestDB(t)
|
||
repo := repository.NewMessageRepo(db)
|
||
dispatcher := channel.NewDispatcher()
|
||
svc := NewMessageService(repo, dispatcher, mockLLM)
|
||
return db, repo, dispatcher, svc
|
||
}
|
||
|
||
// setupMessageServiceWithDefaultLLM creates a MessageService with a default mock LLM that returns "translated text".
|
||
func setupMessageServiceWithDefaultLLM(t *testing.T) (*gorm.DB, *repository.MessageRepo, *channel.Dispatcher, *MessageService) {
|
||
t.Helper()
|
||
mockLLM := &mockMessageLLMProvider{
|
||
chatResponse: &llm.ChatResponse{
|
||
Choices: []llm.ChatChoice{
|
||
{Message: llm.ChatMessage{Role: "assistant", Content: "translated text"}},
|
||
},
|
||
},
|
||
}
|
||
return setupMessageService(t, mockLLM)
|
||
}
|
||
|
||
// ========== ListByConversation 测试 ==========
|
||
|
||
func TestMessageService_ListByConversation(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
user := createTestUser(t, db, account.ID)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
// 创建多条消息
|
||
msg1 := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "消息1", MessageType: "incoming", ContentType: "text", SenderType: "contact", SenderID: &contact.ID,
|
||
}
|
||
msg2 := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "消息2", MessageType: "outgoing", ContentType: "text", SenderType: "user", SenderID: &user.ID,
|
||
}
|
||
require.NoError(t, db.Create(msg1).Error)
|
||
require.NoError(t, db.Create(msg2).Error)
|
||
|
||
// 正常路径:查询会话消息列表
|
||
messages, total, err := svc.ListByConversation(ctx, conv.ID, 0, 10)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, int64(2), total)
|
||
assert.Len(t, messages, 2)
|
||
|
||
// 正常路径:分页查询
|
||
messages2, total2, err2 := svc.ListByConversation(ctx, conv.ID, 1, 1)
|
||
assert.NoError(t, err2)
|
||
assert.Equal(t, int64(2), total2)
|
||
assert.Len(t, messages2, 1)
|
||
|
||
// 正常路径:空会话
|
||
messages3, total3, err3 := svc.ListByConversation(ctx, 9999, 0, 10)
|
||
assert.NoError(t, err3)
|
||
assert.Equal(t, int64(0), total3)
|
||
assert.Len(t, messages3, 0)
|
||
}
|
||
|
||
// ========== GetByID 测试 ==========
|
||
|
||
func TestMessageService_GetByID(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "Hello", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
// 正常路径:按ID查询
|
||
found, err := svc.GetByID(ctx, msg.ID)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "Hello", found.Content)
|
||
assert.Equal(t, "incoming", found.MessageType)
|
||
|
||
// 错误路径:不存在的ID
|
||
_, err = svc.GetByID(ctx, 9999)
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
// ========== GetByAccountAndID 测试 ==========
|
||
|
||
func TestMessageService_GetByAccountAndID(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "ScopedMsg", MessageType: "outgoing", ContentType: "text", SenderType: "user",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
// 正常路径:按account和ID查询
|
||
found, err := svc.GetByAccountAndID(ctx, account.ID, msg.ID)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "ScopedMsg", found.Content)
|
||
|
||
// 错误路径:accountID不匹配
|
||
_, err = svc.GetByAccountAndID(ctx, 9999, msg.ID)
|
||
assert.Error(t, err)
|
||
|
||
// 错误路径:ID不存在
|
||
_, err = svc.GetByAccountAndID(ctx, account.ID, 9999)
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
// ========== GetByConversationAndID 测试 ==========
|
||
|
||
func TestMessageService_GetByConversationAndID(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "ConvScopedMsg", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
// 正常路径:按conversation和ID查询
|
||
found, err := svc.GetByConversationAndID(ctx, conv.ID, msg.ID)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "ConvScopedMsg", found.Content)
|
||
|
||
// 错误路径:conversationID不匹配
|
||
_, err = svc.GetByConversationAndID(ctx, 9999, msg.ID)
|
||
assert.Error(t, err)
|
||
|
||
// 错误路径:ID不存在
|
||
_, err = svc.GetByConversationAndID(ctx, conv.ID, 9999)
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
// ========== Search 测试 ==========
|
||
|
||
func TestMessageService_Search(t *testing.T) {
|
||
// Search底层使用ILIKE,SQLite不支持该语法
|
||
// 在SQLite环境下Search会返回SQL错误,Service层只做委托调用,这是预期行为
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg1 := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "important meeting", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
msg2 := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "casual chat", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg1).Error)
|
||
require.NoError(t, db.Create(msg2).Error)
|
||
|
||
// ILIKE不兼容SQLite,在SQLite下搜索会返回SQL语法错误
|
||
// 这属于Repo层SQL兼容性问题,Service层本身只做委托调用
|
||
results, total, err := svc.Search(ctx, account.ID, "meeting", 0, 10, search.SearchModeILike)
|
||
if err != nil {
|
||
// SQLite环境:ILIKE语法不被支持,Search返回错误属于预期行为
|
||
t.Logf("Search在SQLite下返回错误(ILIKE不兼容): %v", err)
|
||
} else {
|
||
// PostgreSQL环境:正常搜索
|
||
assert.Equal(t, int64(1), total)
|
||
assert.Len(t, results, 1)
|
||
if len(results) > 0 {
|
||
assert.Equal(t, "important meeting", results[0].Content)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== Create 测试 ==========
|
||
|
||
func TestMessageService_Create(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
user := createTestUser(t, db, account.ID)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
// 正常路径:创建消息
|
||
req := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "测试消息内容",
|
||
MessageType: "outgoing",
|
||
ContentType: "text",
|
||
}
|
||
created, err := svc.Create(ctx, account.ID, user.ID, req)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, conv.ID, created.ConversationID)
|
||
assert.Equal(t, account.ID, created.AccountID)
|
||
assert.Equal(t, "测试消息内容", created.Content)
|
||
assert.Equal(t, "outgoing", created.MessageType)
|
||
assert.Equal(t, "text", created.ContentType)
|
||
assert.Equal(t, user.ID, *created.SenderID)
|
||
assert.Equal(t, "user", created.SenderType)
|
||
assert.False(t, created.Private)
|
||
|
||
// 正常路径:默认ContentType为text
|
||
reqNoContentType := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "无ContentType",
|
||
MessageType: "outgoing",
|
||
}
|
||
created2, err2 := svc.Create(ctx, account.ID, user.ID, reqNoContentType)
|
||
assert.NoError(t, err2)
|
||
assert.Equal(t, "text", created2.ContentType)
|
||
|
||
// Chatwoot parity: incoming messages are only allowed in API inboxes
|
||
reqIncomingNonAPI := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "非 API 收件箱 incoming",
|
||
MessageType: "incoming",
|
||
}
|
||
_, errIncomingNonAPI := svc.Create(ctx, account.ID, user.ID, reqIncomingNonAPI)
|
||
assert.EqualError(t, errIncomingNonAPI, "Incoming messages are only allowed in Api inboxes")
|
||
|
||
apiInbox := createTestInbox(t, db, account.ID, string(model.InboxChannelTypeAPI))
|
||
apiConv := createTestConversation(t, db, account.ID, apiInbox.ID, contact.ID)
|
||
reqIncomingAPI := CreateMessageRequest{
|
||
ConversationID: apiConv.ID,
|
||
Content: "API incoming",
|
||
MessageType: "incoming",
|
||
}
|
||
createdIncomingAPI, errIncomingAPI := svc.Create(ctx, account.ID, user.ID, reqIncomingAPI)
|
||
assert.NoError(t, errIncomingAPI)
|
||
assert.Equal(t, "incoming", createdIncomingAPI.MessageType)
|
||
|
||
// Chatwoot parity: campaign_id and template_params are stored in additional_attributes
|
||
templateParams := datatypes.JSON([]byte(`{"name":"customer_name","processed_params":{"body":"Hi"}}`))
|
||
reqWithAdditionalAttributes := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "带模板参数",
|
||
MessageType: "outgoing",
|
||
CampaignID: float64(42),
|
||
TemplateParams: templateParams,
|
||
}
|
||
createdWithAdditionalAttributes, errWithAdditionalAttributes := svc.Create(ctx, account.ID, user.ID, reqWithAdditionalAttributes)
|
||
assert.NoError(t, errWithAdditionalAttributes)
|
||
var additionalAttributes map[string]any
|
||
require.NoError(t, json.Unmarshal(createdWithAdditionalAttributes.AdditionalAttributes, &additionalAttributes))
|
||
assert.Equal(t, float64(42), additionalAttributes["campaign_id"])
|
||
assert.Equal(t, "customer_name", additionalAttributes["template_params"].(map[string]any)["name"])
|
||
|
||
// Chatwoot parity: email inbox recipient params are stored in content_attributes
|
||
emailInbox := createTestInbox(t, db, account.ID, string(model.InboxChannelTypeEmail))
|
||
emailConv := createTestConversation(t, db, account.ID, emailInbox.ID, contact.ID)
|
||
reqEmailRecipients := CreateMessageRequest{
|
||
ConversationID: emailConv.ID,
|
||
Content: "Email reply",
|
||
MessageType: "outgoing",
|
||
EmailHTMLContent: "<p>Email reply</p>",
|
||
ContentAttributes: datatypes.JSON([]byte(`{"email":{"html_content":{"reply":"<p>Hello</p>"}}}`)),
|
||
CCEmails: "agent_cc1@example.com, agent_cc2@example.com",
|
||
BCCEmails: "agent_bcc@example.com",
|
||
ToEmails: "customer@example.com",
|
||
}
|
||
createdEmailMessage, errEmailMessage := svc.Create(ctx, account.ID, user.ID, reqEmailRecipients)
|
||
assert.NoError(t, errEmailMessage)
|
||
var emailContentAttributes map[string]any
|
||
require.NoError(t, json.Unmarshal(createdEmailMessage.ContentAttributes, &emailContentAttributes))
|
||
assert.Equal(t, []any{"agent_cc1@example.com", "agent_cc2@example.com"}, emailContentAttributes["cc_emails"])
|
||
assert.Equal(t, []any{"agent_bcc@example.com"}, emailContentAttributes["bcc_emails"])
|
||
assert.Equal(t, []any{"customer@example.com"}, emailContentAttributes["to_emails"])
|
||
emailAttrs := emailContentAttributes["email"].(map[string]any)
|
||
assert.Equal(t, "Email reply", emailAttrs["text_content"].(map[string]any)["reply"])
|
||
assert.Equal(t, "<p>Email reply</p>", emailAttrs["html_content"].(map[string]any)["reply"])
|
||
|
||
// Chatwoot parity: stringified content_attributes are parsed as JSON objects
|
||
encodedAttrs, err := json.Marshal(`{"automation_rule_id":123,"items":[{"title":"Plan","value":"pro"}]}`)
|
||
require.NoError(t, err)
|
||
reqStringContentAttributes := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "String attrs",
|
||
MessageType: "outgoing",
|
||
ExternalCreatedAt: "2026-06-09 10:30:00 UTC",
|
||
ContentAttributes: datatypes.JSON(encodedAttrs),
|
||
}
|
||
createdStringAttrsMessage, errStringAttrsMessage := svc.Create(ctx, account.ID, user.ID, reqStringContentAttributes)
|
||
assert.NoError(t, errStringAttrsMessage)
|
||
var stringContentAttributes map[string]any
|
||
require.NoError(t, json.Unmarshal(createdStringAttrsMessage.ContentAttributes, &stringContentAttributes))
|
||
assert.Equal(t, float64(123), stringContentAttributes["automation_rule_id"])
|
||
assert.Equal(t, "2026-06-09 10:30:00 UTC", stringContentAttributes["external_created_at"])
|
||
require.Len(t, stringContentAttributes["items"], 1)
|
||
|
||
// Chatwoot parity: in_reply_to is normalized to a message in the same conversation
|
||
quotedSource := "provider-reply-1"
|
||
quotedMessage := &model.Message{
|
||
ConversationID: conv.ID,
|
||
AccountID: account.ID,
|
||
InboxID: inbox.ID,
|
||
Content: "Quoted",
|
||
MessageType: "incoming",
|
||
ContentType: "text",
|
||
SenderType: "contact",
|
||
SourceID: quotedSource,
|
||
}
|
||
require.NoError(t, db.Create(quotedMessage).Error)
|
||
reqReplyContentAttributes := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "Reply",
|
||
MessageType: "outgoing",
|
||
ContentAttributes: datatypes.JSON([]byte(fmt.Sprintf(`{"in_reply_to":%d,"items":[{"title":"Plan"}]}`, quotedMessage.ID))),
|
||
}
|
||
createdReplyMessage, errReplyMessage := svc.Create(ctx, account.ID, user.ID, reqReplyContentAttributes)
|
||
assert.NoError(t, errReplyMessage)
|
||
var replyContentAttributes map[string]any
|
||
require.NoError(t, json.Unmarshal(createdReplyMessage.ContentAttributes, &replyContentAttributes))
|
||
assert.Equal(t, float64(quotedMessage.ID), replyContentAttributes["in_reply_to"])
|
||
assert.Equal(t, quotedSource, replyContentAttributes["in_reply_to_external_id"])
|
||
require.Len(t, replyContentAttributes["items"], 1)
|
||
|
||
otherConv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
reqCrossConversationReply := CreateMessageRequest{
|
||
ConversationID: otherConv.ID,
|
||
Content: "Cross reply",
|
||
MessageType: "outgoing",
|
||
ContentAttributes: datatypes.JSON([]byte(fmt.Sprintf(`{"in_reply_to":%d}`, quotedMessage.ID))),
|
||
}
|
||
createdCrossReplyMessage, errCrossReplyMessage := svc.Create(ctx, account.ID, user.ID, reqCrossConversationReply)
|
||
assert.NoError(t, errCrossReplyMessage)
|
||
var crossReplyContentAttributes map[string]any
|
||
require.NoError(t, json.Unmarshal(createdCrossReplyMessage.ContentAttributes, &crossReplyContentAttributes))
|
||
assert.Nil(t, crossReplyContentAttributes["in_reply_to"])
|
||
assert.Nil(t, crossReplyContentAttributes["in_reply_to_external_id"])
|
||
|
||
// Chatwoot parity: structured bot content types can be created without text content
|
||
for _, contentType := range []string{"input_select", "cards"} {
|
||
createdStructuredMessage, errStructuredMessage := svc.Create(ctx, account.ID, user.ID, CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
MessageType: "outgoing",
|
||
ContentType: contentType,
|
||
ContentAttributes: datatypes.JSON([]byte(`{"items":[{"title":"Plan","value":"pro"}]}`)),
|
||
})
|
||
assert.NoError(t, errStructuredMessage)
|
||
assert.Equal(t, contentType, createdStructuredMessage.ContentType)
|
||
assert.Empty(t, createdStructuredMessage.Content)
|
||
}
|
||
|
||
// Chatwoot parity: sender_type=AgentBot uses an accessible agent bot as sender
|
||
require.NoError(t, db.AutoMigrate(&model.AgentBot{}))
|
||
agentBot := &model.AgentBot{AccountID: &account.ID, Name: "Reply Bot", BotType: "webhook"}
|
||
require.NoError(t, db.Create(agentBot).Error)
|
||
reqAgentBotSender := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "Agent bot reply",
|
||
MessageType: "outgoing",
|
||
SenderType: string(model.SenderTypeAgentBot),
|
||
SenderID: agentBot.ID,
|
||
}
|
||
createdAgentBotMessage, errAgentBotMessage := svc.Create(ctx, account.ID, user.ID, reqAgentBotSender)
|
||
assert.NoError(t, errAgentBotMessage)
|
||
assert.Equal(t, string(model.SenderTypeAgentBot), createdAgentBotMessage.SenderType)
|
||
assert.Equal(t, agentBot.ID, *createdAgentBotMessage.SenderID)
|
||
|
||
reqMissingAgentBotSender := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "Fallback reply",
|
||
MessageType: "outgoing",
|
||
SenderType: string(model.SenderTypeAgentBot),
|
||
SenderID: agentBot.ID + 999,
|
||
}
|
||
createdFallbackMessage, errFallbackMessage := svc.Create(ctx, account.ID, user.ID, reqMissingAgentBotSender)
|
||
assert.NoError(t, errFallbackMessage)
|
||
assert.Equal(t, "user", createdFallbackMessage.SenderType)
|
||
assert.Equal(t, user.ID, *createdFallbackMessage.SenderID)
|
||
|
||
// Chatwoot parity: is_voice_message tags only audio attachments
|
||
reqVoiceMessage := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "voice note",
|
||
MessageType: "outgoing",
|
||
IsVoiceMessage: true,
|
||
Attachments: []MessageAttachmentInput{
|
||
{FileName: "voice.mp3", FileSize: 1234, ContentType: "audio/mpeg"},
|
||
{FileName: "transcript.txt", FileSize: 12, ContentType: "text/plain"},
|
||
},
|
||
}
|
||
createdVoiceMessage, errVoiceMessage := svc.Create(ctx, account.ID, user.ID, reqVoiceMessage)
|
||
assert.NoError(t, errVoiceMessage)
|
||
var voiceAttachments []model.Attachment
|
||
require.NoError(t, db.Where("message_id = ?", createdVoiceMessage.ID).Order("id ASC").Find(&voiceAttachments).Error)
|
||
require.Len(t, voiceAttachments, 2)
|
||
assert.Equal(t, `{"is_voice_message":true}`, voiceAttachments[0].Metadata)
|
||
assert.Empty(t, voiceAttachments[1].Metadata)
|
||
|
||
// 正常路径:创建私密消息
|
||
reqPrivate := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "私密消息",
|
||
MessageType: "private_note",
|
||
Private: true,
|
||
}
|
||
created3, err3 := svc.Create(ctx, account.ID, user.ID, reqPrivate)
|
||
assert.NoError(t, err3)
|
||
assert.True(t, created3.Private)
|
||
|
||
// 错误路径:验证失败 - 空Content
|
||
reqEmpty := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "",
|
||
MessageType: "outgoing",
|
||
}
|
||
_, err4 := svc.Create(ctx, account.ID, user.ID, reqEmpty)
|
||
assert.Error(t, err4)
|
||
|
||
// 错误路径:验证失败 - 无效MessageType
|
||
reqInvalid := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "内容",
|
||
MessageType: "invalid_type",
|
||
}
|
||
_, err5 := svc.Create(ctx, account.ID, user.ID, reqInvalid)
|
||
assert.Error(t, err5)
|
||
|
||
// 错误路径:验证失败 - 无效ContentType
|
||
reqInvalidCT := CreateMessageRequest{
|
||
ConversationID: conv.ID,
|
||
Content: "内容",
|
||
MessageType: "outgoing",
|
||
ContentType: "invalid_ct",
|
||
}
|
||
_, err6 := svc.Create(ctx, account.ID, user.ID, reqInvalidCT)
|
||
assert.Error(t, err6)
|
||
|
||
// 错误路径:验证失败 - 缺少ConversationID
|
||
reqNoConvID := CreateMessageRequest{
|
||
Content: "内容",
|
||
MessageType: "outgoing",
|
||
}
|
||
_, err7 := svc.Create(ctx, account.ID, user.ID, reqNoConvID)
|
||
assert.Error(t, err7)
|
||
}
|
||
|
||
func TestMessageService_ConversationScopedMessageActions(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
user := createTestUser(t, db, account.ID)
|
||
inbox := createTestInbox(t, db, account.ID, "api")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conversation := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
otherConversation := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
message := &model.Message{
|
||
ConversationID: conversation.ID,
|
||
AccountID: account.ID,
|
||
InboxID: inbox.ID,
|
||
SenderID: &user.ID,
|
||
SenderType: "user",
|
||
Content: "route scoped",
|
||
MessageType: "outgoing",
|
||
ContentType: "text",
|
||
Status: "failed",
|
||
}
|
||
require.NoError(t, db.Create(message).Error)
|
||
|
||
found, err := svc.GetByAccountConversationAndID(ctx, account.ID, conversation.ID, message.ID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, message.ID, found.ID)
|
||
|
||
_, err = svc.GetByAccountConversationAndID(ctx, account.ID, otherConversation.ID, message.ID)
|
||
assert.Error(t, err)
|
||
|
||
_, err = svc.UpdateInConversation(ctx, account.ID, otherConversation.ID, message.ID, UpdateMessageRequest{Status: "delivered"})
|
||
assert.Error(t, err)
|
||
require.NoError(t, db.First(message, message.ID).Error)
|
||
assert.Equal(t, "route scoped", message.Content)
|
||
assert.Equal(t, "failed", message.Status)
|
||
|
||
updated, err := svc.UpdateInConversation(ctx, account.ID, conversation.ID, message.ID, UpdateMessageRequest{Status: "delivered"})
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "route scoped", updated.Content)
|
||
assert.Equal(t, "delivered", updated.Status)
|
||
require.NoError(t, db.Model(message).Update("status", "failed").Error)
|
||
|
||
_, err = svc.RetryInConversation(ctx, account.ID, otherConversation.ID, message.ID)
|
||
assert.Error(t, err)
|
||
require.NoError(t, db.First(message, message.ID).Error)
|
||
assert.Equal(t, "failed", message.Status)
|
||
|
||
retried, err := svc.RetryInConversation(ctx, account.ID, conversation.ID, message.ID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "sent", retried.Status)
|
||
|
||
_, err = svc.DeleteInConversation(ctx, account.ID, otherConversation.ID, message.ID)
|
||
assert.Error(t, err)
|
||
require.NoError(t, db.First(message, message.ID).Error)
|
||
assert.NotContains(t, string(message.ContentAttributes), "deleted")
|
||
|
||
deleted, err := svc.DeleteInConversation(ctx, account.ID, conversation.ID, message.ID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "This message was deleted", deleted.Content)
|
||
assert.JSONEq(t, `{"deleted":true}`, string(deleted.ContentAttributes))
|
||
}
|
||
|
||
// ========== Update 测试 ==========
|
||
|
||
func TestMessageService_Update(t *testing.T) {
|
||
db, _, dispatcher, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "api")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "原始内容", MessageType: "outgoing", ContentType: "text", SenderType: "user", Status: "sent",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
listener := &mockRetryListener{}
|
||
dispatcher.Register(listener)
|
||
|
||
req := UpdateMessageRequest{Status: "failed", ExternalError: "provider rejected message"}
|
||
updated, err := svc.Update(ctx, account.ID, msg.ID, req)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "failed", updated.Status)
|
||
assert.Equal(t, "原始内容", updated.Content)
|
||
assert.JSONEq(t, `{"external_error":"provider rejected message"}`, string(updated.ContentAttributes))
|
||
assert.True(t, listener.received)
|
||
assert.Equal(t, channel.EventMessageUpdated, listener.lastEventType)
|
||
assert.Equal(t, account.ID, listener.lastAccountID)
|
||
assert.Equal(t, inbox.ID, listener.lastInboxID)
|
||
assert.Equal(t, conv.ID, listener.lastConvID)
|
||
eventMessage, ok := listener.lastData["message"].(*model.Message)
|
||
require.True(t, ok)
|
||
assert.Equal(t, msg.ID, eventMessage.ID)
|
||
assert.Equal(t, "failed", eventMessage.Status)
|
||
assert.JSONEq(t, `{"external_error":"provider rejected message"}`, string(eventMessage.ContentAttributes))
|
||
|
||
reqEmpty := UpdateMessageRequest{}
|
||
updated2, err2 := svc.Update(ctx, account.ID, msg.ID, reqEmpty)
|
||
assert.NoError(t, err2)
|
||
assert.Equal(t, "failed", updated2.Status)
|
||
assert.Equal(t, "原始内容", updated2.Content)
|
||
|
||
_, err = svc.Update(ctx, account.ID, msg.ID, UpdateMessageRequest{Status: "invalid"})
|
||
assert.Error(t, err)
|
||
|
||
webInbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
webConv := createTestConversation(t, db, account.ID, webInbox.ID, contact.ID)
|
||
webMessage := &model.Message{
|
||
ConversationID: webConv.ID, AccountID: account.ID, InboxID: webInbox.ID,
|
||
Content: "web", MessageType: "outgoing", ContentType: "text", SenderType: "user", Status: "sent",
|
||
}
|
||
require.NoError(t, db.Create(webMessage).Error)
|
||
_, err = svc.Update(ctx, account.ID, webMessage.ID, UpdateMessageRequest{Status: "delivered"})
|
||
assert.Error(t, err)
|
||
|
||
// 错误路径:accountID不匹配
|
||
_, err = svc.Update(ctx, 9999, msg.ID, req)
|
||
assert.Error(t, err)
|
||
|
||
// 错误路径:消息ID不存在
|
||
_, err = svc.Update(ctx, account.ID, 9999, req)
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
// ========== Delete 测试 ==========
|
||
|
||
func TestMessageService_Delete(t *testing.T) {
|
||
db, _, dispatcher, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "待删除消息", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
attachment := &model.Attachment{MessageID: msg.ID, AccountID: account.ID, FileType: "file", FileName: "delete.txt"}
|
||
require.NoError(t, db.Create(attachment).Error)
|
||
listener := &mockRetryListener{}
|
||
dispatcher.Register(listener)
|
||
|
||
// 正常路径:Chatwoot 删除会保留消息并标记 content_attributes.deleted
|
||
deleted, err := svc.Delete(ctx, account.ID, msg.ID)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "This message was deleted", deleted.Content)
|
||
assert.True(t, listener.received)
|
||
assert.Equal(t, channel.EventMessageDeleted, listener.lastEventType)
|
||
assert.Equal(t, account.ID, listener.lastAccountID)
|
||
assert.Equal(t, inbox.ID, listener.lastInboxID)
|
||
assert.Equal(t, conv.ID, listener.lastConvID)
|
||
eventMessage, ok := listener.lastData["message"].(*model.Message)
|
||
require.True(t, ok)
|
||
assert.Equal(t, msg.ID, eventMessage.ID)
|
||
assert.Equal(t, "This message was deleted", eventMessage.Content)
|
||
assert.JSONEq(t, `{"deleted":true}`, string(eventMessage.ContentAttributes))
|
||
|
||
stored, err := svc.GetByAccountAndID(ctx, account.ID, msg.ID)
|
||
assert.NoError(t, err)
|
||
assert.JSONEq(t, `{"deleted":true}`, string(stored.ContentAttributes))
|
||
|
||
var attachmentCount int64
|
||
require.NoError(t, db.Model(&model.Attachment{}).Where("message_id = ?", msg.ID).Count(&attachmentCount).Error)
|
||
assert.Equal(t, int64(0), attachmentCount)
|
||
|
||
// 错误路径:accountID不匹配
|
||
msg2 := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "另一条消息", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg2).Error)
|
||
_, err = svc.Delete(ctx, 9999, msg2.ID)
|
||
assert.Error(t, err)
|
||
|
||
// 错误路径:ID不存在
|
||
_, err = svc.Delete(ctx, account.ID, 9999)
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
// ========== CountByConversation 测试 ==========
|
||
|
||
func TestMessageService_CountByConversation(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
ctx := context.Background()
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
// 创建3条消息
|
||
for i := 0; i < 3; i++ {
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: fmt.Sprintf("消息%c", 'A'+i),
|
||
MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
}
|
||
|
||
// 正常路径:统计会话消息数量
|
||
count, err := svc.CountByConversation(ctx, conv.ID)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, int64(3), count)
|
||
|
||
// 正常路径:空会话
|
||
count2, err2 := svc.CountByConversation(ctx, 9999)
|
||
assert.NoError(t, err2)
|
||
assert.Equal(t, int64(0), count2)
|
||
}
|
||
|
||
// ========== Translate 测试 ==========
|
||
|
||
func TestMessageService_Translate(t *testing.T) {
|
||
ctx := context.Background()
|
||
|
||
t.Run("success", func(t *testing.T) {
|
||
mockLLM := &mockMessageLLMProvider{
|
||
chatResponse: &llm.ChatResponse{
|
||
Choices: []llm.ChatChoice{
|
||
{Message: llm.ChatMessage{Role: "assistant", Content: "Hello"}},
|
||
},
|
||
},
|
||
}
|
||
db, _, _, svc := setupMessageService(t, mockLLM)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "你好", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
result, err := svc.Translate(ctx, account.ID, msg.ID, TranslateMessageRequest{TargetLanguage: "en"})
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, msg.ID, result.ID)
|
||
assert.Equal(t, "你好", result.OriginalContent)
|
||
assert.Equal(t, "Hello", result.TranslatedContent)
|
||
assert.Equal(t, "en", result.TargetLanguage)
|
||
assert.Equal(t, 1, mockLLM.chatCalls)
|
||
|
||
resultCached, errCached := svc.Translate(ctx, account.ID, msg.ID, TranslateMessageRequest{TargetLanguage: "en"})
|
||
assert.NoError(t, errCached)
|
||
assert.Equal(t, "Hello", resultCached.TranslatedContent)
|
||
assert.Equal(t, 1, mockLLM.chatCalls)
|
||
})
|
||
|
||
t.Run("empty_choices", func(t *testing.T) {
|
||
mockLLM := &mockMessageLLMProvider{
|
||
chatResponse: &llm.ChatResponse{
|
||
Choices: []llm.ChatChoice{},
|
||
},
|
||
}
|
||
db, _, _, svc := setupMessageService(t, mockLLM)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "你好", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
result, err := svc.Translate(ctx, account.ID, msg.ID, TranslateMessageRequest{TargetLanguage: "en"})
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "", result.TranslatedContent)
|
||
})
|
||
|
||
t.Run("llm_error", func(t *testing.T) {
|
||
mockLLM := &mockMessageLLMProvider{
|
||
chatErr: fmt.Errorf("LLM service unavailable"),
|
||
}
|
||
db, _, _, svc := setupMessageService(t, mockLLM)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "你好", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
_, err := svc.Translate(ctx, account.ID, msg.ID, TranslateMessageRequest{TargetLanguage: "en"})
|
||
assert.Error(t, err)
|
||
})
|
||
|
||
t.Run("message_not_found", func(t *testing.T) {
|
||
mockLLM := &mockMessageLLMProvider{
|
||
chatResponse: &llm.ChatResponse{
|
||
Choices: []llm.ChatChoice{
|
||
{Message: llm.ChatMessage{Role: "assistant", Content: "translated"}},
|
||
},
|
||
},
|
||
}
|
||
db, _, _, svc := setupMessageService(t, mockLLM)
|
||
|
||
account := createTestAccount(t, db)
|
||
|
||
_, err := svc.Translate(ctx, account.ID, 9999, TranslateMessageRequest{TargetLanguage: "en"})
|
||
assert.Error(t, err)
|
||
})
|
||
|
||
t.Run("validation_empty_target_language", func(t *testing.T) {
|
||
mockLLM := &mockMessageLLMProvider{}
|
||
db, _, _, svc := setupMessageService(t, mockLLM)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "你好", MessageType: "incoming", ContentType: "text", SenderType: "contact",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
_, err := svc.Translate(ctx, account.ID, msg.ID, TranslateMessageRequest{TargetLanguage: ""})
|
||
assert.Error(t, err)
|
||
})
|
||
}
|
||
|
||
// ========== Retry 测试 ==========
|
||
|
||
func TestMessageService_Retry(t *testing.T) {
|
||
ctx := context.Background()
|
||
|
||
t.Run("success", func(t *testing.T) {
|
||
db, _, dispatcher, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "failed message", MessageType: "outgoing", ContentType: "text", SenderType: "user",
|
||
Status: "failed", ContentAttributes: datatypes.JSON([]byte(`{"external_error":"provider failed"}`)),
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
// Register a listener to verify dispatch
|
||
listener := &mockRetryListener{}
|
||
dispatcher.Register(listener)
|
||
|
||
retried, err := svc.Retry(ctx, account.ID, msg.ID)
|
||
assert.NoError(t, err)
|
||
assert.Equal(t, "sent", retried.Status)
|
||
assert.JSONEq(t, `{}`, string(retried.ContentAttributes))
|
||
assert.True(t, listener.received)
|
||
assert.Equal(t, channel.EventMessageStatusUpdated, listener.lastEventType)
|
||
assert.Equal(t, account.ID, listener.lastAccountID)
|
||
assert.Equal(t, inbox.ID, listener.lastInboxID)
|
||
assert.Equal(t, conv.ID, listener.lastConvID)
|
||
assert.Equal(t, "sent", listener.lastData["status"])
|
||
assert.Equal(t, msg.ID, listener.lastData["message_id"])
|
||
|
||
var stored model.Message
|
||
require.NoError(t, db.First(&stored, msg.ID).Error)
|
||
assert.JSONEq(t, `{}`, string(stored.ContentAttributes))
|
||
})
|
||
|
||
t.Run("queues_send_reply_when_worker_configured", func(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
wp := worker.NewWorkerPool(db)
|
||
svc.SetWorkerPool(wp)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "failed message", MessageType: "outgoing", ContentType: "text", SenderType: "user",
|
||
Status: "failed", ContentAttributes: datatypes.JSON([]byte(`{"external_error":"provider failed"}`)),
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
retried, err := svc.Retry(ctx, account.ID, msg.ID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "sent", retried.Status)
|
||
|
||
var job model.BackgroundJob
|
||
require.NoError(t, db.Where("job_type = ?", TaskTypeMessageSendReply).First(&job).Error)
|
||
assert.Equal(t, model.BackgroundJobStatusQueued, job.Status)
|
||
assert.Equal(t, "message:send_reply", job.JobType)
|
||
assert.JSONEq(t, fmt.Sprintf(`{"message_id":%d}`, msg.ID), string(job.Payload))
|
||
})
|
||
|
||
t.Run("message_not_found", func(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
account := createTestAccount(t, db)
|
||
|
||
_, err := svc.Retry(ctx, account.ID, 9999)
|
||
assert.Error(t, err)
|
||
})
|
||
|
||
t.Run("account_mismatch", func(t *testing.T) {
|
||
db, _, _, svc := setupMessageServiceWithDefaultLLM(t)
|
||
|
||
account := createTestAccount(t, db)
|
||
inbox := createTestInbox(t, db, account.ID, "web_widget")
|
||
contact := createTestContact(t, db, account.ID)
|
||
conv := createTestConversation(t, db, account.ID, inbox.ID, contact.ID)
|
||
|
||
msg := &model.Message{
|
||
ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID,
|
||
Content: "test", MessageType: "outgoing", ContentType: "text", SenderType: "user",
|
||
Status: "failed",
|
||
}
|
||
require.NoError(t, db.Create(msg).Error)
|
||
|
||
_, err := svc.Retry(ctx, 9999, msg.ID)
|
||
assert.Error(t, err)
|
||
})
|
||
}
|