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

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)
}