feat(search): align time filters
This commit is contained in:
@@ -176,7 +176,7 @@ func (r *SearchRepo) searchConversationsInternal(ctx context.Context, accountID
|
||||
q = applyConversationFilters(q, filter)
|
||||
|
||||
// Apply date range filter
|
||||
q = applyDateRangeFilter(q, filter)
|
||||
q = applyDateRangeFilter(q, filter, "last_activity_at", true)
|
||||
|
||||
// Count
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
@@ -230,7 +230,7 @@ func (r *SearchRepo) searchMessagesInternal(ctx context.Context, accountID uint,
|
||||
q = applyMessageFilters(q, filter)
|
||||
|
||||
// Apply date range filter
|
||||
q = applyDateRangeFilter(q, filter)
|
||||
q = applyDateRangeFilter(q, filter, "created_at", false)
|
||||
|
||||
// Count
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
@@ -323,7 +323,7 @@ func (r *SearchRepo) searchContactsInternal(ctx context.Context, accountID uint,
|
||||
q = applyContactFilters(q, filter)
|
||||
|
||||
// Apply date range filter
|
||||
q = applyDateRangeFilter(q, filter)
|
||||
q = applyDateRangeFilter(q, filter, "last_activity_at", true)
|
||||
|
||||
// Count
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
@@ -363,7 +363,7 @@ func (r *SearchRepo) searchCompaniesInternal(ctx context.Context, accountID uint
|
||||
}
|
||||
}
|
||||
|
||||
q = applyDateRangeFilter(q, filter)
|
||||
q = applyDateRangeFilter(q, filter, "created_at", false)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("search companies count: %w", err)
|
||||
}
|
||||
@@ -408,7 +408,7 @@ func (r *SearchRepo) searchArticlesInternal(ctx context.Context, accountID uint,
|
||||
q = applyArticleFilters(q, filter)
|
||||
|
||||
// Apply date range filter
|
||||
q = applyDateRangeFilter(q, filter)
|
||||
q = applyDateRangeFilter(q, filter, "updated_at", false)
|
||||
|
||||
// Count
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
@@ -583,18 +583,32 @@ func applyArticleFilters(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
return q
|
||||
}
|
||||
|
||||
// applyDateRangeFilter applies a date range filter on created_at.
|
||||
func applyDateRangeFilter(q *gorm.DB, filter *RepoSearchFilter) *gorm.DB {
|
||||
// applyDateRangeFilter applies Chatwoot SearchService time filters using the
|
||||
// entity-specific column selected by the caller.
|
||||
func applyDateRangeFilter(q *gorm.DB, filter *RepoSearchFilter, columnName string, unixSeconds bool) *gorm.DB {
|
||||
if filter == nil || !filter.AdvancedSearchEnabled {
|
||||
return q
|
||||
}
|
||||
if columnName == "" {
|
||||
columnName = "created_at"
|
||||
}
|
||||
|
||||
if filter.DateFrom != nil {
|
||||
q = q.Where("created_at >= ?", filter.DateFrom)
|
||||
q = q.Where(columnName+" >= ?", dateRangeValue(filter.DateFrom, unixSeconds))
|
||||
}
|
||||
if filter.DateTo != nil {
|
||||
q = q.Where("created_at <= ?", filter.DateTo)
|
||||
q = q.Where(columnName+" <= ?", dateRangeValue(filter.DateTo, unixSeconds))
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func dateRangeValue(value *time.Time, unixSeconds bool) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if unixSeconds {
|
||||
return value.Unix()
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -350,6 +350,28 @@ func TestSearchRepo_SearchConversations_WithStatusFilter(t *testing.T) {
|
||||
assert.Equal(t, "open", results[0].Status)
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchConversations_DateFilterUsesLastActivityAt(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
cutoff := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
|
||||
oldActivity := cutoff.Add(-time.Hour).Unix()
|
||||
recentActivity := cutoff.Add(time.Hour).Unix()
|
||||
oldConv := createTestConversationForSearch(t, db, account.ID, "open", "date-filter")
|
||||
recentConv := createTestConversationForSearch(t, db, account.ID, "open", "date-filter")
|
||||
require.NoError(t, db.Model(oldConv).Update("last_activity_at", oldActivity).Error)
|
||||
require.NoError(t, db.Model(recentConv).Update("last_activity_at", recentActivity).Error)
|
||||
|
||||
filter := &search.SearchFilter{Page: 1, PerPage: 25, AdvancedSearchEnabled: true, DateFrom: &cutoff}
|
||||
results, total, err := repo.SearchConversations(context.Background(), account.ID, "", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, recentConv.ID, results[0].ID)
|
||||
}
|
||||
|
||||
// ========== SearchMessages DB tests (PG only for ILIKE) ==========
|
||||
|
||||
func TestSearchRepo_SearchMessages_ContentMatch(t *testing.T) {
|
||||
@@ -463,11 +485,33 @@ func TestSearchRepo_SearchContacts_ResolvedScopeCRMV2(t *testing.T) {
|
||||
assert.Equal(t, lead.ID, results[0].ID)
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchContacts_DateFilterUsesLastActivityAt(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
cutoff := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
|
||||
oldActivity := cutoff.Add(-time.Hour).Unix()
|
||||
recentActivity := cutoff.Add(time.Hour).Unix()
|
||||
oldContact := createTestContactForSearch(t, db, account.ID, "Old Activity", "old@example.com", "")
|
||||
recentContact := createTestContactForSearch(t, db, account.ID, "Recent Activity", "recent@example.com", "")
|
||||
require.NoError(t, db.Model(oldContact).Update("last_activity_at", oldActivity).Error)
|
||||
require.NoError(t, db.Model(recentContact).Update("last_activity_at", recentActivity).Error)
|
||||
|
||||
filter := &search.SearchFilter{Page: 1, PerPage: 25, AdvancedSearchEnabled: true, DateFrom: &cutoff}
|
||||
results, total, err := repo.SearchContacts(context.Background(), account.ID, "", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, recentContact.ID, results[0].ID)
|
||||
}
|
||||
|
||||
// ========== SearchArticles DB tests (PG only for ILIKE) ==========
|
||||
|
||||
func TestSearchRepo_SearchArticles_TitleMatch(t *testing.T) {
|
||||
skipIfSQLite(t)
|
||||
db := setupTestDB(t)
|
||||
db := setupTestDB(t, &model.Article{})
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
@@ -483,7 +527,7 @@ func TestSearchRepo_SearchArticles_TitleMatch(t *testing.T) {
|
||||
|
||||
func TestSearchRepo_SearchArticles_WithStatusFilter(t *testing.T) {
|
||||
skipIfSQLite(t)
|
||||
db := setupTestDB(t)
|
||||
db := setupTestDB(t, &model.Article{})
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
@@ -499,6 +543,26 @@ func TestSearchRepo_SearchArticles_WithStatusFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchArticles_DateFilterUsesUpdatedAt(t *testing.T) {
|
||||
db := setupTestDB(t, &model.Article{})
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
cutoff := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
|
||||
oldArticle := createTestArticleForSearch(t, db, account.ID, 1, "Old Updated Article", "desc", "content", "published")
|
||||
recentArticle := createTestArticleForSearch(t, db, account.ID, 1, "Recent Updated Article", "desc", "content", "published")
|
||||
require.NoError(t, db.Model(oldArticle).UpdateColumn("updated_at", cutoff.Add(-time.Hour)).Error)
|
||||
require.NoError(t, db.Model(recentArticle).UpdateColumn("updated_at", cutoff.Add(time.Hour)).Error)
|
||||
|
||||
filter := &search.SearchFilter{Page: 1, PerPage: 25, AdvancedSearchEnabled: true, DateFrom: &cutoff}
|
||||
results, total, err := repo.SearchArticles(context.Background(), account.ID, "", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, recentArticle.ID, results[0].ID)
|
||||
}
|
||||
|
||||
// ========== Interface satisfaction test ==========
|
||||
|
||||
func TestSearchRepo_SatisfiesSearchRepoInterface(t *testing.T) {
|
||||
|
||||
+25
-15
@@ -65,6 +65,7 @@ type SearchDocument struct {
|
||||
Locale string `json:"locale,omitempty"`
|
||||
CreatedAtTS int64 `json:"created_at_ts"`
|
||||
UpdatedAtTS int64 `json:"updated_at_ts"`
|
||||
LastActivityAtTS int64 `json:"last_activity_at_ts,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
@@ -132,24 +133,32 @@ func timestamp(t time.Time) int64 {
|
||||
return t.Unix()
|
||||
}
|
||||
|
||||
func timestampPtr(value *int64) int64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func ConversationDocument(conv model.Conversation) SearchDocument {
|
||||
inboxID := conv.InboxID
|
||||
doc := SearchDocument{
|
||||
ID: conv.ID,
|
||||
Type: ResultTypeConversation,
|
||||
AccountID: conv.AccountID,
|
||||
Title: fmt.Sprintf("Conversation #%d", conv.ID),
|
||||
Content: conv.Labels,
|
||||
Status: conv.Status,
|
||||
Priority: conv.Priority,
|
||||
Labels: splitLabels(conv.Labels),
|
||||
AssigneeID: conv.AssigneeID,
|
||||
TeamID: conv.TeamID,
|
||||
InboxID: &inboxID,
|
||||
ContactID: &conv.ContactID,
|
||||
CreatedAtTS: timestamp(conv.CreatedAt),
|
||||
UpdatedAtTS: timestamp(conv.UpdatedAt),
|
||||
Data: map[string]interface{}{"conversation": conv},
|
||||
ID: conv.ID,
|
||||
Type: ResultTypeConversation,
|
||||
AccountID: conv.AccountID,
|
||||
Title: fmt.Sprintf("Conversation #%d", conv.ID),
|
||||
Content: conv.Labels,
|
||||
Status: conv.Status,
|
||||
Priority: conv.Priority,
|
||||
Labels: splitLabels(conv.Labels),
|
||||
AssigneeID: conv.AssigneeID,
|
||||
TeamID: conv.TeamID,
|
||||
InboxID: &inboxID,
|
||||
ContactID: &conv.ContactID,
|
||||
CreatedAtTS: timestamp(conv.CreatedAt),
|
||||
UpdatedAtTS: timestamp(conv.UpdatedAt),
|
||||
LastActivityAtTS: timestampPtr(conv.LastActivityAt),
|
||||
Data: map[string]interface{}{"conversation": conv},
|
||||
}
|
||||
doc.Snippet = conversationSnippet(&conv, "")
|
||||
doc.ensureUID()
|
||||
@@ -196,6 +205,7 @@ func ContactDocument(contact model.Contact) SearchDocument {
|
||||
ContactHasDetails: content != "",
|
||||
CreatedAtTS: timestamp(contact.CreatedAt),
|
||||
UpdatedAtTS: timestamp(contact.UpdatedAt),
|
||||
LastActivityAtTS: timestampPtr(contact.LastActivityAt),
|
||||
Data: map[string]interface{}{"contact": contact},
|
||||
}
|
||||
doc.ensureUID()
|
||||
|
||||
@@ -174,8 +174,8 @@ func (e *MeiliSearchEngine) ensureIndex(ctx context.Context, docType SearchResul
|
||||
func (e *MeiliSearchEngine) applySettings(ctx context.Context, docType SearchResultType) error {
|
||||
settings := map[string]interface{}{
|
||||
"searchableAttributes": []string{"title", "content", "snippet", "status", "priority", "labels", "locale"},
|
||||
"filterableAttributes": []string{"account_id", "type", "status", "priority", "message_type", "sender_type", "sender_id", "content_type", "private", "contact_source", "contact_type", "contact_has_details", "labels", "assignee_id", "team_id", "inbox_id", "contact_id", "conversation_id", "portal_id", "locale", "created_at_ts", "updated_at_ts"},
|
||||
"sortableAttributes": []string{"created_at_ts", "updated_at_ts", "id"},
|
||||
"filterableAttributes": []string{"account_id", "type", "status", "priority", "message_type", "sender_type", "sender_id", "content_type", "private", "contact_source", "contact_type", "contact_has_details", "labels", "assignee_id", "team_id", "inbox_id", "contact_id", "conversation_id", "portal_id", "locale", "created_at_ts", "updated_at_ts", "last_activity_at_ts"},
|
||||
"sortableAttributes": []string{"created_at_ts", "updated_at_ts", "last_activity_at_ts", "id"},
|
||||
}
|
||||
resp, err := e.client.R().
|
||||
SetContext(ctx).
|
||||
@@ -249,15 +249,27 @@ func (e *MeiliSearchEngine) filterExpression(accountID uint, docType SearchResul
|
||||
parts = append(parts, fmt.Sprintf("locale = %q", filter.ArticleLocale))
|
||||
}
|
||||
}
|
||||
dateField := dateFilterField(docType)
|
||||
if filter.AdvancedSearchEnabled && filter.DateFrom != nil {
|
||||
parts = append(parts, fmt.Sprintf("created_at_ts >= %d", filter.DateFrom.Unix()))
|
||||
parts = append(parts, fmt.Sprintf("%s >= %d", dateField, filter.DateFrom.Unix()))
|
||||
}
|
||||
if filter.AdvancedSearchEnabled && filter.DateTo != nil {
|
||||
parts = append(parts, fmt.Sprintf("created_at_ts <= %d", filter.DateTo.Unix()))
|
||||
parts = append(parts, fmt.Sprintf("%s <= %d", dateField, filter.DateTo.Unix()))
|
||||
}
|
||||
return strings.Join(parts, " AND ")
|
||||
}
|
||||
|
||||
func dateFilterField(docType SearchResultType) string {
|
||||
switch docType {
|
||||
case ResultTypeConversation, ResultTypeContact:
|
||||
return "last_activity_at_ts"
|
||||
case ResultTypeArticle, ResultTypeHelpCenter:
|
||||
return "updated_at_ts"
|
||||
default:
|
||||
return "created_at_ts"
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MeiliSearchEngine) indexName(docType SearchResultType) string {
|
||||
return e.indexPrefix + strings.ReplaceAll(string(docType), "_", "_") + "s"
|
||||
}
|
||||
|
||||
@@ -43,15 +43,18 @@ func TestNewSearchEngine_DBFallbackRequiresRepo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDocumentBuildersSetStableUIDAndType(t *testing.T) {
|
||||
lastActivity := int64(1772884800)
|
||||
conv := makeConversation(12, 3, "open", "billing,urgent")
|
||||
conv.InboxID = 7
|
||||
conv.ContactID = 9
|
||||
conv.LastActivityAt = &lastActivity
|
||||
doc := ConversationDocument(conv)
|
||||
|
||||
assert.Equal(t, "3:conversation:12", doc.UID)
|
||||
assert.Equal(t, ResultTypeConversation, doc.Type)
|
||||
assert.Equal(t, uint(3), doc.AccountID)
|
||||
assert.Equal(t, []string{"billing", "urgent"}, doc.Labels)
|
||||
assert.Equal(t, lastActivity, doc.LastActivityAtTS)
|
||||
}
|
||||
|
||||
func TestContactDocumentSetsResolvedScopeFields(t *testing.T) {
|
||||
@@ -203,6 +206,28 @@ func TestMeiliSearchEngine_SearchSendsMessageRecentBaseFilter(t *testing.T) {
|
||||
assert.Equal(t, "account_id = 42 AND created_at_ts >= 1772884800", requestBody["filter"])
|
||||
}
|
||||
|
||||
func TestMeiliSearchEngine_SearchUsesReferenceTimeFilterFields(t *testing.T) {
|
||||
seen := map[string]string{}
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
var requestBody map[string]interface{}
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&requestBody))
|
||||
seen[r.URL.Path] = requestBody["filter"].(string)
|
||||
return jsonResponse(http.StatusOK, `{"hits":[],"estimatedTotalHits":0}`), nil
|
||||
})
|
||||
|
||||
engine := NewMeiliSearchEngine(EngineConfig{Host: "http://meili.test", IndexPrefix: "gochat_"})
|
||||
engine.client.SetTransport(transport)
|
||||
from := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
|
||||
filter := &SearchFilter{Page: 1, PerPage: 10, Types: []SearchResultType{ResultTypeConversation, ResultTypeContact, ResultTypeArticle, ResultTypeMessage}, AdvancedSearchEnabled: true, DateFrom: &from}
|
||||
_, err := engine.Search(context.Background(), 42, "hello", filter)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "account_id = 42 AND last_activity_at_ts >= 1772884800", seen["/indexes/gochat_conversations/search"])
|
||||
assert.Equal(t, "account_id = 42 AND last_activity_at_ts >= 1772884800", seen["/indexes/gochat_contacts/search"])
|
||||
assert.Equal(t, "account_id = 42 AND updated_at_ts >= 1772884800", seen["/indexes/gochat_articles/search"])
|
||||
assert.Equal(t, "account_id = 42 AND created_at_ts >= 1772884800", seen["/indexes/gochat_messages/search"])
|
||||
}
|
||||
|
||||
func TestMeiliSearchEngine_SearchIgnoresInaccessibleInboxParam(t *testing.T) {
|
||||
var requestBody map[string]interface{}
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
|
||||
@@ -387,9 +387,26 @@ func (s *SearchService) withAdvancedSearchFeature(ctx context.Context, accountID
|
||||
if s != nil && s.featureResolver != nil {
|
||||
clone.AdvancedSearchEnabled = s.featureResolver.AccountFeatureEnabled(ctx, accountID, "advanced_search")
|
||||
}
|
||||
if clone.AdvancedSearchEnabled {
|
||||
capAdvancedSearchDateRange(clone)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func capAdvancedSearchDateRange(filter *SearchFilter) {
|
||||
now := searchNow()
|
||||
maxLookback := now.Add(-90 * 24 * time.Hour)
|
||||
maxFuture := now.Add(90 * 24 * time.Hour)
|
||||
if filter.DateFrom != nil && filter.DateFrom.Before(maxLookback) {
|
||||
capped := maxLookback
|
||||
filter.DateFrom = &capped
|
||||
}
|
||||
if filter.DateTo != nil && filter.DateTo.After(maxFuture) {
|
||||
capped := maxFuture
|
||||
filter.DateTo = &capped
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SearchService) withContactResolvedScope(ctx context.Context, accountID uint, filter *SearchFilter) *SearchFilter {
|
||||
clone := cloneSearchFilter(filter)
|
||||
clone.ContactResolvedScope = true
|
||||
|
||||
@@ -18,6 +18,7 @@ type mockSearchRepo struct {
|
||||
conversations []model.Conversation
|
||||
convCount int64
|
||||
convErr error
|
||||
convFilter *SearchFilter
|
||||
|
||||
messages []model.Message
|
||||
msgCount int64
|
||||
@@ -33,9 +34,10 @@ type mockSearchRepo struct {
|
||||
companyCount int64
|
||||
companyErr error
|
||||
|
||||
articles []model.Article
|
||||
articleCount int64
|
||||
articleErr error
|
||||
articles []model.Article
|
||||
articleCount int64
|
||||
articleErr error
|
||||
articleFilter *SearchFilter
|
||||
|
||||
features map[string]bool
|
||||
access SearchAccess
|
||||
@@ -43,6 +45,7 @@ type mockSearchRepo struct {
|
||||
}
|
||||
|
||||
func (m *mockSearchRepo) SearchConversations(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Conversation, int64, error) {
|
||||
m.convFilter = filter
|
||||
return m.conversations, m.convCount, m.convErr
|
||||
}
|
||||
|
||||
@@ -69,6 +72,7 @@ func (m *mockSearchRepo) SearchCompanies(ctx context.Context, accountID uint, qu
|
||||
}
|
||||
|
||||
func (m *mockSearchRepo) SearchArticles(ctx context.Context, accountID uint, query string, filter *SearchFilter) ([]model.Article, int64, error) {
|
||||
m.articleFilter = filter
|
||||
return m.articles, m.articleCount, m.articleErr
|
||||
}
|
||||
|
||||
@@ -378,6 +382,32 @@ func TestSearchMessages_AppliesAdvancedSearchFeatureFlag(t *testing.T) {
|
||||
assert.True(t, repo.msgFilter.AdvancedSearchEnabled)
|
||||
}
|
||||
|
||||
func TestSearchMessages_CapsAdvancedSearchDateRange(t *testing.T) {
|
||||
fixedNow := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
|
||||
originalNow := searchNow
|
||||
searchNow = func() time.Time { return fixedNow }
|
||||
t.Cleanup(func() { searchNow = originalNow })
|
||||
repo := &mockSearchRepo{
|
||||
messages: []model.Message{makeMessage(10, 1, "Hello test message", "incoming")},
|
||||
msgCount: 1,
|
||||
features: map[string]bool{"advanced_search": true},
|
||||
}
|
||||
svc := NewSearchService(repo)
|
||||
since := fixedNow.Add(-120 * 24 * time.Hour)
|
||||
until := fixedNow.Add(120 * 24 * time.Hour)
|
||||
|
||||
_, _, err := svc.SearchMessages(context.Background(), 1, "test", &SearchFilter{Page: 1, PerPage: 25, DateFrom: &since, DateTo: &until})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, repo.msgFilter)
|
||||
if assert.NotNil(t, repo.msgFilter.DateFrom) {
|
||||
assert.Equal(t, fixedNow.Add(-90*24*time.Hour), *repo.msgFilter.DateFrom)
|
||||
}
|
||||
if assert.NotNil(t, repo.msgFilter.DateTo) {
|
||||
assert.Equal(t, fixedNow.Add(90*24*time.Hour), *repo.msgFilter.DateTo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContacts_ConvenienceMethod(t *testing.T) {
|
||||
repo := &mockSearchRepo{}
|
||||
repo.contacts = []model.Contact{makeContact(5, 1, "Alice", "alice@test.com", "")}
|
||||
|
||||
Reference in New Issue
Block a user