Files
RenewCMS/test/seo_data_support_test.go

159 lines
5.9 KiB
Go

package test
import (
"RenewCMS/api"
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/site"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/go-chi/chi/v5"
)
const (
seoEditorID = 5
seoSpringID = 8
seoOtherID = 9
seoCoverImage = 5
seoSocialImage = 6
seoSlugHeader = "X-Slug-Suggestion"
seoSlugTaken = "slug_taken"
seoTakenReason = "That slug is already used by another article on this site."
seoSpringTitle = "Spring release"
seoSpringSlug = "spring-release"
)
type seoEnv struct {
*producersEnv
sites *mockSiteRepo
authors *mockAuthorRepo
}
func newSeoEnv(t *testing.T) *seoEnv {
t.Helper()
p := newProducersEnv(t)
p.users.users[seoEditorID] = user.User{ID: seoEditorID, Username: "edith", Email: "edith@example.com", IsVerified: true, Role: user.Role("editor")}
p.users.nextID = seoEditorID
authors := newMockAuthorRepo(p.articles)
sites := newMockSiteRepo(p.articles)
sites.seed(siteMainID, "Main", "main", true)
sites.seed(siteShopID, "Shop", "shop", false)
for id, seeded := range sites.sites {
seeded.TitleTemplate = site.DefaultTitleTemplate
sites.sites[id] = seeded
}
recorder := p.audit.recorder
renderer := &fakeBodyRenderer{}
mergeMedia := useCases.NewMergeMediaUseCase(p.images, p.articles, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites, authors)
c := p.container
c.CreateArticleUseCase = useCases.NewCreateArticleUseCase(p.articles, renderer, recorder, sites, authors)
c.UpdateArticleUseCase = useCases.NewUpdateArticleUseCase(p.articles, renderer, recorder, sites, authors, p.images)
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites)
c.ListMediaUseCase = useCases.NewListMediaUseCase(p.images, p.articles, p.users, newMockVariantFileRepo(), sites, authors)
c.DeleteMediaUseCase = useCases.NewDeleteMediaUseCase(p.images, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites, authors, p.articles)
c.GetMediaUsageUseCase = useCases.NewGetMediaUsageUseCase(p.images, p.articles, sites, authors)
c.MergeMediaUseCase = mergeMedia
c.RunPurgeUseCase = useCases.NewRunPurgeUseCase(p.images, p.articles, mergeMedia, newMockPurgeSettingsRepo(), func() time.Time { return p.now }, log.New(io.Discard, "", 0), recorder, sites, authors)
c.PreviewPurgeUseCase = useCases.NewPreviewPurgeUseCase(p.images, p.articles, sites, authors)
c.ListSitesUseCase = useCases.NewListSitesUseCase(sites)
c.CreateSiteUseCase = useCases.NewCreateSiteUseCase(sites, p.images, recorder)
c.UpdateSiteUseCase = useCases.NewUpdateSiteUseCase(sites, p.images, recorder)
c.SetDefaultSiteUseCase = useCases.NewSetDefaultSiteUseCase(sites, recorder)
c.DeleteSiteUseCase = useCases.NewDeleteSiteUseCase(sites, recorder)
c.ListActivityUseCase = useCases.NewListActivityUseCase(p.audit.store, p.users, p.articles, p.images, p.keys, p.audit.clock, sites, authors)
router := chi.NewRouter()
router.Mount("/v1", api.InitBackendRoutes(c, p.settings))
p.router = router
p.seedBanner(seoCoverImage, "cover.png", "hash-cover")
p.seedBanner(seoSocialImage, "social.png", "hash-social")
return &seoEnv{producersEnv: p, sites: sites, authors: authors}
}
func (e *seoEnv) seedArticle(id uint32, title string, siteID uint32, slug string) article.Article {
seeded := article.Article{ID: id, Title: title, Body: "Body of " + title, SiteID: siteID, Slug: slug, SEODismissed: []string{}}
e.articles.articles[id] = seeded
if id > e.articles.nextID {
e.articles.nextID = id
}
return seeded
}
func (e *seoEnv) seedSpring() article.Article {
return e.seedArticle(seoSpringID, seoSpringTitle, siteMainID, seoSpringSlug)
}
func (e *seoEnv) seedFullyOptimised(id uint32) {
stored := e.seedArticle(id, seoSpringTitle, siteMainID, "spring-full")
stored.Excerpt = "Short teaser."
stored.SEOTitle = "Spring release notes"
stored.SEODescription = "Everything new this spring."
stored.CoverImageID = idPointer(seoCoverImage)
stored.CoverImageAlt = "A field of tulips"
stored.SocialTitle = "Spring is here"
stored.SocialDescription = "Read the notes."
stored.SocialImageID = idPointer(seoSocialImage)
stored.CanonicalURL = "https://www.example.com/spring"
stored.Noindex = true
stored.SEODismissed = []string{"cover_alt", "short"}
e.articles.articles[id] = stored
}
func (e *seoEnv) patch(t *testing.T, id uint32, body string) *httptest.ResponseRecorder {
t.Helper()
return e.owner(t, http.MethodPatch, "/v1/article/"+strconv.Itoa(int(id)), body)
}
func (e *seoEnv) owner(t *testing.T, method string, path string, body string) *httptest.ResponseRecorder {
t.Helper()
return e.as(t, apiKeyOwnerId, method, path, body)
}
func (e *seoEnv) stored(id uint32) article.Article {
return e.articles.articles[id]
}
func decodeSeoObject(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
t.Helper()
var decoded map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("decode %q: %v", rec.Body.String(), err)
}
return decoded
}
func expectStatusAndText(t *testing.T, rec *httptest.ResponseRecorder, status int, body string) {
t.Helper()
if rec.Code != status || strings.TrimSpace(rec.Body.String()) != body {
t.Fatalf("expected %d %q, got %d %q", status, body, rec.Code, strings.TrimSpace(rec.Body.String()))
}
}
func expectStatusAndValidatorText(t *testing.T, rec *httptest.ResponseRecorder, status int) {
t.Helper()
if rec.Code != status || !strings.Contains(rec.Body.String(), "Field validation") {
t.Fatalf("expected %d with the validator text, got %d %q", status, rec.Code, rec.Body.String())
}
}
func repeated(character string, count int) string {
return strings.Repeat(character, count)
}
func jsonString(t *testing.T, value string) string {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return string(encoded)
}