327 lines
13 KiB
Go
327 lines
13 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
type PortalHandlerTestSuite struct {
|
|
suite.Suite
|
|
db *gorm.DB
|
|
handler *PortalHandler
|
|
account *model.Account
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) SetupSuite() {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
s.Require().NoError(err)
|
|
s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.Portal{}, &model.Article{}, &model.Category{}, &model.Folder{}, &model.PortalMember{}))
|
|
s.db = db
|
|
|
|
repo := repository.NewPortalRepo(db)
|
|
svc := service.NewPortalService(repo)
|
|
s.handler = NewPortalHandler(svc)
|
|
|
|
s.account = &model.Account{Name: "test-portal-account"}
|
|
s.Require().NoError(db.Create(s.account).Error)
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) SetupTest() {
|
|
s.db.Exec("DELETE FROM articles")
|
|
s.db.Exec("DELETE FROM categories")
|
|
s.db.Exec("DELETE FROM portals")
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TearDownSuite() {
|
|
if s.db != nil {
|
|
sqlDB, _ := s.db.DB()
|
|
sqlDB.Close()
|
|
}
|
|
}
|
|
|
|
func TestPortalHandlerSuite(t *testing.T) {
|
|
suite.Run(t, new(PortalHandlerTestSuite))
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestCreate_Success() {
|
|
r := gin.New()
|
|
r.POST("/api/v1/accounts/:account_id/portals", s.handler.Create)
|
|
|
|
w := httptest.NewRecorder()
|
|
body := fmt.Sprintf(`{"name":"test-portal","slug":"test-slug"}`)
|
|
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/portals", 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(), "test-portal", payload["name"])
|
|
assert.Equal(s.T(), "test-slug", payload["slug"])
|
|
assert.NotContains(s.T(), payload, "success")
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestGet_Success() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "get-portal", Slug: "get-slug"}
|
|
s.Require().NoError(s.db.Create(portal).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/%d", s.account.ID, portal.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.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) TestPublicRedirectDefaultLocale() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "Public", Slug: "public", Locale: "en", PortalConfiguration: json.RawMessage(`{"default_locale":"fr"}`)}
|
|
s.Require().NoError(s.db.Create(portal).Error)
|
|
|
|
r := gin.New()
|
|
r.GET("/hc/:slug", s.handler.PublicRedirectDefaultLocale)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/hc/public", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusFound, w.Code)
|
|
assert.Equal(s.T(), "/hc/public/fr", w.Header().Get("Location"))
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestPublicGet_ReturnsChatwootHCPortalPayload() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "Help", Slug: "help", HeaderText: "How can we help?", HomepageLink: "https://example.com", PageTitle: "Help Center", LogoURL: "https://cdn.example/logo.png", Locale: "en", PortalConfiguration: json.RawMessage(`{"default_locale":"en"}`)}
|
|
s.Require().NoError(s.db.Create(portal).Error)
|
|
category := &model.Category{AccountID: s.account.ID, PortalID: portal.ID, Name: "Basics", Slug: "basics", Locale: "en", Description: "Start here", Position: 1}
|
|
s.Require().NoError(s.db.Create(category).Error)
|
|
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, CategoryID: &category.ID, Title: "Published", Slug: "published", Status: "published", Locale: "en"}).Error)
|
|
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, CategoryID: &category.ID, Title: "Draft", Slug: "draft", Status: "draft", Locale: "en"}).Error)
|
|
|
|
r := gin.New()
|
|
r.GET("/hc/:slug/:locale", s.handler.PublicGet)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/hc/help/en.json", 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.Equal(s.T(), "help", payload["slug"])
|
|
assert.Equal(s.T(), "How can we help?", payload["header_text"])
|
|
assert.NotContains(s.T(), payload, "id")
|
|
assert.NotContains(s.T(), payload, "account_id")
|
|
categories := payload["categories"].([]any)
|
|
assert.Len(s.T(), categories, 1)
|
|
cat := categories[0].(map[string]any)
|
|
assert.Equal(s.T(), "basics", cat["slug"])
|
|
assert.EqualValues(s.T(), 1, cat["meta"].(map[string]any)["articles_count"])
|
|
meta := payload["meta"].(map[string]any)
|
|
assert.EqualValues(s.T(), 1, meta["articles_count"])
|
|
assert.EqualValues(s.T(), 1, meta["categories_count"])
|
|
assert.Equal(s.T(), "en", meta["default_locale"])
|
|
logo := payload["logo"].(map[string]any)
|
|
assert.Equal(s.T(), "https://cdn.example/logo.png", logo["file_url"])
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestPublicGet_NotFoundForArchivedPortal() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "Archived", Slug: "archived", Archived: true}
|
|
s.Require().NoError(s.db.Create(portal).Error)
|
|
|
|
r := gin.New()
|
|
r.GET("/hc/:slug/:locale", s.handler.PublicGet)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/hc/archived/en", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestPublicSitemap_ReturnsPublishedArticleURLs() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "Sitemap", Slug: "sitemap", CustomDomain: "help.example.com"}
|
|
s.Require().NoError(s.db.Create(portal).Error)
|
|
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, 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: "en"}).Error)
|
|
|
|
r := gin.New()
|
|
r.GET("/hc/:slug/sitemap.xml", s.handler.PublicSitemap)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/hc/sitemap/sitemap.xml", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
assert.Equal(s.T(), "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
|
|
body := w.Body.String()
|
|
assert.Contains(s.T(), body, `<?xml version="1.0" encoding="UTF-8"?>`)
|
|
assert.Contains(s.T(), body, `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`)
|
|
assert.Contains(s.T(), body, `<loc>https://help.example.com/hc/sitemap/articles/published</loc>`)
|
|
assert.Contains(s.T(), body, `<lastmod>`)
|
|
assert.NotContains(s.T(), body, "draft")
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestUpdate_Success() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "update-portal", Slug: "update-slug"}
|
|
s.Require().NoError(s.db.Create(portal).Error)
|
|
|
|
r := gin.New()
|
|
r.PATCH("/api/v1/accounts/:account_id/portals/:portal_id", s.handler.Update)
|
|
|
|
w := httptest.NewRecorder()
|
|
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() {
|
|
portal := &model.Portal{AccountID: s.account.ID, Name: "delete-portal", Slug: "delete-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/%d", s.account.ID, portal.ID), nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
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() {
|
|
r := gin.New()
|
|
r.POST("/api/v1/accounts/:account_id/portals", s.handler.Create)
|
|
|
|
w := httptest.NewRecorder()
|
|
body := `{"name":"test-portal","slug":"test-slug"}`
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/portals", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *PortalHandlerTestSuite) TestGet_BadRequest_InvalidID() {
|
|
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/abc", s.account.ID), nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
}
|