380 lines
12 KiB
Go
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())
|
|
})
|
|
} |