Files
RenewCMS/test/seo_data_cleaner_persistence_test.go

195 lines
6.7 KiB
Go

package test
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/site"
"RenewCMS/internal/domain/slug"
entity "RenewCMS/internal/infrastructure/persistence/models"
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
"errors"
"reflect"
"strings"
"testing"
"gorm.io/gorm"
)
func legacyWithSlugs(t *testing.T, slugs map[uint32]string, rows ...seoLegacyRow) *gorm.DB {
t.Helper()
db := openPreSliceDatabase(t, rows...)
migrateSeo(t, db)
if err := db.Exec("DROP INDEX idx_articles_site_slug").Error; err != nil {
t.Fatal(err)
}
for id, value := range slugs {
if err := db.Unscoped().Model(&entity.Article{}).Where("id = ?", id).UpdateColumn("slug", value).Error; err != nil {
t.Fatal(err)
}
}
return db
}
func TestSeoDataCleanerBackfillRepair(t *testing.T) {
rows := func(count int) []seoLegacyRow {
result := []seoLegacyRow{}
for id := 1; id <= count; id++ {
result = append(result, seoLegacyRow{id: uint32(id), siteID: 1, title: "Row"})
}
return result
}
t.Run("Live duplicates keep the lowest id and the others are suffixed", func(t *testing.T) {
db := legacyWithSlugs(t, map[uint32]string{1: "news", 2: "news", 3: "news", 4: "news-2", 5: "news"}, rows(5)...)
if err := db.Model(&entity.Article{}).Where("id = ?", 5).UpdateColumn("site_id", 2).Error; err != nil {
t.Fatal(err)
}
migrateSeo(t, db)
want := map[uint32]string{1: "news", 2: "news-3", 3: "news-4", 4: "news-2", 5: "news"}
if got := storedSlugs(t, db); !reflect.DeepEqual(got, want) {
t.Errorf("slugs = %v", got)
}
if !db.Migrator().HasIndex(&entity.Article{}, "idx_articles_site_slug") {
t.Error("the index is missing")
}
})
t.Run("A soft-deleted row sharing a slug with a live row is tombstoned", func(t *testing.T) {
db := legacyWithSlugs(t, map[uint32]string{1: "news", 2: "news"}, rows(2)...)
if err := db.Delete(&entity.Article{}, 1).Error; err != nil {
t.Fatal(err)
}
migrateSeo(t, db)
if got := storedSlugs(t, db); !reflect.DeepEqual(got, map[uint32]string{1: article.Tombstone(1), 2: "news"}) {
t.Errorf("slugs = %v", got)
}
})
t.Run("Every soft-deleted row is tombstoned whatever its slug", func(t *testing.T) {
db := legacyWithSlugs(t, map[uint32]string{1: "custom", 2: "", 3: "other"}, rows(3)...)
if err := db.Delete(&entity.Article{}, []uint32{1, 2}).Error; err != nil {
t.Fatal(err)
}
migrateSeo(t, db)
if got := storedSlugs(t, db); !reflect.DeepEqual(got, map[uint32]string{1: "#deleted-1", 2: "#deleted-2", 3: "other"}) {
t.Errorf("slugs = %v", got)
}
})
t.Run("A second run changes nothing", func(t *testing.T) {
db := legacyWithSlugs(t, map[uint32]string{1: "news", 2: "news", 3: "gone"}, rows(3)...)
if err := db.Delete(&entity.Article{}, 3).Error; err != nil {
t.Fatal(err)
}
migrateSeo(t, db)
first := storedSlugs(t, db)
migrateSeo(t, db)
if second := storedSlugs(t, db); !reflect.DeepEqual(first, second) || first[2] != "news-2" || first[3] != "#deleted-3" {
t.Errorf("first %v second %v", first, second)
}
})
}
func TestSeoDataCleanerBackfillAlgorithmIsFrozen(t *testing.T) {
titles := []string{"Café Déjà Vu", "Straße & Æon", "!!!", "Ça va?", strings.Repeat("a", 120), strings.Repeat("ab-", 40), "日本語", " Spaced Out ", "Ø Ł Ñ", "Hello, World 2024"}
legacy := []seoLegacyRow{}
for index, title := range titles {
legacy = append(legacy, seoLegacyRow{id: uint32(index + 1), siteID: 1, title: title})
}
db := openPreSliceDatabase(t, legacy...)
migrateSeo(t, db)
want := map[uint32]string{
1: "cafe-deja-vu", 2: "strasse-aeon", 3: "article", 4: "ca-va", 5: strings.Repeat("a", 100),
6: strings.TrimRight(strings.Repeat("ab-", 40)[:100], "-"), 7: "article-2", 8: "spaced-out", 9: "o-l-n", 10: "hello-world-2024",
}
if got := storedSlugs(t, db); !reflect.DeepEqual(got, want) {
t.Errorf("slugs = %v", got)
}
}
func TestSeoDataCleanerTombstone(t *testing.T) {
if article.Tombstone(7) != "#deleted-7" || slug.IsValid(article.Tombstone(7)) || slug.IsValid(article.Tombstone(123456)) {
t.Errorf("tombstone %q", article.Tombstone(7))
}
db := openPreSliceDatabase(t)
migrateSeo(t, db)
articles := persistence.NewArticleRepository(db)
created, err := articles.Create(article.Article{Title: "Gone", SiteID: 1, Slug: "gone", SEODismissed: []string{}})
if err != nil {
t.Fatal(err)
}
if err := articles.Delete(created.ID); err != nil {
t.Fatal(err)
}
if got := storedSlugs(t, db)[created.ID]; got != article.Tombstone(created.ID) {
t.Errorf("slug after delete %q", got)
}
}
func TestSeoDataCleanerEmptyValues(t *testing.T) {
t.Run("Empty SEO text and site text insert after two migrations", func(t *testing.T) {
db := openPreSliceDatabase(t)
migrateSeo(t, db)
migrateSeo(t, db)
sites := persistence.NewSiteRepository(db)
articles := persistence.NewArticleRepository(db)
created, siteErr := sites.Create(site.Site{Name: "Blank", Key: "blank", BaseURL: "https://blank.example.com", DefaultRobots: site.RobotsIndex, TitleTemplate: site.DefaultTitleTemplate})
stored, articleErr := articles.Create(article.Article{Title: "Blank", SiteID: created.ID, Slug: "blank", SEODismissed: []string{}})
if siteErr != nil || articleErr != nil {
t.Fatalf("site %v article %v", siteErr, articleErr)
}
reloaded, err := sites.Get(created.ID)
found, foundErr := articles.Get(stored.ID)
if err != nil || reloaded.SiteName != "" || reloaded.DefaultDescription != "" || foundErr != nil || found.SEOTitle != "" || found.SEODescription != "" || found.Excerpt != "" {
t.Errorf("site %+v (%v) article %+v (%v)", reloaded, err, found, foundErr)
}
})
t.Run("An empty stored title template reads as the default", func(t *testing.T) {
db := openPreSliceDatabase(t)
migrateSeo(t, db)
sites := persistence.NewSiteRepository(db)
created := mustCreateSite(t, sites, "Blank", "blank", false)
if err := db.Model(&entity.Site{}).Where("id = ?", created.ID).UpdateColumn("title_template", "").Error; err != nil {
t.Fatal(err)
}
got, err := sites.Get(created.ID)
listed, listErr := sites.List()
if err != nil || got.TitleTemplate != site.DefaultTitleTemplate || listErr != nil {
t.Fatalf("got %q %v %v", got.TitleTemplate, err, listErr)
}
for _, entry := range listed {
if entry.TitleTemplate == "" {
t.Errorf("listed site %d has an empty template", entry.ID)
}
}
})
}
func TestSeoDataCleanerSiteKeyRace(t *testing.T) {
db := openPreSliceDatabase(t)
migrateSeo(t, db)
sites := persistence.NewSiteRepository(db)
mustCreateSite(t, sites, "Shop", "shop", false)
_, err := sites.Create(site.Site{Name: "Again", Key: "shop", BaseURL: "https://again.example.com", DefaultRobots: site.RobotsIndex})
if !errors.Is(err, site.ErrSiteKeyTaken) {
t.Errorf("err = %v", err)
}
}