Files
gochat/internal/service/search_indexer_worker_test.go
T

146 lines
5.4 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/worker"
)
type recordingDurableSearchIndexer struct {
indexedContacts []uint
deletedContacts []uint
err error
}
func (r *recordingDurableSearchIndexer) IndexConversation(ctx context.Context, conversation *model.Conversation) error {
return r.err
}
func (r *recordingDurableSearchIndexer) DeleteConversation(ctx context.Context, accountID uint, id uint) error {
return r.err
}
func (r *recordingDurableSearchIndexer) IndexMessage(ctx context.Context, message *model.Message) error {
return r.err
}
func (r *recordingDurableSearchIndexer) DeleteMessage(ctx context.Context, accountID uint, id uint) error {
return r.err
}
func (r *recordingDurableSearchIndexer) IndexContact(ctx context.Context, contact *model.Contact) error {
r.indexedContacts = append(r.indexedContacts, contact.ID)
return r.err
}
func (r *recordingDurableSearchIndexer) DeleteContact(ctx context.Context, accountID uint, id uint) error {
r.deletedContacts = append(r.deletedContacts, id)
return r.err
}
func (r *recordingDurableSearchIndexer) IndexCompany(ctx context.Context, company *model.Company) error {
return r.err
}
func (r *recordingDurableSearchIndexer) DeleteCompany(ctx context.Context, accountID uint, id uint) error {
return r.err
}
func (r *recordingDurableSearchIndexer) IndexArticle(ctx context.Context, article *model.Article) error {
return r.err
}
func (r *recordingDurableSearchIndexer) DeleteArticle(ctx context.Context, accountID uint, id uint) error {
return r.err
}
func TestDurableSearchIndexerQueuesAndReplaysContactIndex(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "Index Me", Email: "index@example.com"}
if err := db.Create(contact).Error; err != nil {
t.Fatalf("create contact: %v", err)
}
delegate := &recordingDurableSearchIndexer{}
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 5, 14, 0, 0, 0, time.UTC) }))
indexer := NewDurableSearchIndexer(db, wp, delegate)
if err := indexer.IndexContact(context.Background(), contact); err != nil {
t.Fatalf("queue contact index: %v", err)
}
if len(delegate.indexedContacts) != 0 {
t.Fatalf("contact indexed synchronously before worker replay")
}
var count int64
if err := db.Model(&model.BackgroundJob{}).Where("job_type = ? AND queue = ? AND status = ?", TaskTypeSearchIndex, "search", model.BackgroundJobStatusQueued).Count(&count).Error; err != nil {
t.Fatalf("count search jobs: %v", err)
}
if count != 1 {
t.Fatalf("expected one queued search job, got %d", count)
}
processed, err := wp.ProcessOne(context.Background())
if err != nil || !processed {
t.Fatalf("process search job: processed=%v err=%v", processed, err)
}
if len(delegate.indexedContacts) != 1 || delegate.indexedContacts[0] != contact.ID {
t.Fatalf("expected durable contact index replay, got %#v", delegate.indexedContacts)
}
}
func TestDurableSearchIndexerRetriesDelegateFailure(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "Retry Me"}
if err := db.Create(contact).Error; err != nil {
t.Fatalf("create contact: %v", err)
}
delegate := &recordingDurableSearchIndexer{err: errors.New("meili unavailable")}
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 5, 14, 15, 0, 0, time.UTC) }), worker.WithBackoff(func(attempt int) time.Duration { return time.Minute }))
indexer := NewDurableSearchIndexer(db, wp, delegate)
if err := indexer.IndexContact(context.Background(), contact); err != nil {
t.Fatalf("queue contact index: %v", err)
}
processed, err := wp.ProcessOne(context.Background())
if err == nil || !processed {
t.Fatalf("expected retryable worker error, processed=%v err=%v", processed, err)
}
var job model.BackgroundJob
if err := db.Where("job_type = ?", TaskTypeSearchIndex).First(&job).Error; err != nil {
t.Fatalf("load search job: %v", err)
}
if job.Status != model.BackgroundJobStatusRetrying || job.LastError == "" {
t.Fatalf("expected retrying search job with error, got status=%s last_error=%q", job.Status, job.LastError)
}
}
func TestDurableSearchIndexerDeletesWhenIndexedRecordDisappears(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "Delete Before Replay"}
if err := db.Create(contact).Error; err != nil {
t.Fatalf("create contact: %v", err)
}
delegate := &recordingDurableSearchIndexer{}
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 5, 14, 30, 0, 0, time.UTC) }))
indexer := NewDurableSearchIndexer(db, wp, delegate)
if err := indexer.IndexContact(context.Background(), contact); err != nil {
t.Fatalf("queue contact index: %v", err)
}
if err := db.Delete(contact).Error; err != nil {
t.Fatalf("delete contact before replay: %v", err)
}
processed, err := wp.ProcessOne(context.Background())
if err != nil || !processed {
t.Fatalf("process search job: processed=%v err=%v", processed, err)
}
if len(delegate.deletedContacts) != 1 || delegate.deletedContacts[0] != contact.ID {
t.Fatalf("expected missing record to delete search document, got %#v", delegate.deletedContacts)
}
}