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() svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 3}) svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 2, ContactID: 2, MessageID: 2, Rating: 5}) 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) 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) svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 5, AssignedAgentID: &agent1}) 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++ { 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() svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 10, ContactID: 1, MessageID: 1, Rating: 5}) svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 10, ContactID: 1, MessageID: 2, Rating: 4}) 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() svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 1, ContactID: 1, MessageID: 1, Rating: 4}) svc.Create(ctx, &CsatSurveyResponse{AccountID: 1, ConversationID: 2, ContactID: 2, MessageID: 2, Rating: 5}) 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) }