feat(companies): align avatar form payloads
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -33,7 +35,7 @@ func (h *CompanyHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pg := pagination.Parse(c)
|
||||
pg := parseCompanyPagination(c)
|
||||
sort := c.DefaultQuery("sort", "")
|
||||
|
||||
companies, total, err := h.svc.List(c.Request.Context(), accountID, pg.Offset, pg.PerPage, sort)
|
||||
@@ -55,7 +57,7 @@ func (h *CompanyHandler) Search(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pg := pagination.Parse(c)
|
||||
pg := parseCompanyPagination(c)
|
||||
query := c.DefaultQuery("q", "")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Specify search string with parameter q"})
|
||||
@@ -108,7 +110,7 @@ func (h *CompanyHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req service.CreateCompanyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := bindCompanyRequest(c, &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -138,7 +140,7 @@ func (h *CompanyHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req service.UpdateCompanyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := bindCompanyRequest(c, &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -239,7 +241,7 @@ func (h *CompanyHandler) ListContacts(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pg := pagination.Parse(c)
|
||||
pg := parseCompanyPagination(c)
|
||||
|
||||
contacts, total, svcErr := h.svc.ListContacts(c.Request.Context(), uint(companyID), accountID, pg.Offset, pg.PerPage)
|
||||
if svcErr != nil {
|
||||
@@ -292,7 +294,7 @@ func (h *CompanyHandler) SearchContacts(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Specify search string with parameter q"})
|
||||
return
|
||||
}
|
||||
pg := pagination.Parse(c)
|
||||
pg := parseCompanyPagination(c)
|
||||
contacts, total, svcErr := h.svc.SearchContacts(c.Request.Context(), uint(companyID), accountID, query, pg.Offset, pg.PerPage)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
@@ -471,3 +473,82 @@ func parseCompanyContactID(c *gin.Context) (uint, error) {
|
||||
}
|
||||
return req.ContactID, nil
|
||||
}
|
||||
|
||||
type companyRequest interface {
|
||||
*service.CreateCompanyRequest | *service.UpdateCompanyRequest
|
||||
}
|
||||
|
||||
func parseCompanyPagination(c *gin.Context) pagination.Params {
|
||||
pg := pagination.Parse(c)
|
||||
pg.PerPage = service.CompanyResultsPerPage
|
||||
pg.Offset = (pg.Page - 1) * pg.PerPage
|
||||
return pg
|
||||
}
|
||||
|
||||
func bindCompanyRequest[T companyRequest](c *gin.Context, req T) error {
|
||||
if !strings.HasPrefix(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
return c.ShouldBindJSON(req)
|
||||
}
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
return err
|
||||
}
|
||||
name := companyFormValue(c, "name")
|
||||
description := companyFormValue(c, "description")
|
||||
websiteURL := companyFormValue(c, "website_url")
|
||||
faviconURL := companyFormValue(c, "favicon_url")
|
||||
domain := companyFormValue(c, "domain")
|
||||
if file, err := c.FormFile("company[avatar]"); err == nil && file != nil {
|
||||
faviconURL = file.Filename
|
||||
}
|
||||
customAttributes := companyFormJSON(c, "custom_attributes")
|
||||
|
||||
switch r := any(req).(type) {
|
||||
case *service.CreateCompanyRequest:
|
||||
r.Name = name
|
||||
r.Description = description
|
||||
r.WebsiteURL = websiteURL
|
||||
r.FaviconURL = faviconURL
|
||||
r.Domain = domain
|
||||
r.CustomAttributes = customAttributes
|
||||
case *service.UpdateCompanyRequest:
|
||||
r.Name = name
|
||||
r.Description = description
|
||||
r.WebsiteURL = websiteURL
|
||||
r.FaviconURL = faviconURL
|
||||
r.Domain = domain
|
||||
r.CustomAttributes = customAttributes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func companyFormValue(c *gin.Context, key string) string {
|
||||
if value := c.PostForm("company[" + key + "]"); value != "" {
|
||||
return value
|
||||
}
|
||||
return c.PostForm(key)
|
||||
}
|
||||
|
||||
func companyFormJSON(c *gin.Context, key string) []byte {
|
||||
if c.Request.MultipartForm == nil {
|
||||
return nil
|
||||
}
|
||||
if value := companyFormValue(c, key); value != "" {
|
||||
if json.Valid([]byte(value)) {
|
||||
return []byte(value)
|
||||
}
|
||||
}
|
||||
prefix := "company[" + key + "]["
|
||||
attrs := map[string]any{}
|
||||
for formKey, values := range c.Request.MultipartForm.Value {
|
||||
if !strings.HasPrefix(formKey, prefix) || !strings.HasSuffix(formKey, "]") || len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
attrKey := strings.TrimSuffix(strings.TrimPrefix(formKey, prefix), "]")
|
||||
attrs[attrKey] = values[0]
|
||||
}
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
data, _ := json.Marshal(attrs)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -153,6 +154,26 @@ func (s *CompanyHandlerTestSuite) makeRequest(method, path string, body interfac
|
||||
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() {
|
||||
@@ -184,6 +205,22 @@ func (s *CompanyHandlerTestSuite) TestList_WithCompanies() {
|
||||
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() {
|
||||
@@ -204,6 +241,29 @@ func (s *CompanyHandlerTestSuite) TestCreate_Success() {
|
||||
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
|
||||
@@ -276,6 +336,26 @@ func (s *CompanyHandlerTestSuite) TestUpdate_Success() {
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user