314 lines
9.7 KiB
Go
314 lines
9.7 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-desk/internal/ai"
|
|
"agent-desk/internal/models"
|
|
"agent-desk/internal/pkg/constants"
|
|
"agent-desk/internal/pkg/dto"
|
|
"agent-desk/internal/pkg/dto/request"
|
|
"agent-desk/internal/pkg/dto/response"
|
|
"agent-desk/internal/pkg/enums"
|
|
"agent-desk/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/mlogclub/simple/sqls"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/schema"
|
|
)
|
|
|
|
func setupConversationDashboardTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
|
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{
|
|
NamingStrategy: schema.NamingStrategy{
|
|
TablePrefix: "t_",
|
|
SingularTable: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if sqlDB, err := db.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
})
|
|
|
|
if err := db.AutoMigrate(
|
|
&models.Conversation{},
|
|
&models.Message{},
|
|
&models.User{},
|
|
); err != nil {
|
|
t.Fatalf("auto migrate error = %v", err)
|
|
}
|
|
sqls.SetDB(db)
|
|
return db
|
|
}
|
|
|
|
func TestConversationSuggestReply(t *testing.T) {
|
|
db := setupConversationDashboardTestDB(t)
|
|
|
|
conv := &models.Conversation{
|
|
ID: 101,
|
|
Subject: "Suggest Reply Test",
|
|
}
|
|
if err := db.Create(conv).Error; err != nil {
|
|
t.Fatalf("create conv error = %v", err)
|
|
}
|
|
msg := &models.Message{
|
|
ID: 1,
|
|
ConversationID: conv.ID,
|
|
ClientMsgID: "msg_1",
|
|
SenderType: enums.IMSenderTypeCustomer,
|
|
MessageType: enums.IMMessageTypeText,
|
|
Content: "Can you help me reset my password?",
|
|
}
|
|
if err := db.Create(msg).Error; err != nil {
|
|
t.Fatalf("create msg error = %v", err)
|
|
}
|
|
|
|
t.Run("unauthorized when no auth principal", func(t *testing.T) {
|
|
router := gin.New()
|
|
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
|
|
|
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: conv.ID})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
if resp["success"] == true {
|
|
t.Fatalf("expected failure without auth, got success: true")
|
|
}
|
|
})
|
|
|
|
t.Run("forbidden when principal lacks permission", func(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("authPrincipal", &dto.AuthPrincipal{
|
|
UserID: 1,
|
|
Username: "agent",
|
|
Permissions: []string{"unrelated.permission"},
|
|
})
|
|
c.Next()
|
|
})
|
|
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
|
|
|
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: conv.ID})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
if resp["success"] == true {
|
|
t.Fatalf("expected forbidden error, got success: true")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid params when conversationId <= 0", func(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("authPrincipal", &dto.AuthPrincipal{
|
|
UserID: 1,
|
|
Username: "agent",
|
|
Permissions: []string{constants.PermissionConversationView.Code},
|
|
})
|
|
c.Next()
|
|
})
|
|
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
|
|
|
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: 0})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
if resp["success"] == true {
|
|
t.Fatalf("expected error for conversationId <= 0, got success: true")
|
|
}
|
|
})
|
|
|
|
t.Run("successful suggestion", func(t *testing.T) {
|
|
restore := services.AICopilotService.SetChatFnForTest(func(ctx context.Context, systemPrompt, userPrompt string) (*ai.ChatCompletionResult, error) {
|
|
return &ai.ChatCompletionResult{
|
|
Content: "Click 'Forgot Password' to reset your credentials.",
|
|
ModelName: "mock-model",
|
|
}, nil
|
|
})
|
|
defer restore()
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("authPrincipal", &dto.AuthPrincipal{
|
|
UserID: 1,
|
|
Username: "agent",
|
|
Permissions: []string{constants.PermissionConversationView.Code},
|
|
})
|
|
c.Next()
|
|
})
|
|
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
|
|
|
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: conv.ID})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Code int `json:"code"`
|
|
Success bool `json:"success"`
|
|
Data response.SuggestReplyResponse `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal response error = %v", err)
|
|
}
|
|
if !resp.Success {
|
|
t.Fatalf("expected success: true, got false. Body: %s", rec.Body.String())
|
|
}
|
|
expectedSuggestion := "Click 'Forgot Password' to reset your credentials."
|
|
if resp.Data.Suggestion != expectedSuggestion {
|
|
t.Fatalf("expected suggestion %q, got %q", expectedSuggestion, resp.Data.Suggestion)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConversationSummarize(t *testing.T) {
|
|
db := setupConversationDashboardTestDB(t)
|
|
|
|
conv := &models.Conversation{
|
|
ID: 201,
|
|
Subject: "Summarize Test",
|
|
}
|
|
if err := db.Create(conv).Error; err != nil {
|
|
t.Fatalf("create conv error = %v", err)
|
|
}
|
|
msg := &models.Message{
|
|
ID: 20,
|
|
ConversationID: conv.ID,
|
|
ClientMsgID: "msg_20",
|
|
SenderType: enums.IMSenderTypeCustomer,
|
|
MessageType: enums.IMMessageTypeText,
|
|
Content: "Billing dispute for invoice #1234",
|
|
}
|
|
if err := db.Create(msg).Error; err != nil {
|
|
t.Fatalf("create msg error = %v", err)
|
|
}
|
|
|
|
t.Run("unauthorized when no auth principal", func(t *testing.T) {
|
|
router := gin.New()
|
|
router.POST("/api/dashboard/conversation/summarize", ConversationPostSummarize)
|
|
|
|
body, _ := json.Marshal(request.SummarizeConversationRequest{ConversationID: conv.ID})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/summarize", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
if resp["success"] == true {
|
|
t.Fatalf("expected failure without auth, got success: true")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid params when conversationId <= 0", func(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("authPrincipal", &dto.AuthPrincipal{
|
|
UserID: 1,
|
|
Username: "agent",
|
|
Permissions: []string{constants.PermissionConversationView.Code},
|
|
})
|
|
c.Next()
|
|
})
|
|
router.POST("/api/dashboard/conversation/summarize", ConversationPostSummarize)
|
|
|
|
body, _ := json.Marshal(request.SummarizeConversationRequest{ConversationID: -5})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/summarize", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
var resp map[string]any
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
if resp["success"] == true {
|
|
t.Fatalf("expected error for conversationId <= 0, got success: true")
|
|
}
|
|
})
|
|
|
|
t.Run("successful summary", func(t *testing.T) {
|
|
restore := services.AICopilotService.SetChatFnForTest(func(ctx context.Context, systemPrompt, userPrompt string) (*ai.ChatCompletionResult, error) {
|
|
return &ai.ChatCompletionResult{
|
|
Content: "Summary: Customer inquired about invoice #1234.",
|
|
ModelName: "mock-model",
|
|
}, nil
|
|
})
|
|
defer restore()
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("authPrincipal", &dto.AuthPrincipal{
|
|
UserID: 1,
|
|
Username: "agent",
|
|
Permissions: []string{constants.PermissionConversationView.Code},
|
|
})
|
|
c.Next()
|
|
})
|
|
router.POST("/api/dashboard/conversation/summarize", ConversationPostSummarize)
|
|
|
|
body, _ := json.Marshal(request.SummarizeConversationRequest{ConversationID: conv.ID})
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/summarize", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Code int `json:"code"`
|
|
Success bool `json:"success"`
|
|
Data response.ConversationSummaryResponse `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal response error = %v", err)
|
|
}
|
|
if !resp.Success {
|
|
t.Fatalf("expected success: true, got false. Body: %s", rec.Body.String())
|
|
}
|
|
expectedSummary := "Summary: Customer inquired about invoice #1234."
|
|
if resp.Data.Summary != expectedSummary {
|
|
t.Fatalf("expected summary %q, got %q", expectedSummary, resp.Data.Summary)
|
|
}
|
|
})
|
|
}
|