package v1 import ( "bytes" "context" "encoding/json" "fmt" "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 } // ========== 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"]) } // ========== 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_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_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) }