Files
Rogeeandrogee 2b182f9956 H-300: wire Captain Skills into Web runtime (#48)
* 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>
2026-08-19 07:08:14 +08:00

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)
}