package v1 import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/search" ) // mockSearchRepo implements search.SearchRepoInterface for handler tests. type mockSearchRepo struct { conversations []model.Conversation convTotal int64 convErr error messages []model.Message msgTotal int64 msgErr error contacts []model.Contact contactTotal int64 contactErr error articles []model.Article articleTotal int64 articleErr error } func (m *mockSearchRepo) SearchConversations(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Conversation, int64, error) { return m.conversations, m.convTotal, m.convErr } func (m *mockSearchRepo) SearchMessages(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Message, int64, error) { return m.messages, m.msgTotal, m.msgErr } func (m *mockSearchRepo) SearchContacts(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Contact, int64, error) { return m.contacts, m.contactTotal, m.contactErr } func (m *mockSearchRepo) SearchArticles(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Article, int64, error) { return m.articles, m.articleTotal, m.articleErr } // Helper to create model objects without embedded Base in literals. func makeConversation(accountID uint, status string) model.Conversation { return model.Conversation{AccountID: accountID, Status: status} } func makeMessage(accountID, conversationID uint, content string) model.Message { return model.Message{AccountID: accountID, ConversationID: conversationID, Content: content} } func makeContact(accountID uint, name string) model.Contact { return model.Contact{AccountID: accountID, Name: name} } func makeArticle(accountID, portalID uint, title, desc, content, status string) model.Article { return model.Article{AccountID: accountID, PortalID: portalID, Title: title, Description: desc, Content: content, Status: status} } // setupSearchHandlerRouter creates a test router with search handler routes. func setupSearchHandlerRouter(h *SearchHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() api := r.Group("/api/v1/accounts/:account_id/search") { api.GET("", h.GlobalSearch) api.GET("/conversations", h.SearchConversations) api.GET("/messages", h.SearchMessages) api.GET("/contacts", h.SearchContacts) api.GET("/articles", h.SearchArticles) } return r } // ========== GlobalSearch handler tests ========== func TestSearchHandler_GlobalSearch_Success(t *testing.T) { repo := &mockSearchRepo{ conversations: []model.Conversation{makeConversation(1, "open")}, convTotal: 1, messages: []model.Message{makeMessage(1, 1, "hello")}, msgTotal: 1, contacts: []model.Contact{makeContact(1, "Alice")}, contactTotal: 1, articles: []model.Article{makeArticle(1, 1, "FAQ", "desc", "content", "published")}, articleTotal: 1, } svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search?q=test&page=1&per_page=10", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var body map[string]interface{} assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, true, body["success"]) } func TestSearchHandler_GlobalSearch_InvalidAccountID(t *testing.T) { repo := &mockSearchRepo{} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/abc/search?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSearchHandler_GlobalSearch_WithFilterParams(t *testing.T) { repo := &mockSearchRepo{ conversations: []model.Conversation{makeConversation(1, "open")}, convTotal: 1, messages: []model.Message{makeMessage(1, 1, "msg")}, msgTotal: 1, contacts: []model.Contact{makeContact(1, "Bob")}, contactTotal: 1, articles: []model.Article{makeArticle(1, 1, "FAQ", "desc", "content", "published")}, articleTotal: 1, } svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search?q=test&status=open&page=2&per_page=5&sort_by=updated_at&sort_order=asc", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var body map[string]interface{} assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, true, body["success"]) meta := body["meta"].(map[string]interface{}) assert.Equal(t, float64(2), meta["page"]) assert.Equal(t, float64(5), meta["per_page"]) } // ========== SearchConversations handler tests ========== func TestSearchHandler_SearchConversations_Success(t *testing.T) { repo := &mockSearchRepo{ conversations: []model.Conversation{makeConversation(1, "open")}, convTotal: 1, } svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/conversations?q=billing&page=1&per_page=10", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var body map[string]interface{} assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, true, body["success"]) data := body["data"].(map[string]interface{}) results := data["results"] assert.NotNil(t, results) } func TestSearchHandler_SearchConversations_InvalidAccountID(t *testing.T) { repo := &mockSearchRepo{} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/invalid/search/conversations?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSearchHandler_SearchConversations_ServiceError(t *testing.T) { repo := &mockSearchRepo{convErr: assert.AnError} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/conversations?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) } // ========== SearchMessages handler tests ========== func TestSearchHandler_SearchMessages_Success(t *testing.T) { repo := &mockSearchRepo{ messages: []model.Message{makeMessage(1, 1, "hello world")}, msgTotal: 1, } svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/messages?q=hello&page=1&per_page=10", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var body map[string]interface{} assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, true, body["success"]) data := body["data"].(map[string]interface{}) results := data["results"] assert.NotNil(t, results) } func TestSearchHandler_SearchMessages_InvalidAccountID(t *testing.T) { repo := &mockSearchRepo{} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/abc/search/messages?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSearchHandler_SearchMessages_ServiceError(t *testing.T) { repo := &mockSearchRepo{msgErr: assert.AnError} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/messages?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) } // ========== SearchContacts handler tests ========== func TestSearchHandler_SearchContacts_Success(t *testing.T) { repo := &mockSearchRepo{ contacts: []model.Contact{makeContact(1, "Alice")}, contactTotal: 1, } svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/contacts?q=Alice&page=1&per_page=10", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var body map[string]interface{} assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, true, body["success"]) data := body["data"].(map[string]interface{}) results := data["results"] assert.NotNil(t, results) } func TestSearchHandler_SearchContacts_InvalidAccountID(t *testing.T) { repo := &mockSearchRepo{} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/abc/search/contacts?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSearchHandler_SearchContacts_ServiceError(t *testing.T) { repo := &mockSearchRepo{contactErr: assert.AnError} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/contacts?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) } // ========== SearchArticles handler tests ========== func TestSearchHandler_SearchArticles_Success(t *testing.T) { repo := &mockSearchRepo{ articles: []model.Article{makeArticle(1, 1, "FAQ Guide", "desc", "content", "published")}, articleTotal: 1, } svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/articles?q=FAQ&page=1&per_page=10", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var body map[string]interface{} assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, true, body["success"]) data := body["data"].(map[string]interface{}) results := data["results"] assert.NotNil(t, results) } func TestSearchHandler_SearchArticles_InvalidAccountID(t *testing.T) { repo := &mockSearchRepo{} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/abc/search/articles?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSearchHandler_SearchArticles_ServiceError(t *testing.T) { repo := &mockSearchRepo{articleErr: assert.AnError} svc := search.NewSearchService(repo) handler := NewSearchHandler(svc) router := setupSearchHandlerRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/articles?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) }