Align GoChat with Chatwoot frontend contracts
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
imagepng "image/png"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -62,10 +63,35 @@ func (s *ArticleHandlerTestSuite) TearDownSuite() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) SetupTest() {
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM background_jobs").Error)
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM articles").Error)
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM categories").Error)
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM folders").Error)
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM portal_members").Error)
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM portals").Error)
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM users").Error)
|
||||
s.portal = &model.Portal{AccountID: s.account.ID, Name: "test-portal", Slug: "test-portal"}
|
||||
s.Require().NoError(s.db.Create(s.portal).Error)
|
||||
}
|
||||
|
||||
func TestArticleHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(ArticleHandlerTestSuite))
|
||||
}
|
||||
|
||||
func assertDashboardArticlePayload(t *testing.T, payload map[string]interface{}, expected map[string]interface{}) {
|
||||
t.Helper()
|
||||
for key, value := range expected {
|
||||
assert.Equal(t, value, payload[key], "article payload field %s", key)
|
||||
}
|
||||
for _, key := range []string{"id", "slug", "title", "content", "description", "status", "position", "account_id", "updated_at", "meta", "category", "views", "associated_articles"} {
|
||||
assert.Contains(t, payload, key)
|
||||
}
|
||||
assert.IsType(t, map[string]interface{}{}, payload["meta"])
|
||||
assert.IsType(t, map[string]interface{}{}, payload["category"])
|
||||
assert.IsType(t, []interface{}{}, payload["associated_articles"])
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) TestCreate_BadRequest_InvalidAccountID() {
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/accounts/:account_id/portals/:portal_id/articles", s.handler.Create)
|
||||
@@ -273,6 +299,7 @@ func (s *ArticleHandlerTestSuite) TestPublicMarkdown_ReturnsOnlyPublishedMarkdow
|
||||
portal := &model.Portal{AccountID: s.account.ID, Name: "Markdown Portal", Slug: "markdown-portal"}
|
||||
s.Require().NoError(s.db.Create(portal).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, Title: "Markdown", Slug: "markdown", Content: "# Raw markdown", Status: "published", Locale: "en"}).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, Title: "Markdown FR", Slug: "markdown-fr", Content: "# Contenu français", Status: "published", Locale: "fr"}).Error)
|
||||
s.Require().NoError(s.db.Create(&model.Article{AccountID: s.account.ID, PortalID: portal.ID, Title: "Draft", Slug: "markdown-draft", Content: "draft", Status: "draft", Locale: "en"}).Error)
|
||||
|
||||
r := gin.New()
|
||||
@@ -286,6 +313,14 @@ func (s *ArticleHandlerTestSuite) TestPublicMarkdown_ReturnsOnlyPublishedMarkdow
|
||||
assert.Equal(s.T(), "text/markdown; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
assert.Equal(s.T(), "# Raw markdown", w.Body.String())
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/hc/markdown-portal/articles/markdown-fr.md", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Equal(s.T(), "text/markdown; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
assert.Equal(s.T(), "# Contenu français", w.Body.String())
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/hc/markdown-portal/articles/markdown-draft.md", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
@@ -298,6 +333,8 @@ func (s *ArticleHandlerTestSuite) TestPublicTrackingPixel_IncrementsPublishedArt
|
||||
s.Require().NoError(s.db.Create(portal).Error)
|
||||
article := &model.Article{AccountID: s.account.ID, PortalID: portal.ID, Title: "Pixel", Slug: "pixel", Content: "pixel", Status: "published", Locale: "en", Views: 3}
|
||||
s.Require().NoError(s.db.Create(article).Error)
|
||||
draft := &model.Article{AccountID: s.account.ID, PortalID: portal.ID, Title: "Pixel Draft", Slug: "pixel-draft", Content: "pixel draft", Status: "draft", Locale: "en", Views: 8}
|
||||
s.Require().NoError(s.db.Create(draft).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/hc/:slug/articles/:article_slug", s.handler.PublicArticle)
|
||||
@@ -308,10 +345,27 @@ func (s *ArticleHandlerTestSuite) TestPublicTrackingPixel_IncrementsPublishedArt
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Equal(s.T(), "image/png", w.Header().Get("Content-Type"))
|
||||
assert.NotEmpty(s.T(), w.Body.Bytes())
|
||||
assert.Equal(s.T(), "private, max-age=86400", w.Header().Get("Cache-Control"))
|
||||
assert.Equal(s.T(), publicArticleTrackingPixelPNG, w.Body.Bytes())
|
||||
config, err := imagepng.DecodeConfig(bytes.NewReader(w.Body.Bytes()))
|
||||
s.Require().NoError(err)
|
||||
assert.Equal(s.T(), 1, config.Width)
|
||||
assert.Equal(s.T(), 1, config.Height)
|
||||
var updated model.Article
|
||||
s.Require().NoError(s.db.First(&updated, article.ID).Error)
|
||||
assert.Equal(s.T(), 4, updated.Views)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/hc/pixel-portal/articles/pixel-draft.png", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Equal(s.T(), "image/png", w.Header().Get("Content-Type"))
|
||||
assert.Equal(s.T(), "private, max-age=86400", w.Header().Get("Cache-Control"))
|
||||
assert.Equal(s.T(), publicArticleTrackingPixelPNG, w.Body.Bytes())
|
||||
var unchangedDraft model.Article
|
||||
s.Require().NoError(s.db.First(&unchangedDraft, draft.ID).Error)
|
||||
assert.Equal(s.T(), 8, unchangedDraft.Views)
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) TestStatusCounts_BadRequest_InvalidPortalID() {
|
||||
@@ -450,6 +504,44 @@ func (s *ArticleHandlerTestSuite) TestPublicList_WidgetPopularArticles() {
|
||||
assert.EqualValues(s.T(), 2, meta["articles_count"])
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) TestPublicList_CategoryRouteFiltersArticlesBySlugAndLocale() {
|
||||
r := gin.New()
|
||||
r.GET("/hc/:slug/:locale/categories/:category_slug/articles.json", s.handler.PublicList)
|
||||
|
||||
category := &model.Category{AccountID: s.account.ID, PortalID: s.portal.ID, Name: "Guides", Slug: "guides", Locale: "en"}
|
||||
otherCategory := &model.Category{AccountID: s.account.ID, PortalID: s.portal.ID, Name: "Other", Slug: "other", Locale: "en"}
|
||||
s.Require().NoError(s.db.Create(category).Error)
|
||||
s.Require().NoError(s.db.Create(otherCategory).Error)
|
||||
inCategory := &model.Article{AccountID: s.account.ID, PortalID: s.portal.ID, CategoryID: &category.ID, Title: "Guide", Slug: "category-guide", Status: "published", Locale: "en", Position: 1, Content: "guide"}
|
||||
other := &model.Article{AccountID: s.account.ID, PortalID: s.portal.ID, CategoryID: &otherCategory.ID, Title: "Other", Slug: "category-other", Status: "published", Locale: "en", Position: 2, Content: "other"}
|
||||
french := &model.Article{AccountID: s.account.ID, PortalID: s.portal.ID, CategoryID: &category.ID, Title: "French Guide", Slug: "category-guide-fr", Status: "published", Locale: "fr", Position: 3, Content: "guide fr"}
|
||||
draft := &model.Article{AccountID: s.account.ID, PortalID: s.portal.ID, CategoryID: &category.ID, Title: "Draft Guide", Slug: "category-guide-draft", Status: "draft", Locale: "en", Position: 4, Content: "draft"}
|
||||
s.Require().NoError(s.db.Create(inCategory).Error)
|
||||
s.Require().NoError(s.db.Create(other).Error)
|
||||
s.Require().NoError(s.db.Create(french).Error)
|
||||
s.Require().NoError(s.db.Create(draft).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/hc/test-portal/en/categories/guides/articles.json", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
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, 1)
|
||||
article := payload[0].(map[string]interface{})
|
||||
assert.Equal(s.T(), "Guide", article["title"])
|
||||
assert.Equal(s.T(), "category-guide", article["slug"])
|
||||
assert.Equal(s.T(), "guides", article["category"].(map[string]interface{})["slug"])
|
||||
assert.NotContains(s.T(), w.Body.String(), "category-other")
|
||||
assert.NotContains(s.T(), w.Body.String(), "category-guide-fr")
|
||||
assert.NotContains(s.T(), w.Body.String(), "category-guide-draft")
|
||||
meta := resp["meta"].(map[string]interface{})
|
||||
assert.EqualValues(s.T(), 1, meta["articles_count"])
|
||||
assert.EqualValues(s.T(), 1, meta["current_page"])
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) TestPublicList_ArchivedPortalNotFound() {
|
||||
r := gin.New()
|
||||
r.GET("/hc/:slug/:locale/articles.json", s.handler.PublicList)
|
||||
@@ -490,6 +582,103 @@ func (s *ArticleHandlerTestSuite) TestCreate_RawFrontendPayloadAndSlugPortal() {
|
||||
assert.NotEmpty(s.T(), payload["slug"])
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) TestDashboardArticleCRUD_ChatwootFrontendPayloadsAndRoutes() {
|
||||
category := &model.Category{AccountID: s.account.ID, PortalID: s.portal.ID, Name: "Dashboard Guides", Slug: "dashboard-guides", Locale: "en"}
|
||||
s.Require().NoError(s.db.Create(category).Error)
|
||||
author := &model.User{Name: "Article Author", Email: "article-author@example.com", Role: "agent"}
|
||||
s.Require().NoError(s.db.Create(author).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/accounts/:account_id/portals/:portal_id/articles", s.handler.Create)
|
||||
r.GET("/api/v1/accounts/:account_id/portals/:portal_id/articles", s.handler.List)
|
||||
r.GET("/api/v1/accounts/:account_id/portals/:portal_id/articles/:article_id", s.handler.Get)
|
||||
r.GET("/api/v1/accounts/:account_id/portals/:portal_id/articles/:article_id/edit", s.handler.Edit)
|
||||
r.PATCH("/api/v1/accounts/:account_id/portals/:portal_id/articles/:article_id", s.handler.Update)
|
||||
r.DELETE("/api/v1/accounts/:account_id/portals/:portal_id/articles/:article_id", s.handler.Delete)
|
||||
|
||||
createBody := fmt.Sprintf(`{"article":{"title":"Dashboard CRUD Article","content":"# dashboard content","description":"dashboard description","status":"draft","author_id":%d,"category_id":%d,"locale":"en","position":7,"meta":{"seo_title":"Dashboard SEO"}}}`, author.ID, category.ID)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/portals/test-portal/articles", s.account.ID), bytes.NewBufferString(createBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var createResp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &createResp))
|
||||
created := createResp["payload"].(map[string]interface{})
|
||||
articleID := uint(created["id"].(float64))
|
||||
assertDashboardArticlePayload(s.T(), created, map[string]interface{}{
|
||||
"title": "Dashboard CRUD Article",
|
||||
"content": "# dashboard content",
|
||||
"description": "dashboard description",
|
||||
"status": "draft",
|
||||
"position": float64(7),
|
||||
})
|
||||
assert.Contains(s.T(), created["slug"], "dashboard-crud-article")
|
||||
assert.Equal(s.T(), "Dashboard Guides", created["category"].(map[string]interface{})["name"])
|
||||
assert.Equal(s.T(), "Article Author", created["author"].(map[string]interface{})["name"])
|
||||
assert.Equal(s.T(), "Dashboard SEO", created["meta"].(map[string]interface{})["seo_title"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/portals/test-portal/articles?status=draft&locale=en&category_slug=dashboard-guides&sort=position", s.account.ID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var listResp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &listResp))
|
||||
articles := listResp["payload"].([]interface{})
|
||||
s.Require().Len(articles, 1)
|
||||
listed := articles[0].(map[string]interface{})
|
||||
assert.EqualValues(s.T(), articleID, listed["id"])
|
||||
meta := listResp["meta"].(map[string]interface{})
|
||||
assert.EqualValues(s.T(), 1, meta["articles_count"])
|
||||
assert.Contains(s.T(), meta, "all_articles_count")
|
||||
assert.Contains(s.T(), meta, "draft_articles_count")
|
||||
assert.Contains(s.T(), meta, "published_count")
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/portals/test-portal/articles/%d/edit", s.account.ID, articleID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var editResp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &editResp))
|
||||
assert.Equal(s.T(), "# dashboard content", editResp["payload"].(map[string]interface{})["content"])
|
||||
|
||||
updateBody := `{"article":{"title":"Dashboard CRUD Article Updated","content":"updated content","description":"","status":"published","position":3,"meta":{"seo_description":"updated seo"}}}`
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/portals/test-portal/articles/%d", s.account.ID, articleID), bytes.NewBufferString(updateBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var updateResp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &updateResp))
|
||||
updated := updateResp["payload"].(map[string]interface{})
|
||||
assertDashboardArticlePayload(s.T(), updated, map[string]interface{}{
|
||||
"title": "Dashboard CRUD Article Updated",
|
||||
"content": "updated content",
|
||||
"description": "",
|
||||
"status": "published",
|
||||
"position": float64(3),
|
||||
})
|
||||
assert.Equal(s.T(), "updated seo", updated["meta"].(map[string]interface{})["seo_description"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/portals/test-portal/articles/%d", s.account.ID, articleID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var showResp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &showResp))
|
||||
shown := showResp["payload"].(map[string]interface{})
|
||||
assert.EqualValues(s.T(), 1, shown["views"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/portals/test-portal/articles/%d", s.account.ID, articleID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var count int64
|
||||
s.Require().NoError(s.db.Model(&model.Article{}).Where("id = ?", articleID).Count(&count).Error)
|
||||
assert.EqualValues(s.T(), 0, count)
|
||||
}
|
||||
|
||||
func (s *ArticleHandlerTestSuite) TestPatch_RawPayloadClearsDescription() {
|
||||
article := &model.Article{AccountID: s.account.ID, PortalID: s.portal.ID, Title: "patch-article", Slug: "patch-article", Description: "old", Status: "draft"}
|
||||
s.Require().NoError(s.db.Create(article).Error)
|
||||
|
||||
Reference in New Issue
Block a user