feat: SEO backend data (1/4)
This commit is contained in:
1 parent
cb1410f0b0
commit
0eb46b0d74
90 files changed
+6094
-350
No files matched your search
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user