feat(help-center): align portal payloads

This commit is contained in:
2026-06-06 03:39:57 +08:00
parent 24a5cb3718
commit f3ac4a7ee3
7 changed files with 572 additions and 84 deletions
+286 -61
View File
@@ -1,13 +1,15 @@
package v1
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
@@ -24,162 +26,182 @@ func NewPortalHandler(svc *service.PortalService) *PortalHandler {
// Create creates a new portal.
// POST /api/v1/accounts/:account_id/portals
func (h *PortalHandler) Create(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
// Chatwoot: params.require(:portal) → {"portal": {...}}
var wrapper struct {
Portal service.CreatePortalRequest `json:"portal"`
}
if err := c.ShouldBindJSON(&wrapper); err != nil {
var req service.CreatePortalRequest
if err := bindChatwootPayload(c, "portal", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
req := wrapper.Portal
portal, err := h.svc.Create(c.Request.Context(), uint(accountID), &req)
portal, err := h.svc.Create(c.Request.Context(), accountID, &req)
if err != nil {
applogger.L().Errorf("Create portal: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create portal")
return
}
response.Created(c, portal)
c.JSON(http.StatusOK, portalPayload(portal, "", 0))
}
// Get retrieves a portal by ID.
// GET /api/v1/accounts/:account_id/portals/:portal_id
func (h *PortalHandler) Get(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
portal, err := h.svc.GetByID(c.Request.Context(), uint(portalID))
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
applogger.L().Errorf("Get portal: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
response.OK(c, portal)
c.JSON(http.StatusOK, portalPayload(portal, c.Query("locale"), currentUserID(c)))
}
// Update modifies an existing portal.
// PUT /api/v1/accounts/:account_id/portals/:portal_id
func (h *PortalHandler) Update(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
// Chatwoot: params.require(:portal) → {"portal": {...}}
var wrapper struct {
Portal service.UpdatePortalRequest `json:"portal"`
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
if err := c.ShouldBindJSON(&wrapper); err != nil {
var req service.PatchPortalRequest
if err := bindChatwootPayload(c, "portal", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
req := wrapper.Portal
portal, err := h.svc.Update(c.Request.Context(), uint(portalID), &req)
portal, err = h.svc.UpdatePatch(c.Request.Context(), portal, &req)
if err != nil {
applogger.L().Errorf("Update portal: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update portal")
return
}
response.OK(c, portal)
c.JSON(http.StatusOK, portalPayload(portal, c.Query("locale"), currentUserID(c)))
}
// Delete soft-deletes a portal.
// DELETE /api/v1/accounts/:account_id/portals/:portal_id
func (h *PortalHandler) Delete(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
if err := h.svc.Delete(c.Request.Context(), uint(portalID)); err != nil {
if err := h.svc.Delete(c.Request.Context(), portal.ID); err != nil {
applogger.L().Errorf("Delete portal: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete portal")
return
}
response.NoContent(c)
c.Status(http.StatusOK)
}
// List retrieves all portals for an account (paginated).
// GET /api/v1/accounts/:account_id/portals
func (h *PortalHandler) List(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
if err != nil {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
pg := pagination.Parse(c)
portals, total, err := h.svc.ListByAccountID(c.Request.Context(), uint(accountID), pg.Page, pg.PerPage)
portals, _, err := h.svc.ListByAccountIDWithAssociations(c.Request.Context(), accountID, 0, 0)
if err != nil {
applogger.L().Errorf("List portals: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list portals")
return
}
response.OKWithMeta(c, portals, pg.Page, pg.PerPage, total)
payload := make([]gin.H, 0, len(portals))
for i := range portals {
payload = append(payload, portalPayload(&portals[i], "", 0))
}
currentPage := c.DefaultQuery("page", "1")
c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": gin.H{"current_page": currentPage, "portals_count": len(portals)}})
}
// Archive sets archived=true on a portal.
// POST /api/v1/accounts/:account_id/portals/:portal_id/archive
func (h *PortalHandler) Archive(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
portal, err := h.svc.Archive(c.Request.Context(), uint(portalID))
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
_, err = h.svc.Archive(c.Request.Context(), portal.ID)
if err != nil {
applogger.L().Errorf("Archive portal: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to archive portal")
return
}
response.OK(c, portal)
c.Status(http.StatusOK)
}
// RemoveLogo clears the logo_url on a portal.
// DELETE /api/v1/accounts/:account_id/portals/:portal_id/logo
func (h *PortalHandler) RemoveLogo(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
portal, err := h.svc.RemoveLogo(c.Request.Context(), uint(portalID))
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
_, err = h.svc.RemoveLogo(c.Request.Context(), portal.ID)
if err != nil {
applogger.L().Errorf("Remove portal logo: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to remove portal logo")
return
}
response.OK(c, portal)
c.Status(http.StatusOK)
}
// SendInstructions sends CNAME configuration instructions to an email address.
// POST /api/v1/accounts/:account_id/portals/:portal_id/send_instructions
func (h *PortalHandler) SendInstructions(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
@@ -189,30 +211,233 @@ func (h *PortalHandler) SendInstructions(c *gin.Context) {
return
}
if err := h.svc.SendInstructions(c.Request.Context(), uint(portalID), &req); err != nil {
if err := h.svc.SendInstructions(c.Request.Context(), portal.ID, &req); err != nil {
applogger.L().Errorf("Send portal instructions: %v", err)
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": portalInstructionError(err.Error())})
return
}
response.NoContent(c)
c.JSON(http.StatusOK, gin.H{"message": "Instructions sent successfully"})
}
// SSLStatus returns the SSL certificate status for a portal's custom domain.
// GET /api/v1/accounts/:account_id/portals/:portal_id/ssl_status
func (h *PortalHandler) SSLStatus(c *gin.Context) {
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
status, err := h.svc.SSLStatus(c.Request.Context(), uint(portalID))
portal, err := h.svc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
if err != nil {
applogger.L().Errorf("Get SSL status: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get SSL status")
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return
}
if portal.CustomDomain == "" {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "Custom domain is not configured"})
return
}
c.JSON(http.StatusOK, portalSSLStatusPayload(portal))
}
response.OK(c, status)
}
func portalPayload(portal *model.Portal, locale string, currentUserID uint) gin.H {
if portal == nil {
return gin.H{}
}
config := portalConfig(portal.PortalConfiguration)
defaultLocale := configString(config, "default_locale", portal.Locale)
if defaultLocale == "" {
defaultLocale = "en"
}
allowedLocales := configStringSlice(config, "allowed_locales")
if len(allowedLocales) == 0 {
allowedLocales = []string{defaultLocale}
}
draftLocales := configStringSet(configStringSlice(config, "draft_locales"))
selectedArticles := portal.Articles
if locale != "" {
selectedArticles = make([]model.Article, 0, len(portal.Articles))
for _, article := range portal.Articles {
if article.Locale == locale {
selectedArticles = append(selectedArticles, article)
}
}
}
payload := gin.H{
"id": portal.ID,
"color": portal.Color,
"custom_domain": portal.CustomDomain,
"header_text": portal.HeaderText,
"homepage_link": portal.HomepageLink,
"name": portal.Name,
"page_title": portal.PageTitle,
"slug": portal.Slug,
"archived": portal.Archived,
"account_id": portal.AccountID,
"config": gin.H{
"allowed_locales": portalAllowedLocalePayloads(allowedLocales, draftLocales, portal.Articles, portal.Categories),
"default_locale": defaultLocale,
"layout": configString(config, "layout", "classic"),
"social_profiles": configMap(config, "social_profiles"),
},
"meta": portalMeta(portal, selectedArticles, defaultLocale, currentUserID),
}
if portal.ChannelWebWidgetID != nil {
payload["inbox"] = gin.H{"id": *portal.ChannelWebWidgetID}
}
if portal.LogoURL != "" {
payload["logo"] = gin.H{"file_url": portal.LogoURL, "portal_id": portal.ID, "account_id": portal.AccountID}
}
sslSettings := portalSSLStatusPayload(portal)
if sslSettings["status"] != nil || sslSettings["verification_errors"] != nil {
payload["ssl_settings"] = sslSettings
}
return payload
}
func portalMeta(portal *model.Portal, articles []model.Article, defaultLocale string, currentUserID uint) gin.H {
meta := gin.H{
"all_articles_count": len(articles),
"archived_articles_count": articleStatusCount(articles, "archived"),
"published_count": articleStatusCount(articles, "published"),
"draft_articles_count": articleStatusCount(articles, "draft"),
"categories_count": len(portal.Categories),
"default_locale": defaultLocale,
}
if currentUserID != 0 && len(articles) > 0 {
mine := 0
for _, article := range articles {
if article.AuthorID != nil && *article.AuthorID == currentUserID {
mine++
}
}
meta["mine_articles_count"] = mine
}
return meta
}
func articleStatusCount(articles []model.Article, status string) int {
count := 0
for _, article := range articles {
if article.Status == status {
count++
}
}
return count
}
func portalAllowedLocalePayloads(locales []string, draftLocales map[string]bool, articles []model.Article, categories []model.Category) []gin.H {
items := make([]gin.H, 0, len(locales))
for _, locale := range locales {
items = append(items, gin.H{
"code": locale,
"articles_count": articleLocaleCount(articles, locale),
"categories_count": categoryLocaleCount(categories, locale),
"draft": draftLocales[locale],
})
}
return items
}
func articleLocaleCount(articles []model.Article, locale string) int {
count := 0
for _, article := range articles {
if article.Locale == locale {
count++
}
}
return count
}
func categoryLocaleCount(categories []model.Category, locale string) int {
count := 0
for _, category := range categories {
if category.Locale == locale {
count++
}
}
return count
}
func portalConfig(raw json.RawMessage) map[string]any {
if len(raw) == 0 {
return map[string]any{}
}
var cfg map[string]any
if err := json.Unmarshal(raw, &cfg); err != nil {
return map[string]any{}
}
return cfg
}
func configString(cfg map[string]any, key, fallback string) string {
if value, ok := cfg[key].(string); ok && value != "" {
return value
}
return fallback
}
func configStringSlice(cfg map[string]any, key string) []string {
items, ok := cfg[key].([]any)
if !ok {
return nil
}
values := make([]string, 0, len(items))
for _, item := range items {
if value, ok := item.(string); ok && value != "" {
values = append(values, value)
}
}
return values
}
func configStringSet(values []string) map[string]bool {
set := make(map[string]bool, len(values))
for _, value := range values {
set[value] = true
}
return set
}
func configMap(cfg map[string]any, key string) map[string]any {
if value, ok := cfg[key].(map[string]any); ok {
return value
}
return map[string]any{}
}
func portalSSLStatusPayload(portal *model.Portal) gin.H {
settings := portalConfig(portal.SSLSettings)
return gin.H{"status": settings["cf_status"], "verification_errors": settings["cf_verification_errors"]}
}
func currentUserID(c *gin.Context) uint {
if id, err := strconv.ParseUint(c.GetHeader("X-User-ID"), 10, 32); err == nil {
return uint(id)
}
if userID, exists := c.Get("user_id"); exists {
switch v := userID.(type) {
case uint:
return v
case int:
return uint(v)
case float64:
return uint(v)
}
}
return 0
}
func portalInstructionError(message string) string {
lower := strings.ToLower(message)
switch {
case strings.Contains(lower, "no custom domain") || strings.Contains(lower, "custom domain"):
return "Custom domain is not configured"
case strings.Contains(lower, "invalid email"):
return "Invalid email format"
case strings.Contains(lower, "email is required") || strings.Contains(lower, "email"):
return "Email is required"
default:
return message
}
}
+105 -7
View File
@@ -2,6 +2,7 @@ package v1
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
@@ -67,7 +68,12 @@ func (s *PortalHandlerTestSuite) TestCreate_Success() {
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusCreated, w.Code)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.Equal(s.T(), "test-portal", payload["name"])
assert.Equal(s.T(), "test-slug", payload["slug"])
assert.NotContains(s.T(), payload, "success")
}
func (s *PortalHandlerTestSuite) TestGet_Success() {
@@ -82,6 +88,41 @@ func (s *PortalHandlerTestSuite) TestGet_Success() {
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.Equal(s.T(), "get-slug", payload["slug"])
assert.Contains(s.T(), payload, "meta")
}
func (s *PortalHandlerTestSuite) TestGet_BySlugReturnsChatwootMeta() {
portal := &model.Portal{AccountID: s.account.ID, Name: "slug-portal", Slug: "sluggy", Locale: "en", PortalConfiguration: json.RawMessage(`{"allowed_locales":["en","fr"],"default_locale":"en","draft_locales":["fr"],"layout":"header"}`)}
s.Require().NoError(s.db.Create(portal).Error)
category := &model.Category{AccountID: s.account.ID, PortalID: portal.ID, Name: "Basics", Slug: "basics", Locale: "en"}
s.Require().NoError(s.db.Create(category).Error)
authorID := uint(9)
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, CategoryID: &category.ID, AuthorID: &authorID, Title: "Published", Slug: "published", Status: "published", Locale: "en"}).Error)
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, Title: "Draft", Slug: "draft", Status: "draft", Locale: "fr"}).Error)
r := gin.New()
r.GET("/api/v1/accounts/:account_id/portals/:portal_id", s.handler.Get)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/portals/sluggy?locale=en", s.account.ID), nil)
req.Header.Set("X-User-ID", "9")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.Equal(s.T(), "sluggy", payload["slug"])
meta := payload["meta"].(map[string]any)
assert.EqualValues(s.T(), 1, meta["all_articles_count"])
assert.EqualValues(s.T(), 1, meta["published_count"])
assert.EqualValues(s.T(), 1, meta["mine_articles_count"])
config := payload["config"].(map[string]any)
assert.Equal(s.T(), "header", config["layout"])
locales := config["allowed_locales"].([]any)
assert.Len(s.T(), locales, 2)
}
func (s *PortalHandlerTestSuite) TestUpdate_Success() {
@@ -89,15 +130,19 @@ func (s *PortalHandlerTestSuite) TestUpdate_Success() {
s.Require().NoError(s.db.Create(portal).Error)
r := gin.New()
r.PUT("/api/v1/accounts/:account_id/portals/:portal_id", s.handler.Update)
r.PATCH("/api/v1/accounts/:account_id/portals/:portal_id", s.handler.Update)
w := httptest.NewRecorder()
body := `{"name":"updated-portal","slug":"updated-slug"}`
req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/portals/%d", s.account.ID, portal.ID), bytes.NewBufferString(body))
body := `{"name":"updated-portal","slug":"updated-slug","custom_domain":""}`
req, _ := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/portals/update-slug", s.account.ID), bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.Equal(s.T(), "updated-slug", payload["slug"])
assert.Equal(s.T(), "", payload["custom_domain"])
}
func (s *PortalHandlerTestSuite) TestDelete_Success() {
@@ -111,7 +156,60 @@ func (s *PortalHandlerTestSuite) TestDelete_Success() {
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/portals/%d", s.account.ID, portal.ID), nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusNoContent, w.Code)
assert.Equal(s.T(), http.StatusOK, w.Code)
}
func (s *PortalHandlerTestSuite) TestList_ReturnsChatwootPayloadEnvelope() {
s.Require().NoError(s.db.Create(&model.Portal{AccountID: s.account.ID, Name: "List One", Slug: "list-one"}).Error)
s.Require().NoError(s.db.Create(&model.Portal{AccountID: s.account.ID, Name: "List Two", Slug: "list-two"}).Error)
r := gin.New()
r.GET("/api/v1/accounts/:account_id/portals", s.handler.List)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/portals?page=2", s.account.ID), nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.NotContains(s.T(), payload, "data")
assert.Len(s.T(), payload["payload"], 2)
meta := payload["meta"].(map[string]any)
assert.Equal(s.T(), "2", meta["current_page"])
assert.EqualValues(s.T(), 2, meta["portals_count"])
}
func (s *PortalHandlerTestSuite) TestDelete_BySlugReturnsOK() {
portal := &model.Portal{AccountID: s.account.ID, Name: "delete-slug-portal", Slug: "delete-by-slug"}
s.Require().NoError(s.db.Create(portal).Error)
r := gin.New()
r.DELETE("/api/v1/accounts/:account_id/portals/:portal_id", s.handler.Delete)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/portals/delete-by-slug", s.account.ID), nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
}
func (s *PortalHandlerTestSuite) TestSendInstructions_ChatwootPayload() {
portal := &model.Portal{AccountID: s.account.ID, Name: "domain-portal", Slug: "domain-portal", CustomDomain: "help.example.com"}
s.Require().NoError(s.db.Create(portal).Error)
r := gin.New()
r.POST("/api/v1/accounts/:account_id/portals/:portal_id/send_instructions", s.handler.SendInstructions)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/portals/domain-portal/send_instructions", s.account.ID), bytes.NewBufferString(`{"email":"agent@example.com"}`))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.Equal(s.T(), "Instructions sent successfully", payload["message"])
}
func (s *PortalHandlerTestSuite) TestCreate_BadRequest_InvalidAccountID() {
@@ -135,5 +233,5 @@ func (s *PortalHandlerTestSuite) TestGet_BadRequest_InvalidID() {
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/portals/abc", s.account.ID), nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
assert.Equal(s.T(), http.StatusNotFound, w.Code)
}