Files
gochat/internal/repository/search_repo_test.go
T
2026-06-04 15:44:48 +08:00

380 lines
12 KiB
Go

package repository
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
)
// ========== Helper functions ==========
func createTestAccountForSearch(t *testing.T, db *gorm.DB) *model.Account {
t.Helper()
account := &model.Account{Name: "SearchTestAccount", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
return account
}
func createTestConversationForSearch(t *testing.T, db *gorm.DB, accountID uint, status, labels string) *model.Conversation {
t.Helper()
conv := &model.Conversation{
AccountID: accountID,
Status: status,
Labels: labels,
}
require.NoError(t, db.Create(conv).Error)
return conv
}
func createTestMessageForSearch(t *testing.T, db *gorm.DB, accountID, conversationID uint, content, msgType string) *model.Message {
t.Helper()
msg := &model.Message{
AccountID: accountID,
ConversationID: conversationID,
Content: content,
MessageType: msgType,
}
require.NoError(t, db.Create(msg).Error)
return msg
}
func createTestContactForSearch(t *testing.T, db *gorm.DB, accountID uint, name, email, phone string) *model.Contact {
t.Helper()
contact := &model.Contact{
AccountID: accountID,
Name: name,
Email: email,
PhoneNumber: phone,
}
require.NoError(t, db.Create(contact).Error)
return contact
}
func createTestArticleForSearch(t *testing.T, db *gorm.DB, accountID, portalID uint, title, description, content, status string) *model.Article {
t.Helper()
article := &model.Article{
AccountID: accountID,
PortalID: portalID,
Title: title,
Slug: strings.ToLower(strings.ReplaceAll(title, " ", "-")),
Description: description,
Content: content,
Status: status,
}
require.NoError(t, db.Create(article).Error)
return article
}
// ========== RepoSearchFilter unit tests ==========
func TestRepoSearchFilter_Offset(t *testing.T) {
f := &RepoSearchFilter{Page: 2, PerPage: 10}
assert.Equal(t, 10, f.Offset())
f2 := &RepoSearchFilter{Page: 1, PerPage: 25}
assert.Equal(t, 0, f2.Offset())
}
func TestRepoSearchFilter_OrderClause(t *testing.T) {
t.Run("defaults", func(t *testing.T) {
f := &RepoSearchFilter{}
assert.Equal(t, "created_at desc", f.OrderClause())
})
t.Run("explicit sort", func(t *testing.T) {
f := &RepoSearchFilter{SortBy: "updated_at", SortOrder: "asc"}
assert.Equal(t, "updated_at asc", f.OrderClause())
})
t.Run("priority sort", func(t *testing.T) {
f := &RepoSearchFilter{SortBy: "priority", SortOrder: "desc"}
assert.Equal(t, "priority desc", f.OrderClause())
})
}
// ========== searchFilterToRepo conversion tests ==========
func TestSearchFilterToRepo_NilFilter(t *testing.T) {
result := searchFilterToRepo(nil)
assert.Equal(t, 1, result.Page)
assert.Equal(t, 25, result.PerPage)
assert.Equal(t, "created_at", result.SortBy)
assert.Equal(t, "desc", result.SortOrder)
}
func TestSearchFilterToRepo_PreservesFields(t *testing.T) {
assigneeID := uint(5)
teamID := uint(10)
private := true
sf := &search.SearchFilter{
Status: []string{"open"},
Priority: []string{"urgent"},
AssigneeID: &assigneeID,
TeamID: &teamID,
SortBy: "updated_at",
SortOrder: "asc",
Page: 3,
PerPage: 15,
Private: &private,
ContactSource: "api",
MessageType: "incoming",
}
result := searchFilterToRepo(sf)
assert.Equal(t, []string{"open"}, result.Status)
assert.Equal(t, []string{"urgent"}, result.Priority)
assert.Equal(t, uint(5), *result.AssigneeID)
assert.Equal(t, uint(10), *result.TeamID)
assert.Equal(t, "updated_at", result.SortBy)
assert.Equal(t, "asc", result.SortOrder)
assert.Equal(t, 3, result.Page)
assert.Equal(t, 15, result.PerPage)
assert.True(t, *result.Private)
assert.Equal(t, "api", result.ContactSource)
assert.Equal(t, "incoming", result.MessageType)
}
// ========== SearchConversations DB tests (PG only for ILIKE) ==========
func TestSearchRepo_SearchConversations_EmptyQuery(t *testing.T) {
skipIfSQLite(t) // ILIKE requires PostgreSQL
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
conv := createTestConversationForSearch(t, db, account.ID, "open", "support,billing")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchConversations(context.Background(), account.ID, "", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
found := false
for _, c := range results {
if c.ID == conv.ID {
found = true
}
}
assert.True(t, found, "expected conversation %d in results", conv.ID)
}
func TestSearchRepo_SearchConversations_LabelMatch(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
_ = createTestConversationForSearch(t, db, account.ID, "open", "support,billing")
_ = createTestConversationForSearch(t, db, account.ID, "resolved", "sales,marketing")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchConversations(context.Background(), account.ID, "billing", filter)
assert.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, results, 1)
}
func TestSearchRepo_SearchConversations_WithStatusFilter(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
_ = createTestConversationForSearch(t, db, account.ID, "open", "")
_ = createTestConversationForSearch(t, db, account.ID, "resolved", "")
filter := &search.SearchFilter{Status: []string{"open"}, Page: 1, PerPage: 25}
results, total, err := repo.SearchConversations(context.Background(), account.ID, "", filter)
assert.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, results, 1)
assert.Equal(t, "open", results[0].Status)
}
// ========== SearchMessages DB tests (PG only for ILIKE) ==========
func TestSearchRepo_SearchMessages_ContentMatch(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
conv := createTestConversationForSearch(t, db, account.ID, "open", "")
_ = createTestMessageForSearch(t, db, account.ID, conv.ID, "hello world", "incoming")
_ = createTestMessageForSearch(t, db, account.ID, conv.ID, "goodbye moon", "incoming")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchMessages(context.Background(), account.ID, "world", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
assert.NotEmpty(t, results)
}
func TestSearchRepo_SearchMessages_EmptyQuery(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
conv := createTestConversationForSearch(t, db, account.ID, "open", "")
msg := createTestMessageForSearch(t, db, account.ID, conv.ID, "hello world", "incoming")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchMessages(context.Background(), account.ID, "", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
found := false
for _, m := range results {
if m.ID == msg.ID {
found = true
}
}
assert.True(t, found, "expected message %d in results", msg.ID)
}
// ========== SearchContacts DB tests (PG only for ILIKE) ==========
func TestSearchRepo_SearchContacts_NameMatch(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
_ = createTestContactForSearch(t, db, account.ID, "Alice Smith", "alice@example.com", "")
_ = createTestContactForSearch(t, db, account.ID, "Bob Jones", "bob@example.com", "")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchContacts(context.Background(), account.ID, "Alice", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
assert.NotEmpty(t, results)
}
func TestSearchRepo_SearchContacts_EmailMatch(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
_ = createTestContactForSearch(t, db, account.ID, "Alice Smith", "alice@example.com", "")
_ = createTestContactForSearch(t, db, account.ID, "Bob Jones", "bob@example.com", "")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchContacts(context.Background(), account.ID, "bob@example", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
assert.NotEmpty(t, results)
}
// ========== SearchArticles DB tests (PG only for ILIKE) ==========
func TestSearchRepo_SearchArticles_TitleMatch(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
_ = createTestArticleForSearch(t, db, account.ID, 1, "Getting Started Guide", "How to get started", "Full guide content here", "published")
_ = createTestArticleForSearch(t, db, account.ID, 1, "FAQ", "Common questions", "FAQ content", "published")
filter := &search.SearchFilter{Page: 1, PerPage: 25}
results, total, err := repo.SearchArticles(context.Background(), account.ID, "Guide", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
assert.NotEmpty(t, results)
}
func TestSearchRepo_SearchArticles_WithStatusFilter(t *testing.T) {
skipIfSQLite(t)
db := setupTestDB(t)
repo := NewSearchRepo(db)
account := createTestAccountForSearch(t, db)
_ = createTestArticleForSearch(t, db, account.ID, 1, "Published Article", "desc", "content", "published")
_ = createTestArticleForSearch(t, db, account.ID, 1, "Draft Article", "desc2", "content2", "draft")
filter := &search.SearchFilter{ArticleStatus: "published", Page: 1, PerPage: 25}
results, total, err := repo.SearchArticles(context.Background(), account.ID, "", filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
for _, a := range results {
assert.Equal(t, "published", a.Status)
}
}
// ========== Interface satisfaction test ==========
func TestSearchRepo_SatisfiesSearchRepoInterface(t *testing.T) {
// Verify SearchRepo satisfies SearchRepoInterface at compile time
var _ search.SearchRepoInterface = (*SearchRepo)(nil)
}
// ========== SearchMode / SearchFilter tests ==========
func TestSearchFilter_SearchMode_Default(t *testing.T) {
sf := &search.SearchFilter{Page: 1, PerPage: 25}
// SearchMode zero value is "" when struct is created directly.
// Default SearchModeILike is only set via ParseSearchFilter or explicit assignment.
assert.Equal(t, search.SearchMode(""), sf.SearchMode)
}
func TestSearchFilter_SearchMode_Trigram(t *testing.T) {
sf := &search.SearchFilter{SearchMode: search.SearchModeTrigram, Page: 1, PerPage: 25}
assert.True(t, sf.IsTrigram())
}
func TestSearchFilterToRepo_SearchMode_Trigram(t *testing.T) {
sf := &search.SearchFilter{SearchMode: search.SearchModeTrigram, Page: 1, PerPage: 25}
result := searchFilterToRepo(sf)
assert.Equal(t, search.SearchModeTrigram, result.SearchMode)
assert.True(t, result.IsTrigram())
}
func TestSearchFilterToRepo_SearchMode_Default(t *testing.T) {
sf := &search.SearchFilter{Page: 1, PerPage: 25} // SearchMode defaults to ilike
result := searchFilterToRepo(sf)
assert.Equal(t, search.SearchModeILike, result.SearchMode)
assert.False(t, result.IsTrigram())
}
func TestRepoSearchFilter_IsTrigram(t *testing.T) {
t.Run("ilike mode", func(t *testing.T) {
f := &RepoSearchFilter{SearchMode: search.SearchModeILike}
assert.False(t, f.IsTrigram())
})
t.Run("trigram mode", func(t *testing.T) {
f := &RepoSearchFilter{SearchMode: search.SearchModeTrigram}
assert.True(t, f.IsTrigram())
})
t.Run("empty mode defaults to ilike", func(t *testing.T) {
f := &RepoSearchFilter{}
assert.False(t, f.IsTrigram())
})
}
// ========== ParseSearchFilter search_mode tests ==========
func TestSearchModeConstants(t *testing.T) {
assert.Equal(t, search.SearchMode("ilike"), search.SearchModeILike)
assert.Equal(t, search.SearchMode("trigram"), search.SearchModeTrigram)
}
func TestSearchFilter_IsTrigram_Unit(t *testing.T) {
t.Run("empty filter defaults to ilike (not trigram)", func(t *testing.T) {
sf := &search.SearchFilter{}
assert.False(t, sf.IsTrigram())
})
t.Run("explicit ilike mode", func(t *testing.T) {
sf := &search.SearchFilter{SearchMode: search.SearchModeILike}
assert.False(t, sf.IsTrigram())
})
t.Run("explicit trigram mode", func(t *testing.T) {
sf := &search.SearchFilter{SearchMode: search.SearchModeTrigram}
assert.True(t, sf.IsTrigram())
})
}