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))) } }