test(search): add meilisearch live gate
This commit is contained in:
@@ -37,6 +37,10 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "config load: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := validateSearchEngineForReindex(cfg.Search); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "search config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
_ = applogger.Init(applogger.Config{Level: cfg.Log.Level, Format: cfg.Log.Format, Output: "stdout", ErrorOutput: "stderr"})
|
||||
|
||||
db, err := app.NewDatabase(&cfg.Database, cfg.Log.Level)
|
||||
@@ -95,6 +99,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func validateSearchEngineForReindex(cfg config.SearchConfig) error {
|
||||
if strings.ToLower(strings.TrimSpace(cfg.Engine)) != search.EngineMeilisearch {
|
||||
return fmt.Errorf("reindex_search requires search.engine=meilisearch; db fallback does not index documents")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTypes(raw string) map[search.SearchResultType]bool {
|
||||
selected := map[search.SearchResultType]bool{}
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gochat/gochat/internal/config"
|
||||
"github.com/gochat/gochat/internal/search"
|
||||
)
|
||||
|
||||
func TestValidateSearchEngineForReindexRequiresMeilisearch(t *testing.T) {
|
||||
require.NoError(t, validateSearchEngineForReindex(config.SearchConfig{Engine: search.EngineMeilisearch}))
|
||||
|
||||
err := validateSearchEngineForReindex(config.SearchConfig{Engine: search.EngineDB})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "requires search.engine=meilisearch")
|
||||
}
|
||||
@@ -190,6 +190,23 @@ func TestValidate_SearchDBFallbackAllowed(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidate_SearchDBFallbackRejectedInRelease(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1},
|
||||
RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60},
|
||||
Search: SearchConfig{Engine: "db"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "release mode requires meilisearch")
|
||||
}
|
||||
|
||||
func TestDatabaseConfig_DSN(t *testing.T) {
|
||||
cfg := DatabaseConfig{
|
||||
Host: "localhost",
|
||||
|
||||
@@ -80,6 +80,9 @@ func Validate(cfg *Config) error {
|
||||
if engine != "meilisearch" && engine != "db" {
|
||||
return fmt.Errorf("invalid search engine: %s (must be meilisearch or db)", cfg.Search.Engine)
|
||||
}
|
||||
if engine == "db" && cfg.Server.Mode == "release" {
|
||||
return fmt.Errorf("search.engine=db is only allowed for local development; release mode requires meilisearch")
|
||||
}
|
||||
if engine == "meilisearch" {
|
||||
if cfg.Search.Host == "" {
|
||||
return fmt.Errorf("search.host is required when search.engine=meilisearch")
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
)
|
||||
|
||||
func TestLiveMeiliSearchEngineIndexesAndSearchesChatwootShapes(t *testing.T) {
|
||||
host := os.Getenv("GOCHAT_LIVE_MEILI_HOST")
|
||||
if host == "" {
|
||||
t.Skip("set GOCHAT_LIVE_MEILI_HOST to run live Meilisearch gate")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
now := time.Now().UTC()
|
||||
token := fmt.Sprintf("gochat-live-%d", now.UnixNano())
|
||||
accountID := uint(990001)
|
||||
senderID := uint(1234)
|
||||
prefix := fmt.Sprintf("gochat_live_%d_", now.UnixNano())
|
||||
engine := NewMeiliSearchEngine(EngineConfig{
|
||||
Host: host,
|
||||
APIKey: os.Getenv("GOCHAT_LIVE_MEILI_API_KEY"),
|
||||
IndexPrefix: prefix,
|
||||
TimeoutSeconds: 5,
|
||||
})
|
||||
defer cleanupLiveIndexes(context.Background(), engine)
|
||||
|
||||
require.NoError(t, engine.Bootstrap(ctx))
|
||||
waitForLiveIndex(t, ctx, engine, ResultTypeMessage)
|
||||
waitForLiveIndex(t, ctx, engine, ResultTypeContact)
|
||||
|
||||
require.NoError(t, engine.IndexBatch(ctx, []SearchDocument{
|
||||
MessageDocument(model.Message{
|
||||
Base: model.Base{ID: 7001, CreatedAt: now, UpdatedAt: now},
|
||||
AccountID: accountID,
|
||||
InboxID: 77,
|
||||
ConversationID: 88,
|
||||
SenderType: "contact",
|
||||
SenderID: &senderID,
|
||||
MessageType: "incoming",
|
||||
ContentType: "text",
|
||||
Status: "sent",
|
||||
Content: token + " message body",
|
||||
}),
|
||||
ContactDocument(model.Contact{
|
||||
Base: model.Base{ID: 7002, CreatedAt: now, UpdatedAt: now},
|
||||
AccountID: accountID,
|
||||
Name: token + " Contact",
|
||||
Email: token + "@example.com",
|
||||
}),
|
||||
}))
|
||||
|
||||
messageFilter := &SearchFilter{Page: 1, PerPage: 15, Types: []SearchResultType{ResultTypeMessage}, SenderType: "contact", SenderID: &senderID}
|
||||
messageResp := waitForLiveSearchResult(t, ctx, engine, accountID, token, messageFilter, ResultTypeMessage)
|
||||
require.Equal(t, int64(1), messageResp.ByType[string(ResultTypeMessage)])
|
||||
require.Equal(t, uint(7001), messageResp.Results[0].ID)
|
||||
require.Equal(t, accountID, messageResp.Results[0].AccountID)
|
||||
|
||||
contactFilter := &SearchFilter{Page: 1, PerPage: 15, Types: []SearchResultType{ResultTypeContact}}
|
||||
contactResp := waitForLiveSearchResult(t, ctx, engine, accountID, token, contactFilter, ResultTypeContact)
|
||||
require.Equal(t, int64(1), contactResp.ByType[string(ResultTypeContact)])
|
||||
require.Equal(t, uint(7002), contactResp.Results[0].ID)
|
||||
require.Equal(t, accountID, contactResp.Results[0].AccountID)
|
||||
}
|
||||
|
||||
func waitForLiveIndex(t *testing.T, ctx context.Context, engine *MeiliSearchEngine, docType SearchResultType) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for {
|
||||
resp, err := engine.client.R().SetContext(ctx).Get(fmt.Sprintf("/indexes/%s", engine.indexName(docType)))
|
||||
if err == nil && resp.StatusCode() == 200 {
|
||||
return
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode())
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForLiveSearchResult(t *testing.T, ctx context.Context, engine *MeiliSearchEngine, accountID uint, query string, filter *SearchFilter, resultType SearchResultType) *SearchResponse {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
var lastErr error
|
||||
for {
|
||||
resp, err := engine.Search(ctx, accountID, query, filter)
|
||||
if err == nil && len(resp.Results) > 0 && resp.Results[0].Type == resultType {
|
||||
return resp
|
||||
}
|
||||
lastErr = err
|
||||
if time.Now().After(deadline) {
|
||||
require.NoError(t, lastErr)
|
||||
require.FailNow(t, "timed out waiting for live Meilisearch result")
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupLiveIndexes(ctx context.Context, engine *MeiliSearchEngine) {
|
||||
for _, docType := range searchableTypes(nil) {
|
||||
_, _ = engine.client.R().SetContext(ctx).Delete(fmt.Sprintf("/indexes/%s", engine.indexName(docType)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user