Files
RenewCMS/test/seo_output_list_http_test.go
T

235 lines
8.2 KiB
Go

package test
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/internal/domain/article"
"net/http"
"reflect"
"strconv"
"testing"
"time"
)
func (e *seoOutEnv) cleanArticles() {
delete(e.articles.articles, seoOutArticleID)
delete(e.articles.articles, seoOutDraftID)
}
func (e *seoOutEnv) seedListed(id uint32, title string, online bool, mutate func(*article.Article)) {
e.credit(seoOutAuthorMarta, "Marta Alves", "")
e.articles.credits[seoOutArticleID] = nil
e.articles.credit(id, seoOutAuthorMarta)
e.articles.articles[id] = article.Article{
ID: id, Title: title, Body: textBody(400, "<h2>h</h2>"), IsOnline: online, SiteID: siteMainID, Slug: "slug-" + strconv.Itoa(int(id)),
SEOTitle: repeated("t", 45), SEODescription: repeated("d", 100), CoverImageID: idPointer(5), CoverImageAlt: "alt",
SEODismissed: []string{}, CreatedAt: seoOutCreated, UpdatedAt: seoOutCreated.Add(time.Duration(id) * time.Hour),
}
if mutate != nil {
found := e.articles.articles[id]
mutate(&found)
e.articles.articles[id] = found
}
}
func (e *seoOutEnv) seedFourListed() {
e.cleanArticles()
e.seedListed(1, "Ready", true, nil)
e.seedListed(2, "Two", true, func(a *article.Article) { a.CoverImageID, a.CoverImageAlt = nil, ""; a.SEOTitle = repeated("t", 70) })
e.seedListed(3, "Hidden", true, func(a *article.Article) {
a.Noindex = true
a.CoverImageID, a.CoverImageAlt = nil, ""
a.SEOTitle = repeated("t", 70)
})
e.seedListed(4, "Four", true, func(a *article.Article) {
a.CoverImageID, a.CoverImageAlt = nil, ""
a.SEOTitle = repeated("t", 70)
a.Slug = repeated("s", 80)
a.SEODismissed = []string{"slug_long"}
})
}
func (e *seoOutEnv) list(t *testing.T, who string, query string) map[string]any {
t.Helper()
rec := e.call(t, who, http.MethodGet, "/v1/article/"+query, "")
if rec.Code != http.StatusOK {
t.Fatalf("list %s: %d %s", query, rec.Code, rec.Body.String())
}
return decodeSeoObject(t, rec)
}
func itemByID(list map[string]any, id int) map[string]any {
for _, item := range list["items"].([]any) {
if found := item.(map[string]any); int(found["id"].(float64)) == id {
return found
}
}
return nil
}
func TestSeoOutputList(t *testing.T) {
t.Run("Items carry seo_status and seo_suggestions", func(t *testing.T) {
e := newSeoOutEnv(t)
e.seedFourListed()
list := e.list(t, "owner", "")
want := map[int][2]any{1: {"ready", 0.0}, 2: {"suggestions", 2.0}, 3: {"hidden", 2.0}, 4: {"suggestions", 2.0}}
for id, expected := range want {
item := itemByID(list, id)
if item["seo_status"] != expected[0] || item["seo_suggestions"] != expected[1] {
t.Errorf("item %d = %v %v", id, item["seo_status"], item["seo_suggestions"])
}
if _, present := item["seo_dismissed"]; present {
t.Errorf("item %d carries seo_dismissed", id)
}
if _, present := item["excerpt"]; !present {
t.Errorf("item %d has no excerpt", id)
}
}
})
t.Run("The list and the check agree", func(t *testing.T) {
e := newSeoOutEnv(t)
e.seedFourListed()
list := e.list(t, "owner", "")
for id := 1; id <= 4; id++ {
rec := e.call(t, "owner", http.MethodPost, "/v1/article/"+strconv.Itoa(id)+"/seo/check", `{}`)
checked := decodeSeoObject(t, rec)
item := itemByID(list, id)
if checked["status"] != item["seo_status"] || float64(len(checked["suggestions"].([]any))) != item["seo_suggestions"] {
t.Errorf("article %d: list %v/%v, check %v", id, item["seo_status"], item["seo_suggestions"], checked)
}
}
})
t.Run("Anonymous callers see online items with their status", func(t *testing.T) {
e := newSeoOutEnv(t)
e.seedFourListed()
e.seedListed(5, "Draft", false, nil)
list := e.list(t, "", "")
if !reflect.DeepEqual(listIDs(list), []int{4, 3, 2, 1}) || itemByID(list, 2)["seo_status"] != "suggestions" {
t.Errorf("ids = %v", listIDs(list))
}
})
t.Run("The seo filter", func(t *testing.T) {
cases := []struct {
value string
ids []int
total float64
}{{"ready", []int{1}, 1}, {"suggestions", []int{4, 2}, 2}, {"hidden", []int{3}, 1}}
for _, c := range cases {
e := newSeoOutEnv(t)
e.seedFourListed()
list := e.list(t, "owner", "?seo="+c.value)
if !reflect.DeepEqual(listIDs(list), c.ids) || list["total"] != c.total {
t.Errorf("%s: ids %v total %v", c.value, listIDs(list), list["total"])
}
}
})
t.Run("The filter composes with search, status, site, sort and paging", func(t *testing.T) {
e := newSeoOutEnv(t)
e.cleanArticles()
for id := uint32(1); id <= 7; id++ {
e.seedListed(id, "Tile "+strconv.Itoa(int(id)), false, func(a *article.Article) { a.SiteID = seoOutJournalID; a.CoverImageID, a.CoverImageAlt = nil, "" })
}
for id := uint32(8); id <= 10; id++ {
e.seedListed(id, "Tile "+strconv.Itoa(int(id)), true, func(a *article.Article) { a.SiteID = seoOutJournalID; a.CoverImageID, a.CoverImageAlt = nil, "" })
}
for id := uint32(11); id <= 14; id++ {
e.seedListed(id, "Tile "+strconv.Itoa(int(id)), false, func(a *article.Article) { a.CoverImageID, a.CoverImageAlt = nil, "" })
}
base := "?seo=suggestions&q=tile&status=draft&site=journal&sort=title:asc&size=5&page="
first, second := e.list(t, "owner", base+"1"), e.list(t, "owner", base+"2")
if first["total"] != 7.0 || !reflect.DeepEqual(listIDs(second), []int{6, 7}) {
t.Errorf("first %v second %v total %v", listIDs(first), listIDs(second), first["total"])
}
for _, id := range listIDs(first) {
for _, other := range listIDs(second) {
if id == other {
t.Errorf("id %d on both pages", id)
}
}
}
})
t.Run("Counts stay unfiltered", func(t *testing.T) {
e := newSeoOutEnv(t)
e.cleanArticles()
for id := uint32(1); id <= 6; id++ {
online := id <= 4
e.seedListed(id, "A"+strconv.Itoa(int(id)), online, func(a *article.Article) {
if id%2 == 0 {
a.CoverImageID, a.CoverImageAlt = nil, ""
}
})
}
list := e.list(t, "owner", "?seo=suggestions")
if !reflect.DeepEqual(list["counts"], map[string]any{"all": 6.0, "online": 4.0, "draft": 2.0}) || list["total"] != 3.0 {
t.Errorf("list = %v %v", list["counts"], list["total"])
}
})
t.Run("The filter is applied before paging", func(t *testing.T) {
e := newSeoOutEnv(t)
e.cleanArticles()
for id := uint32(1); id <= 30; id++ {
e.seedListed(id, "A"+strconv.Itoa(int(id)), true, func(a *article.Article) {
if id <= 12 {
a.CoverImageID, a.CoverImageAlt = nil, ""
}
})
}
list := e.list(t, "owner", "?seo=suggestions&size=5&page=3")
if len(listIDs(list)) != 2 || list["total"] != 12.0 {
t.Errorf("ids %v total %v", listIDs(list), list["total"])
}
})
t.Run("The filter respects visibility", func(t *testing.T) {
e := newSeoOutEnv(t)
e.cleanArticles()
e.seedListed(1, "Offline", false, func(a *article.Article) { a.CoverImageID, a.CoverImageAlt = nil, "" })
list := e.list(t, "", "?seo=suggestions")
if len(listIDs(list)) != 0 || list["total"] != 0.0 {
t.Errorf("list = %v", list)
}
})
t.Run("Dismissed codes are never counted in the filter", func(t *testing.T) {
e := newSeoOutEnv(t)
e.cleanArticles()
e.seedListed(1, "Dismissed", true, func(a *article.Article) {
a.CoverImageID, a.CoverImageAlt = nil, ""
a.SEODismissed = []string{"cover_none"}
})
if ids := listIDs(e.list(t, "owner", "?seo=suggestions")); len(ids) != 0 {
t.Errorf("ids = %v", ids)
}
})
t.Run("An invalid seo value is refused", func(t *testing.T) {
for _, query := range []string{"seo=maybe", "seo=", "seo=READY", "seo=ready,hidden", "seo=1"} {
e := newSeoOutEnv(t)
e.seedFourListed()
before := e.counted.readCalls()
expectStatusAndText(t, e.call(t, "owner", http.MethodGet, "/v1/article/?"+query, ""), 400, apperr.BodyErrorMessage)
if e.counted.readCalls() != before {
t.Errorf("%s: the repository was called", query)
}
}
})
t.Run("Invalid format is still reported first", func(t *testing.T) {
e := newSeoOutEnv(t)
expectStatusAndText(t, e.call(t, "owner", http.MethodGet, "/v1/article/?seo=maybe&format=pdf", ""), 400, apperr.ArticleBodyFormatErrorMessage)
})
t.Run("The existing list behaviour is unchanged without the filter", func(t *testing.T) {
e := newSeoOutEnv(t)
list := e.list(t, "owner", "")
if list["total"] != 2.0 || list["page"] != 1.0 || list["size"] != 25.0 || !reflect.DeepEqual(list["counts"], map[string]any{"all": 2.0, "online": 1.0, "draft": 1.0}) {
t.Errorf("list = %v", list)
}
})
}