* H-300: wire Captain Skills into Web runtime * H-300: enforce effective model and conservative skill budget * H-300: fix CI gosec step * ci: extend golangci-lint timeout * fix lint findings across backend * fix(push): resolve delivery protocol blockers * test(repository): close SQLite test databases * test(repository): reuse SQLite schema per package * H-307: restore backend Go cache in CI * H-307: prefetch modules before cold lint * H-307: resolve govulncheck security gate * H-307: build lint with patched Go toolchain * H-307: clear remaining security scan findings --------- Co-authored-by: Rogee <rogee@ipao.vip>
528 lines
15 KiB
Go
528 lines
15 KiB
Go
package csat
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
func newTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
err = db.AutoMigrate(
|
|
&CsatSurveyResponse{},
|
|
&model.Conversation{},
|
|
&model.Message{},
|
|
)
|
|
require.NoError(t, err)
|
|
return db
|
|
}
|
|
|
|
// --- Model Tests ---
|
|
|
|
func TestCsatSurveyResponse_TableName(t *testing.T) {
|
|
assert.Equal(t, "csat_survey_responses", CsatSurveyResponse{}.TableName())
|
|
}
|
|
|
|
func TestCsatSurveyResponse_Fields(t *testing.T) {
|
|
now := time.Now()
|
|
c := CsatSurveyResponse{
|
|
AccountID: 1,
|
|
ConversationID: 2,
|
|
ContactID: 3,
|
|
MessageID: 4,
|
|
Rating: 5,
|
|
FeedbackMessage: "Great service!",
|
|
CsatReviewNotes: "Reviewed by admin",
|
|
ReviewNotesUpdatedAt: &now,
|
|
ReviewNotesUpdatedByID: uintPtr(10),
|
|
}
|
|
assert.Equal(t, uint(1), c.AccountID)
|
|
assert.Equal(t, uint(2), c.ConversationID)
|
|
assert.Equal(t, uint(3), c.ContactID)
|
|
assert.Equal(t, uint(4), c.MessageID)
|
|
assert.Equal(t, 5, c.Rating)
|
|
assert.Equal(t, "Great service!", c.FeedbackMessage)
|
|
assert.Equal(t, "Reviewed by admin", c.CsatReviewNotes)
|
|
}
|
|
|
|
func uintPtr(v uint) *uint { return &v }
|
|
|
|
// --- Service Tests ---
|
|
|
|
func TestNewCsatSurveyService(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
require.NotNil(t, svc)
|
|
assert.Equal(t, db, svc.db)
|
|
}
|
|
|
|
func TestCsatSurveyService_Create(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
resp := &CsatSurveyResponse{
|
|
AccountID: 1,
|
|
ConversationID: 2,
|
|
ContactID: 3,
|
|
MessageID: 4,
|
|
Rating: 5,
|
|
FeedbackMessage: "Excellent!",
|
|
}
|
|
err := svc.Create(ctx, resp)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, resp.ID)
|
|
}
|
|
|
|
func TestCsatSurveyService_GetByID(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
resp := &CsatSurveyResponse{
|
|
AccountID: 1,
|
|
ConversationID: 2,
|
|
ContactID: 3,
|
|
MessageID: 4,
|
|
Rating: 4,
|
|
FeedbackMessage: "Good",
|
|
}
|
|
err := svc.Create(ctx, resp)
|
|
require.NoError(t, err)
|
|
|
|
retrieved, err := svc.GetByID(ctx, resp.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, resp.Rating, retrieved.Rating)
|
|
assert.Equal(t, resp.FeedbackMessage, retrieved.FeedbackMessage)
|
|
}
|
|
|
|
func TestCsatSurveyService_GetByID_NotFound(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
_, err := svc.GetByID(ctx, 99999)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestCsatSurveyService_ListByAccount(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
err := svc.Create(ctx, &CsatSurveyResponse{
|
|
AccountID: 1,
|
|
ConversationID: uint(i + 1),
|
|
ContactID: uint(i + 1),
|
|
MessageID: uint(i + 1),
|
|
Rating: i + 1,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
// Different account
|
|
err := svc.Create(ctx, &CsatSurveyResponse{
|
|
AccountID: 2, ConversationID: 10, ContactID: 10, MessageID: 10, Rating: 5,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
responses, count, err := svc.ListByAccount(ctx, 1, CsatFilterParams{}, 0, 25)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count)
|
|
assert.Len(t, responses, 3)
|
|
}
|
|
|
|
func TestCsatSurveyService_ListByAccount_WithRatingFilter(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 3}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 2, ContactID: 2, MessageID: 2, Rating: 5}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 3, ContactID: 3, MessageID: 3, Rating: 5}))
|
|
|
|
responses, count, err := svc.ListByAccount(ctx, 1, CsatFilterParams{Rating: 5}, 0, 25)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), count)
|
|
assert.Len(t, responses, 2)
|
|
}
|
|
|
|
func TestCsatSurveyService_ListByAccount_WithDateFilter(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
since := time.Now().Add(-1 * time.Hour)
|
|
until := time.Now().Add(1 * time.Hour)
|
|
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 5}))
|
|
|
|
responses, count, err := svc.ListByAccount(ctx, 1, CsatFilterParams{Since: &since, Until: &until}, 0, 25)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), count)
|
|
assert.Len(t, responses, 1)
|
|
}
|
|
|
|
func TestCsatSurveyService_ListByAccount_WithAgentFilter(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
agent1 := uint(100)
|
|
agent2 := uint(200)
|
|
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 5, AssignedAgentID: &agent1}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 2, ContactID: 2, MessageID: 2, Rating: 4, AssignedAgentID: &agent2}))
|
|
|
|
responses, count, err := svc.ListByAccount(ctx, 1, CsatFilterParams{AssignedAgentIDs: []uint{100}}, 0, 25)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), count)
|
|
assert.Len(t, responses, 1)
|
|
assert.Equal(t, uint(100), *responses[0].AssignedAgentID)
|
|
}
|
|
|
|
func TestCsatSurveyService_ListByAccount_Pagination(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{
|
|
AccountID: 1, ConversationID: uint(i + 1), ContactID: uint(i + 1), MessageID: uint(i + 1), Rating: i + 1,
|
|
}))
|
|
}
|
|
|
|
// Page 1: offset=0, limit=2
|
|
responses, count, err := svc.ListByAccount(ctx, 1, CsatFilterParams{}, 0, 2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(5), count)
|
|
assert.Len(t, responses, 2)
|
|
|
|
// Page 2: offset=2, limit=2
|
|
responses2, _, err := svc.ListByAccount(ctx, 1, CsatFilterParams{}, 2, 2)
|
|
require.NoError(t, err)
|
|
assert.Len(t, responses2, 2)
|
|
}
|
|
|
|
func TestCsatSurveyService_ListByConversation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 10, ContactID: 1, MessageID: 1, Rating: 5}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 10, ContactID: 1, MessageID: 2, Rating: 4}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 20, ContactID: 2, MessageID: 3, Rating: 3}))
|
|
|
|
responses, err := svc.ListByConversation(ctx, 1, 10)
|
|
require.NoError(t, err)
|
|
assert.Len(t, responses, 2)
|
|
}
|
|
|
|
func TestCsatSurveyService_UpdateReviewNotes(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
resp := &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 5}
|
|
err := svc.Create(ctx, resp)
|
|
require.NoError(t, err)
|
|
|
|
err = svc.UpdateReviewNotes(ctx, resp.ID, "Updated notes", 99)
|
|
require.NoError(t, err)
|
|
|
|
retrieved, err := svc.GetByID(ctx, resp.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated notes", retrieved.CsatReviewNotes)
|
|
}
|
|
|
|
func TestCsatSurveyService_GetAverageRating(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 4}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 2, ContactID: 2, MessageID: 2, Rating: 5}))
|
|
require.NoError(t, svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 3, ContactID: 3, MessageID: 3, Rating: 3}))
|
|
|
|
avg, count, err := svc.GetAverageRating(ctx, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count)
|
|
assert.InDelta(t, 4.0, avg, 0.01)
|
|
}
|
|
|
|
func TestCsatSurveyService_GetAverageRating_NoData(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
ctx := context.Background()
|
|
|
|
avg, count, err := svc.GetAverageRating(ctx, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
assert.Equal(t, 0.0, avg)
|
|
}
|
|
|
|
// --- ResponseBuilder Tests ---
|
|
|
|
func TestNewResponseBuilder(t *testing.T) {
|
|
b := NewResponseBuilder()
|
|
require.NotNil(t, b)
|
|
}
|
|
|
|
func TestResponseBuilder_BuildFromSurveyInput(t *testing.T) {
|
|
b := NewResponseBuilder()
|
|
agentID := uint(10)
|
|
|
|
resp := b.BuildFromSurveyInput(1, 2, 3, 4, &agentID, 5, "Great!")
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, uint(1), resp.AccountID)
|
|
assert.Equal(t, uint(2), resp.ConversationID)
|
|
assert.Equal(t, uint(3), resp.ContactID)
|
|
assert.Equal(t, uint(4), resp.MessageID)
|
|
assert.Equal(t, &agentID, resp.AssignedAgentID)
|
|
assert.Equal(t, 5, resp.Rating)
|
|
assert.Equal(t, "Great!", resp.FeedbackMessage)
|
|
}
|
|
|
|
func TestResponseBuilder_BuildFromSurveyInput_NilAgent(t *testing.T) {
|
|
b := NewResponseBuilder()
|
|
|
|
resp := b.BuildFromSurveyInput(1, 2, 3, 4, nil, 3, "OK")
|
|
require.NotNil(t, resp)
|
|
assert.Nil(t, resp.AssignedAgentID)
|
|
assert.Equal(t, 3, resp.Rating)
|
|
}
|
|
|
|
// --- CsatSurveyListener Tests ---
|
|
|
|
func TestNewCsatSurveyListener(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
require.NotNil(t, l)
|
|
assert.Equal(t, "csat_survey_listener", l.Name())
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnEvent_Unknown(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
event := &channel.ChannelEvent{Type: "unknown.event", Data: map[string]interface{}{}}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnConversationResolved(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
conv := &model.Conversation{Base: model.Base{ID: 1}, AccountID: 10, InboxID: 20, Status: "resolved"}
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventConversationResolved,
|
|
AccountID: 10,
|
|
InboxID: 20,
|
|
Data: map[string]interface{}{"conversation": conv},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnConversationResolved_NoConversation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventConversationResolved,
|
|
Data: map[string]interface{}{},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnMessageUpdated_InputCsat(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
agentID := uint(10)
|
|
msg := &model.Message{
|
|
Base: model.Base{ID: 5},
|
|
ConversationID: 2,
|
|
AccountID: 10,
|
|
InboxID: 20,
|
|
ContentType: "input_csat",
|
|
SenderID: &agentID,
|
|
}
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventMessageUpdated,
|
|
AccountID: 10,
|
|
ContactID: 3,
|
|
Data: map[string]interface{}{
|
|
"message": msg,
|
|
"csat_rating": 5,
|
|
"csat_feedback": "Excellent!",
|
|
},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
require.NoError(t, err)
|
|
|
|
// Verify CSAT response was created
|
|
responses, err := svc.ListByConversation(ctx, 10, 2)
|
|
require.NoError(t, err)
|
|
assert.Len(t, responses, 1)
|
|
assert.Equal(t, 5, responses[0].Rating)
|
|
assert.Equal(t, "Excellent!", responses[0].FeedbackMessage)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnMessageUpdated_InputCsat_FloatRating(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
msg := &model.Message{
|
|
Base: model.Base{ID: 5},
|
|
ConversationID: 2,
|
|
AccountID: 10,
|
|
InboxID: 20,
|
|
ContentType: "input_csat",
|
|
}
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventMessageUpdated,
|
|
AccountID: 10,
|
|
ContactID: 3,
|
|
Data: map[string]interface{}{
|
|
"message": msg,
|
|
"csat_rating": float64(4),
|
|
"csat_feedback": "Good",
|
|
},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
require.NoError(t, err)
|
|
|
|
responses, err := svc.ListByConversation(ctx, 10, 2)
|
|
require.NoError(t, err)
|
|
assert.Len(t, responses, 1)
|
|
assert.Equal(t, 4, responses[0].Rating)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnMessageUpdated_InvalidRating(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
msg := &model.Message{
|
|
Base: model.Base{ID: 5},
|
|
ConversationID: 2,
|
|
AccountID: 10,
|
|
InboxID: 20,
|
|
ContentType: "input_csat",
|
|
}
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventMessageUpdated,
|
|
AccountID: 10,
|
|
ContactID: 3,
|
|
Data: map[string]interface{}{
|
|
"message": msg,
|
|
"csat_rating": 0, // invalid
|
|
},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.NoError(t, err) // returns nil for invalid rating
|
|
|
|
// No response should be created
|
|
responses, err := svc.ListByConversation(ctx, 10, 2)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, responses)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnMessageUpdated_NonInputCsat(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
msg := &model.Message{
|
|
Base: model.Base{ID: 5},
|
|
ConversationID: 2,
|
|
AccountID: 10,
|
|
InboxID: 20,
|
|
ContentType: "text", // not input_csat
|
|
}
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventMessageUpdated,
|
|
AccountID: 10,
|
|
ContactID: 3,
|
|
Data: map[string]interface{}{"message": msg},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnMessageUpdated_NoMessage(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventMessageUpdated,
|
|
Data: map[string]interface{}{},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.NoError(t, err) // returns nil if no message
|
|
}
|
|
|
|
func TestCsatSurveyListener_OnMessageUpdated_RatingTooHigh(t *testing.T) {
|
|
db := newTestDB(t)
|
|
svc := NewCsatSurveyService(db)
|
|
builder := NewResponseBuilder()
|
|
l := NewCsatSurveyListener(db, svc, builder)
|
|
ctx := context.Background()
|
|
|
|
msg := &model.Message{
|
|
Base: model.Base{ID: 5},
|
|
ConversationID: 2,
|
|
AccountID: 10,
|
|
InboxID: 20,
|
|
ContentType: "input_csat",
|
|
}
|
|
event := &channel.ChannelEvent{
|
|
Type: channel.EventMessageUpdated,
|
|
AccountID: 10,
|
|
ContactID: 3,
|
|
Data: map[string]interface{}{
|
|
"message": msg,
|
|
"csat_rating": 6, // out of range
|
|
},
|
|
}
|
|
err := l.OnEvent(ctx, event)
|
|
assert.NoError(t, err)
|
|
}
|