Files
gochat/internal/handler/api/v1/search_handler_test.go
T

373 lines
12 KiB
Go

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
companies []model.Company
companyTotal int64
companyErr 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) SearchCompanies(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]model.Company, int64, error) {
return m.companies, m.companyTotal, m.companyErr
}
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)
}