836 lines
25 KiB
Go
836 lines
25 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
)
|
|
|
|
// ContactHandlerCRUDTestSuite tests ContactHandler core CRUD methods
|
|
// with a real SQLite database and wired services.
|
|
type ContactHandlerCRUDTestSuite struct {
|
|
suite.Suite
|
|
|
|
db *gorm.DB
|
|
router *gin.Engine
|
|
handler *ContactHandler
|
|
|
|
account *model.Account
|
|
user *model.User
|
|
contact *model.Contact
|
|
}
|
|
|
|
// SetupSuite initializes the database, services, handler, and test data.
|
|
func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
s.Require().NoError(err, "failed to open SQLite test database")
|
|
|
|
s.Require().NoError(db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.AccountUser{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.ContactInbox{},
|
|
&model.InboxMember{},
|
|
&model.ContactNote{},
|
|
&model.Note{},
|
|
), "failed to auto-migrate models")
|
|
|
|
s.db = db
|
|
|
|
// Wire repos → services → handler
|
|
contactRepo := repository.NewContactRepo(db)
|
|
contactInboxRepo := repository.NewContactInboxRepo(db)
|
|
contactNoteRepo := repository.NewContactNoteRepo(db)
|
|
noteRepo := repository.NewNoteRepo(db)
|
|
conversationRepo := repository.NewConversationRepo(db)
|
|
|
|
contactInboxSvc := service.NewContactInboxService(contactInboxRepo)
|
|
mergeRepo := repository.NewContactMergeRepo(db)
|
|
mergeSvc := service.NewContactMergeService(mergeRepo, db)
|
|
contactSvc := service.NewContactService(contactRepo, contactInboxSvc, noteRepo)
|
|
contactNoteSvc := service.NewContactNoteService(contactRepo, contactNoteRepo)
|
|
conversationSvc := service.NewConversationService(conversationRepo, nil, nil, nil, nil, nil, nil)
|
|
|
|
s.handler = NewContactHandler(contactSvc, contactInboxSvc, mergeSvc, contactNoteSvc, conversationSvc)
|
|
|
|
// Setup router with all contact routes
|
|
s.router = gin.New()
|
|
s.router.Use(gin.Recovery(), s.mockAuthMiddleware())
|
|
s.router.GET("/api/v1/accounts/:id/contacts", s.handler.List)
|
|
s.router.GET("/api/v1/accounts/:id/contacts/search", s.handler.Search)
|
|
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Get)
|
|
s.router.POST("/api/v1/accounts/:id/contacts", s.handler.Create)
|
|
s.router.PUT("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Update)
|
|
s.router.DELETE("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Delete)
|
|
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/conversations", s.handler.ListConversations)
|
|
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/contact_inboxes", s.handler.ListContactInboxes)
|
|
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.ListNotes)
|
|
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.CreateNote)
|
|
|
|
// Create test data
|
|
s.account = &model.Account{Name: "Test Account", Locale: "en", Status: "active"}
|
|
s.Require().NoError(db.Create(s.account).Error)
|
|
|
|
s.user = &model.User{
|
|
Name: "Test User",
|
|
Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()),
|
|
AccountID: s.account.ID,
|
|
}
|
|
s.Require().NoError(db.Create(s.user).Error)
|
|
|
|
s.contact = &model.Contact{
|
|
AccountID: s.account.ID,
|
|
Name: "Jane Doe",
|
|
Email: "jane@example.com",
|
|
PhoneNumber: "1234567890",
|
|
}
|
|
s.Require().NoError(db.Create(s.contact).Error)
|
|
}
|
|
|
|
// SetupTest re-creates core test data before each test so tests don't leak state.
|
|
func (s *ContactHandlerCRUDTestSuite) SetupTest() {
|
|
s.db.Exec("DELETE FROM contact_notes")
|
|
s.db.Exec("DELETE FROM notes")
|
|
s.db.Exec("DELETE FROM contact_inboxes")
|
|
s.db.Exec("DELETE FROM conversations")
|
|
s.db.Exec("DELETE FROM contacts")
|
|
s.db.Exec("DELETE FROM inbox_members")
|
|
s.db.Exec("DELETE FROM inboxes")
|
|
s.db.Exec("DELETE FROM account_users")
|
|
s.db.Exec("DELETE FROM users")
|
|
s.db.Exec("DELETE FROM accounts")
|
|
|
|
s.account = &model.Account{Name: "CRUDTestOrg", Locale: "en", Active: true}
|
|
s.Require().NoError(s.db.Create(s.account).Error)
|
|
|
|
s.user = &model.User{
|
|
Name: "CRUDTestUser",
|
|
Email: "crud@example.com",
|
|
Password: "hashed",
|
|
}
|
|
s.Require().NoError(s.db.Create(s.user).Error)
|
|
|
|
au := &model.AccountUser{
|
|
UserID: s.user.ID,
|
|
AccountID: s.account.ID,
|
|
}
|
|
s.Require().NoError(s.db.Create(au).Error)
|
|
|
|
s.contact = &model.Contact{
|
|
AccountID: s.account.ID,
|
|
Name: "Jane Doe",
|
|
Email: "jane@example.com",
|
|
PhoneNumber: "1234567890",
|
|
}
|
|
s.Require().NoError(s.db.Create(s.contact).Error)
|
|
}
|
|
|
|
// TearDownSuite closes the database.
|
|
func (s *ContactHandlerCRUDTestSuite) TearDownSuite() {
|
|
if s.db != nil {
|
|
sqlDB, _ := s.db.DB()
|
|
sqlDB.Close()
|
|
}
|
|
}
|
|
|
|
// skipIfSQLite skips tests that require PostgreSQL-specific features (ILIKE, pg_trgm).
|
|
func skipIfSQLiteForHandler(t *testing.T) {
|
|
t.Helper()
|
|
t.Skip("Skipping: this test requires PostgreSQL (ILIKE / trigram)")
|
|
}
|
|
|
|
// mockAuthMiddleware sets user_id in the Gin context (simulates authenticated user).
|
|
func (s *ContactHandlerCRUDTestSuite) mockAuthMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Set("user_id", s.user.ID)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ===========================
|
|
// List Contacts
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestList_Success() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts?page=1&page_size=25", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
s.Contains(resp, "contacts")
|
|
|
|
meta, ok := resp["meta"]
|
|
s.True(ok, "response should contain 'meta' key")
|
|
metaMap := meta.(map[string]interface{})
|
|
s.Equal(float64(1), metaMap["count"])
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestList_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/contacts?page=1&page_size=25", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestList_Pagination() {
|
|
// Create additional contacts
|
|
for i := 0; i < 3; i++ {
|
|
c := &model.Contact{
|
|
AccountID: s.account.ID,
|
|
Name: fmt.Sprintf("Contact %d", i),
|
|
}
|
|
s.Require().NoError(s.db.Create(c).Error)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts?page=1&page_size=2", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
meta, ok := resp["meta"]
|
|
s.True(ok)
|
|
metaMap := meta.(map[string]interface{})
|
|
s.Equal(float64(4), metaMap["count"]) // 1 original + 3 new
|
|
}
|
|
|
|
// ===========================
|
|
// Search Contacts
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestSearch_Success() {
|
|
skipIfSQLiteForHandler(s.T()) // Search uses ILIKE which SQLite doesn't support
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/search?q=Jane&page=1&page_size=25", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "contacts")
|
|
s.Contains(resp, "meta")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestSearch_EmptyQuery() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/search?q=&page=1&page_size=25", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// Empty query falls back to ListByAccount
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "contacts")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestSearch_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/contacts/search?q=test", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
// ===========================
|
|
// Get Contact
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestGet_Success() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.account.ID, s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var contact model.Contact
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &contact))
|
|
s.Equal(s.contact.ID, contact.ID)
|
|
s.Equal("Jane Doe", contact.Name)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestGet_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/abc/contacts/%d", s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestGet_InvalidContactID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/abc", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestGet_NotFound() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/99999", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// Contact not found → 404 per handler code
|
|
s.Equal(http.StatusNotFound, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
// ===========================
|
|
// Create Contact
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreate_Success() {
|
|
body := map[string]interface{}{
|
|
"name": "New Contact",
|
|
"email": "new@example.com",
|
|
"phone": "5551234",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusCreated, w.Code)
|
|
|
|
var contact model.Contact
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &contact))
|
|
s.Equal("New Contact", contact.Name)
|
|
s.Equal("new@example.com", contact.Email)
|
|
s.NotZero(contact.ID)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreate_InvalidAccountID() {
|
|
body := map[string]interface{}{
|
|
"name": "New Contact",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
"/api/v1/accounts/abc/contacts",
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreate_EmptyName() {
|
|
body := map[string]interface{}{
|
|
"name": "",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// Should fail validation (name required, min=1)
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreate_WithCustomAttributes() {
|
|
body := map[string]interface{}{
|
|
"name": "Custom Contact",
|
|
"email": "custom@example.com",
|
|
"custom_attributes": map[string]interface{}{"tier": "gold", "vip": true},
|
|
"additional_attributes": map[string]interface{}{"city": "NYC"},
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusCreated, w.Code)
|
|
|
|
var contact model.Contact
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &contact))
|
|
s.Equal("Custom Contact", contact.Name)
|
|
}
|
|
|
|
// ===========================
|
|
// Update Contact
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestUpdate_Success() {
|
|
body := map[string]interface{}{
|
|
"name": "Jane Updated",
|
|
"email": "jane.updated@example.com",
|
|
"phone": "9998887776",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.account.ID, s.contact.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var contact model.Contact
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &contact))
|
|
s.Equal("Jane Updated", contact.Name)
|
|
s.Equal("jane.updated@example.com", contact.Email)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestUpdate_InvalidAccountID() {
|
|
body := map[string]interface{}{
|
|
"name": "Updated Name",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT",
|
|
fmt.Sprintf("/api/v1/accounts/abc/contacts/%d", s.contact.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestUpdate_InvalidContactID() {
|
|
body := map[string]interface{}{
|
|
"name": "Updated Name",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/abc", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestUpdate_NotFound() {
|
|
body := map[string]interface{}{
|
|
"name": "Ghost Update",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/99999", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
// ===========================
|
|
// Delete Contact
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestDelete_Success() {
|
|
// Create a new contact specifically for deletion
|
|
delContact := &model.Contact{
|
|
AccountID: s.account.ID,
|
|
Name: "Delete Me",
|
|
}
|
|
s.Require().NoError(s.db.Create(delContact).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.account.ID, delContact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "message")
|
|
|
|
// Verify soft-delete
|
|
var found model.Contact
|
|
err := s.db.First(&found, delContact.ID).Error
|
|
s.Error(err, "contact should be soft-deleted")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestDelete_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE",
|
|
fmt.Sprintf("/api/v1/accounts/abc/contacts/%d", s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestDelete_InvalidContactID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/abc", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestDelete_NotFound() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/99999", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
// ===========================
|
|
// ListContactInboxes
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListContactInboxes_Success() {
|
|
// Create an inbox and a contact_inbox for the test contact
|
|
inbox := &model.Inbox{
|
|
AccountID: s.account.ID,
|
|
Name: "Test Inbox",
|
|
ChannelType: "web_widget",
|
|
ChannelID: 1,
|
|
}
|
|
s.Require().NoError(s.db.Create(inbox).Error)
|
|
|
|
ci := &model.ContactInbox{
|
|
ContactID: s.contact.ID,
|
|
InboxID: inbox.ID,
|
|
SourceID: "test_source",
|
|
}
|
|
s.Require().NoError(s.db.Create(ci).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/contact_inboxes", s.account.ID, s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "contact_inboxes")
|
|
|
|
meta := resp["meta"].(map[string]interface{})
|
|
s.Equal(float64(1), meta["count"])
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListContactInboxes_InvalidContactID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/abc/contact_inboxes", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "error")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListContactInboxes_EmptyResult() {
|
|
// Create a new contact with no inboxes
|
|
emptyContact := &model.Contact{
|
|
AccountID: s.account.ID,
|
|
Name: "No Inboxes Contact",
|
|
}
|
|
s.Require().NoError(s.db.Create(emptyContact).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/contact_inboxes", s.account.ID, emptyContact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
cis := resp["contact_inboxes"]
|
|
s.NotNil(cis)
|
|
|
|
meta := resp["meta"].(map[string]interface{})
|
|
s.Equal(float64(0), meta["count"])
|
|
}
|
|
|
|
// ===========================
|
|
// ListConversations
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListConversations_Success() {
|
|
inbox := &model.Inbox{AccountID: s.account.ID, Name: "Conversation Inbox", ChannelType: "web_widget", ChannelID: 1}
|
|
s.Require().NoError(s.db.Create(inbox).Error)
|
|
|
|
now := time.Now().Unix()
|
|
older := now - 60
|
|
s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "low", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &older}).Error)
|
|
s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "high", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/conversations", s.account.ID, s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].([]interface{})
|
|
s.Len(payload, 2)
|
|
first := payload[0].(map[string]interface{})
|
|
s.Equal("high", first["priority"])
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListConversations_InvalidContactID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/contacts/abc/conversations", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// ===========================
|
|
// ListNotes
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListNotes_Success() {
|
|
// Create a note for the contact via the Note model
|
|
note := &model.Note{
|
|
Content: "Test note content",
|
|
AccountID: s.account.ID,
|
|
ContactID: s.contact.ID,
|
|
}
|
|
note.UserID = &s.user.ID
|
|
s.Require().NoError(s.db.Create(note).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", s.account.ID, s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Contains(resp, "notes")
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListNotes_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/abc/contacts/%d/notes", s.contact.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListNotes_InvalidContactID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/abc/notes", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestListNotes_NotFoundContact() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/99999/notes", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// Service returns error when contact not found
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
// ===========================
|
|
// CreateNote
|
|
// ===========================
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_Success() {
|
|
body := map[string]interface{}{
|
|
"content": "A new note for the contact",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", s.account.ID, s.contact.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusCreated, w.Code)
|
|
|
|
var note model.Note
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), ¬e))
|
|
s.Equal("A new note for the contact", note.Content)
|
|
s.NotZero(note.ID)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_InvalidAccountID() {
|
|
body := map[string]interface{}{
|
|
"content": "Note content",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/abc/contacts/%d/notes", s.contact.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_InvalidContactID() {
|
|
body := map[string]interface{}{
|
|
"content": "Note content",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/abc/notes", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_NoAuth() {
|
|
// Create a router without auth middleware to test getUserID returning 0
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
r.POST("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.CreateNote)
|
|
|
|
body := map[string]interface{}{
|
|
"content": "Unauthorized note",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", s.account.ID, s.contact.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
|
|
// getUserID returns 0 → handler returns 401
|
|
s.Equal(http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_EmptyContent() {
|
|
body := map[string]interface{}{
|
|
"content": "",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/notes", s.account.ID, s.contact.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// Empty content fails validation → service returns error
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
func (s *ContactHandlerCRUDTestSuite) TestCreateNote_NotFoundContact() {
|
|
body := map[string]interface{}{
|
|
"content": "Note for non-existent contact",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/contacts/99999/notes", s.account.ID),
|
|
bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
// ===========================
|
|
// Run the suite
|
|
// ===========================
|
|
|
|
func TestContactHandlerCRUDTestSuite(t *testing.T) {
|
|
suite.Run(t, new(ContactHandlerCRUDTestSuite))
|
|
}
|