885 lines
35 KiB
Go
885 lines
35 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/datatypes"
|
|
"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"
|
|
)
|
|
|
|
// --- Company Handler Test Suite ---
|
|
// Uses real SQLite DB + real repos + real service.
|
|
|
|
type CompanyHandlerTestSuite struct {
|
|
suite.Suite
|
|
router *gin.Engine
|
|
handler *CompanyHandler
|
|
db *gorm.DB
|
|
account *model.Account
|
|
user *model.User
|
|
|
|
accountID uint
|
|
userID uint
|
|
}
|
|
|
|
func TestCompanyHandlerSuite(t *testing.T) {
|
|
suite.Run(t, new(CompanyHandlerTestSuite))
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) SetupSuite() {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Create in-memory SQLite DB
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
// Migrate all models needed
|
|
s.Require().NoError(db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.AccountUser{},
|
|
&model.Company{},
|
|
&model.CompanyNote{},
|
|
&model.Contact{},
|
|
&model.Inbox{},
|
|
&model.Conversation{},
|
|
&model.Message{},
|
|
&model.Attachment{},
|
|
))
|
|
s.db = db
|
|
|
|
// Create test account
|
|
account := &model.Account{Name: "TestAccount"}
|
|
s.Require().NoError(db.Create(account).Error)
|
|
s.account = account
|
|
s.accountID = account.ID
|
|
|
|
// Create test user
|
|
user := &model.User{Name: "TestUser", Email: "test@example.com"}
|
|
s.Require().NoError(db.Create(user).Error)
|
|
s.user = user
|
|
s.userID = user.ID
|
|
|
|
// Link user to account
|
|
accountUser := &model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator"}
|
|
s.Require().NoError(db.Create(accountUser).Error)
|
|
|
|
// Create real repos + service + handler
|
|
companyRepo := repository.NewCompanyRepo(db)
|
|
contactRepo := repository.NewContactRepo(db)
|
|
conversationRepo := repository.NewConversationRepo(db)
|
|
companySvc := service.NewCompanyService(companyRepo, contactRepo, conversationRepo)
|
|
s.handler = NewCompanyHandler(companySvc)
|
|
|
|
// Setup router with middleware that injects account_id and user_id into context
|
|
s.router = gin.New()
|
|
s.router.Use(func(c *gin.Context) {
|
|
c.Set("user_id", s.userID)
|
|
c.Set("account_id", s.accountID)
|
|
c.Next()
|
|
})
|
|
|
|
// Register company routes — matches the real router registration pattern
|
|
companies := s.router.Group("/api/v1/accounts/:id/companies")
|
|
{
|
|
companies.GET("/", s.handler.List)
|
|
companies.POST("/", s.handler.Create)
|
|
companies.GET("/search", s.handler.Search)
|
|
companies.GET("/:company_id", s.handler.Get)
|
|
companies.PUT("/:company_id", s.handler.Update)
|
|
companies.PATCH("/:company_id", s.handler.Update)
|
|
companies.DELETE("/:company_id", s.handler.Delete)
|
|
companies.POST("/:company_id/destroy_custom_attributes", s.handler.DestroyCustomAttributes)
|
|
companies.DELETE("/:company_id/avatar", s.handler.DeleteAvatar)
|
|
companies.GET("/:company_id/contacts", s.handler.ListContacts)
|
|
companies.GET("/:company_id/contacts/search", s.handler.SearchContacts)
|
|
companies.POST("/:company_id/contacts", s.handler.AddContact)
|
|
companies.POST("/:company_id/contacts/:contact_id", s.handler.AddContact)
|
|
companies.DELETE("/:company_id/contacts/:contact_id", s.handler.RemoveContact)
|
|
companies.GET("/:company_id/conversations", s.handler.ListConversations)
|
|
companies.GET("/:company_id/notes", s.handler.ListNotes)
|
|
companies.POST("/:company_id/notes", s.handler.CreateNote)
|
|
companies.DELETE("/:company_id/notes/:note_id", s.handler.DeleteNote)
|
|
}
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TearDownSuite() {
|
|
if s.db != nil {
|
|
sqlDB, err := s.db.DB()
|
|
if err == nil {
|
|
sqlDB.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) SetupTest() {
|
|
// Clean company-related tables between tests
|
|
s.db.Exec("DELETE FROM attachments")
|
|
s.db.Exec("DELETE FROM messages")
|
|
s.db.Exec("DELETE FROM conversations")
|
|
s.db.Exec("DELETE FROM contacts")
|
|
s.db.Exec("DELETE FROM company_notes")
|
|
s.db.Exec("DELETE FROM company_contacts")
|
|
s.db.Exec("DELETE FROM companies")
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) makeRequest(method, path string, body interface{}) *httptest.ResponseRecorder {
|
|
var bodyBytes []byte
|
|
if body != nil {
|
|
bodyBytes, _ = json.Marshal(body)
|
|
}
|
|
req, _ := http.NewRequest(method, path, bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
s.router.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) makeMultipartRequest(method, path string, fields map[string]string, files map[string]string) *httptest.ResponseRecorder {
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
for key, value := range fields {
|
|
s.Require().NoError(writer.WriteField(key, value))
|
|
}
|
|
for key, filename := range files {
|
|
part, err := writer.CreateFormFile(key, filename)
|
|
s.Require().NoError(err)
|
|
_, err = part.Write([]byte("avatar-bytes"))
|
|
s.Require().NoError(err)
|
|
}
|
|
s.Require().NoError(writer.Close())
|
|
req, _ := http.NewRequest(method, path, body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
w := httptest.NewRecorder()
|
|
s.router.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
// ========== List ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestList_Empty() {
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/", s.accountID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Contains(s.T(), resp, "payload")
|
|
assert.Equal(s.T(), float64(0), resp["meta"].(map[string]interface{})["total_count"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestList_WithCompanies() {
|
|
// Create companies directly via repo
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company1 := &model.Company{AccountID: s.accountID, Name: "ListCorp1"}
|
|
company2 := &model.Company{AccountID: s.accountID, Name: "ListCorp2"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company1))
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company2))
|
|
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/", s.accountID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
payload := resp["payload"].([]interface{})
|
|
assert.Len(s.T(), payload, 2)
|
|
assert.Equal(s.T(), float64(2), resp["meta"].(map[string]interface{})["total_count"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestList_IgnoresPerPage() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
for _, name := range []string{"ListCorp1", "ListCorp2", "ListCorp3"} {
|
|
s.Require().NoError(companyRepo.Create(context.Background(), &model.Company{AccountID: s.accountID, Name: name}))
|
|
}
|
|
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/?page=1&per_page=1", s.accountID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].([]interface{})
|
|
assert.Len(s.T(), payload, 3)
|
|
assert.Equal(s.T(), float64(3), resp["meta"].(map[string]interface{})["total_count"])
|
|
}
|
|
|
|
// ========== Create ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestCreate_Success() {
|
|
body := map[string]interface{}{
|
|
"company": map[string]interface{}{
|
|
"name": "NewCorp",
|
|
"description": "A new company",
|
|
"domain": "newcorp.com",
|
|
},
|
|
}
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/", s.accountID), body)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
companyData := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "NewCorp", companyData["name"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestCreate_MultipartAvatar() {
|
|
w := s.makeMultipartRequest(
|
|
"POST",
|
|
fmt.Sprintf("/api/v1/accounts/%d/companies/", s.accountID),
|
|
map[string]string{
|
|
"company[name]": "AvatarCorp",
|
|
"company[domain]": "avatar.example",
|
|
"company[custom_attributes][segment]": "enterprise",
|
|
},
|
|
map[string]string{"company[avatar]": "avatar.png"},
|
|
)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
companyData := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "AvatarCorp", companyData["name"])
|
|
assert.Equal(s.T(), "avatar.example", companyData["domain"])
|
|
assert.Equal(s.T(), "avatar.png", companyData["avatar_url"])
|
|
attrs := companyData["custom_attributes"].(map[string]interface{})
|
|
assert.Equal(s.T(), "enterprise", attrs["segment"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestCreate_ValidationError() {
|
|
body := map[string]interface{}{
|
|
"name": "", // required field
|
|
}
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/", s.accountID), body)
|
|
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// ========== Search ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestSearch_NoResult() {
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/search?q=Nonexistent", s.accountID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].([]interface{})
|
|
assert.Len(s.T(), payload, 0)
|
|
}
|
|
|
|
// ========== Get ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestGet_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "GetCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/%d", s.accountID, company.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
companyData := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "GetCorp", companyData["name"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestGet_NotFound() {
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/9999", s.accountID), nil)
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestGet_InvalidID() {
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/abc", s.accountID), nil)
|
|
// Gin returns 404 or 400 for invalid param — either is acceptable
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
// ========== Update ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestUpdate_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "UpdateCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
body := map[string]interface{}{
|
|
"company": map[string]interface{}{
|
|
"name": "UpdateCorpUpdated",
|
|
"domain": "updated.com",
|
|
},
|
|
}
|
|
w := s.makeRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/companies/%d", s.accountID, company.ID), body)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
companyData := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "UpdateCorpUpdated", companyData["name"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestUpdate_MultipartAvatar() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "AvatarUpdateCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeMultipartRequest(
|
|
"PATCH",
|
|
fmt.Sprintf("/api/v1/accounts/%d/companies/%d", s.accountID, company.ID),
|
|
map[string]string{"company[name]": "AvatarUpdateCorp", "company[domain]": "updated-avatar.example"},
|
|
map[string]string{"company[avatar]": "updated-avatar.png"},
|
|
)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
companyData := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "updated-avatar.example", companyData["domain"])
|
|
assert.Equal(s.T(), "updated-avatar.png", companyData["avatar_url"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestUpdate_NotFound() {
|
|
body := map[string]interface{}{
|
|
"name": "NonexistentCorp",
|
|
}
|
|
w := s.makeRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/companies/9999", s.accountID), body)
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
// ========== Delete ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDelete_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "DeleteCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/%d", s.accountID, company.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
assert.Empty(s.T(), w.Body.String())
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDelete_InvalidID() {
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/abc", s.accountID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDestroyCustomAttributes_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{
|
|
AccountID: s.accountID,
|
|
Name: "AttrCorp",
|
|
CustomAttributes: datatypes.JSON([]byte(`{"plan":"pro","tier":"gold"}`)),
|
|
}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/destroy_custom_attributes", s.accountID, company.ID), map[string]interface{}{
|
|
"custom_attributes": []string{"tier"},
|
|
})
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].(map[string]interface{})
|
|
attrs := payload["custom_attributes"].(map[string]interface{})
|
|
assert.Equal(s.T(), "pro", attrs["plan"])
|
|
assert.NotContains(s.T(), attrs, "tier")
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDestroyCustomAttributes_RequiresArray() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "AttrInvalidCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/destroy_custom_attributes", s.accountID, company.ID), map[string]interface{}{})
|
|
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDeleteAvatar_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "AvatarCorp", FaviconURL: "https://example.com/avatar.png"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/avatar", s.accountID, company.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "", payload["avatar_url"])
|
|
|
|
var found model.Company
|
|
s.Require().NoError(s.db.First(&found, company.ID).Error)
|
|
assert.Equal(s.T(), "", found.FaviconURL)
|
|
}
|
|
|
|
// ========== ListContacts ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestListContacts_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "ContactsCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
// Create contacts and link them
|
|
contact1 := &model.Contact{AccountID: s.accountID, Name: "Contact1", CompanyID: &company.ID}
|
|
contact2 := &model.Contact{AccountID: s.accountID, Name: "Contact2", CompanyID: &company.ID}
|
|
s.Require().NoError(s.db.Create(contact1).Error)
|
|
s.Require().NoError(s.db.Create(contact2).Error)
|
|
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts", s.accountID, company.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].([]interface{})
|
|
assert.Len(s.T(), payload, 2)
|
|
assert.Equal(s.T(), float64(2), resp["meta"].(map[string]interface{})["total_count"])
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestListContacts_InvalidID() {
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/abc/contacts", s.accountID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
// ========== ListConversations ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestListConversations_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "ConvsCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
contact := &model.Contact{AccountID: s.accountID, CompanyID: &company.ID, Name: "Company Contact", Email: "company-contact@example.com"}
|
|
s.Require().NoError(s.db.Create(contact).Error)
|
|
inbox := &model.Inbox{AccountID: s.accountID, Name: "Company Inbox", ChannelType: "web_widget", ChannelID: 1}
|
|
s.Require().NoError(s.db.Create(inbox).Error)
|
|
|
|
now := time.Now().Unix()
|
|
conversation := &model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: contact.ID, Status: "open", Priority: "urgent", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}
|
|
s.Require().NoError(s.db.Create(conversation).Error)
|
|
senderID := contact.ID
|
|
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.accountID, InboxID: inbox.ID, ConversationID: conversation.ID, SenderID: &senderID, SenderType: "contact", MessageType: "incoming", ContentType: "text", Status: "sent", Content: "Company scoped message"}).Error)
|
|
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/conversations", s.accountID, company.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].([]interface{})
|
|
s.Require().Len(payload, 1)
|
|
first := payload[0].(map[string]interface{})
|
|
assert.Equal(s.T(), "urgent", first["priority"])
|
|
assert.Equal(s.T(), float64(conversation.ID), first["id"])
|
|
assert.Equal(s.T(), float64(s.accountID), first["account_id"])
|
|
assert.NotContains(s.T(), first, "contact_id")
|
|
|
|
meta := first["meta"].(map[string]interface{})
|
|
assert.Equal(s.T(), "web_widget", meta["channel"])
|
|
assert.Equal(s.T(), float64(contact.ID), meta["sender"].(map[string]interface{})["id"])
|
|
messages := first["messages"].([]interface{})
|
|
s.Require().Len(messages, 1)
|
|
message := messages[0].(map[string]interface{})
|
|
assert.Equal(s.T(), "Company scoped message", message["content"])
|
|
assert.Equal(s.T(), float64(0), message["message_type"])
|
|
}
|
|
|
|
// ========== ListNotes ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestListNotes_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "NotesCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
// Create notes directly
|
|
note1 := &model.CompanyNote{CompanyID: company.ID, UserID: s.userID, Content: "Note 1"}
|
|
note2 := &model.CompanyNote{CompanyID: company.ID, UserID: s.userID, Content: "Note 2"}
|
|
s.Require().NoError(s.db.Create(note1).Error)
|
|
s.Require().NoError(s.db.Create(note2).Error)
|
|
|
|
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes", s.accountID, company.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
payload := resp["payload"].([]interface{})
|
|
assert.Len(s.T(), payload, 2)
|
|
first := payload[0].(map[string]interface{})
|
|
assert.Contains(s.T(), first, "company_id")
|
|
assert.Contains(s.T(), first, "user")
|
|
user := first["user"].(map[string]interface{})
|
|
assert.Equal(s.T(), "TestUser", user["name"])
|
|
}
|
|
|
|
// ========== CreateNote ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestCreateNote_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "NoteCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
body := map[string]interface{}{
|
|
"content": "This is a new note",
|
|
}
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes", s.accountID, company.ID), body)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
noteData := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "This is a new note", noteData["content"])
|
|
assert.Equal(s.T(), float64(company.ID), noteData["company_id"])
|
|
assert.Contains(s.T(), noteData, "user")
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestCreateNote_ValidationError() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "NoteCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
body := map[string]interface{}{
|
|
"content": "", // required field
|
|
}
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes", s.accountID, company.ID), body)
|
|
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestCreateNote_InvalidCompanyID() {
|
|
body := map[string]interface{}{
|
|
"content": "Note content",
|
|
}
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/abc/notes", s.accountID), body)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
// ========== DeleteNote ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDeleteNote_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "DelNoteCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
// Create a note directly
|
|
note := &model.CompanyNote{CompanyID: company.ID, UserID: s.userID, Content: "Note to delete"}
|
|
s.Require().NoError(s.db.Create(note).Error)
|
|
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes/%d", s.accountID, company.ID, note.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
assert.Empty(s.T(), w.Body.String())
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDeleteNote_InvalidCompanyID() {
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/abc/notes/1", s.accountID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestDeleteNote_InvalidNoteID() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "DelNoteInvalidCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/notes/abc", s.accountID, company.ID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid note ID, got %d", w.Code)
|
|
}
|
|
|
|
// ========== AddContact ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestAddContact_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "AddContactCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
contact := &model.Contact{AccountID: s.accountID, Name: "Test Contact", Email: "addcontact@example.com"}
|
|
s.Require().NoError(s.db.Create(contact).Error)
|
|
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts/%d", s.accountID, company.ID, contact.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
payload := resp["payload"].(map[string]interface{})
|
|
assert.Equal(s.T(), "Test Contact", payload["name"])
|
|
assert.True(s.T(), payload["linked_to_current_company"].(bool))
|
|
|
|
var updated model.Contact
|
|
s.Require().NoError(s.db.First(&updated, contact.ID).Error)
|
|
s.Require().NotNil(updated.CompanyID)
|
|
assert.Equal(s.T(), company.ID, *updated.CompanyID)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestAddContact_InvalidCompanyID() {
|
|
contact := &model.Contact{AccountID: s.accountID, Name: "Contact", Email: "invalidco@example.com"}
|
|
s.Require().NoError(s.db.Create(contact).Error)
|
|
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/abc/contacts/%d", s.accountID, contact.ID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestAddContact_InvalidContactID() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "AddContactInvalidCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts/abc", s.accountID, company.ID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid contact ID, got %d", w.Code)
|
|
}
|
|
|
|
// ========== RemoveContact ==========
|
|
|
|
func (s *CompanyHandlerTestSuite) TestRemoveContact_Success() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "RemoveContactCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
contact := &model.Contact{AccountID: s.accountID, Name: "Remove Contact", Email: "removecontact@example.com", CompanyID: &company.ID}
|
|
s.Require().NoError(s.db.Create(contact).Error)
|
|
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts/%d", s.accountID, company.ID, contact.ID), nil)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
assert.Empty(s.T(), w.Body.String())
|
|
|
|
var updated model.Contact
|
|
s.Require().NoError(s.db.First(&updated, contact.ID).Error)
|
|
assert.Nil(s.T(), updated.CompanyID)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestRemoveContact_InvalidCompanyID() {
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/abc/contacts/1", s.accountID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid company ID, got %d", w.Code)
|
|
}
|
|
|
|
func (s *CompanyHandlerTestSuite) TestRemoveContact_InvalidContactID() {
|
|
companyRepo := repository.NewCompanyRepo(s.db)
|
|
company := &model.Company{AccountID: s.accountID, Name: "RemoveContactInvalidCorp"}
|
|
s.Require().NoError(companyRepo.Create(context.Background(), company))
|
|
|
|
w := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/contacts/abc", s.accountID, company.ID), nil)
|
|
assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
|
|
"Expected 400 or 404 for invalid contact ID, got %d", w.Code)
|
|
}
|
|
|
|
// ========== NoAuth tests (standalone, not in suite) ==========
|
|
|
|
func TestCompanyHandler_List_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.GET("/", h.List)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/companies/", nil)
|
|
r.ServeHTTP(w, req)
|
|
// Without account_id in context, handler should abort with 401 or 400
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_Search_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.GET("/search", h.Search)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/companies/search?q=test", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_Create_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.POST("/", h.Create)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/companies/", bytes.NewReader([]byte(`{"name":"test"}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_Get_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.GET("/:company_id", h.Get)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/companies/1", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_Update_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.PUT("/:company_id", h.Update)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/companies/1", bytes.NewReader([]byte(`{"name":"test"}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_Delete_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.DELETE("/:company_id", h.Delete)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/companies/1", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_ListContacts_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.GET("/:company_id/contacts", h.ListContacts)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/companies/1/contacts", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_ListConversations_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.GET("/:company_id/conversations", h.ListConversations)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/companies/1/conversations", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_ListNotes_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.GET("/:company_id/notes", h.ListNotes)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/companies/1/notes", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_CreateNote_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.POST("/:company_id/notes", h.CreateNote)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/companies/1/notes", bytes.NewReader([]byte(`{"content":"test"}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_DeleteNote_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.DELETE("/:company_id/notes/:note_id", h.DeleteNote)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/companies/1/notes/1", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_AddContact_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.POST("/:company_id/contacts/:contact_id", h.AddContact)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/companies/1/contacts/1", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|
|
|
|
func TestCompanyHandler_RemoveContact_NoAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
companySvc := &service.CompanyService{}
|
|
h := NewCompanyHandler(companySvc)
|
|
companies := r.Group("/api/v1/accounts/:id/companies")
|
|
companies.DELETE("/:company_id/contacts/:contact_id", h.RemoveContact)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/companies/1/contacts/1", nil)
|
|
r.ServeHTTP(w, req)
|
|
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest || w.Code == http.StatusForbidden,
|
|
"Expected auth error, got %d", w.Code)
|
|
}
|