436 lines
18 KiB
Go
436 lines
18 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/domain/site"
|
|
"RenewCMS/internal/infrastructure/persistence/database"
|
|
entity "RenewCMS/internal/infrastructure/persistence/models"
|
|
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type seoPreSliceArticle struct {
|
|
gorm.Model
|
|
ID uint32 `gorm:"primary_key;auto_increment;not_null"`
|
|
Title string
|
|
Body string
|
|
IsOnline bool `gorm:"not_null;default:false"`
|
|
SiteID uint32
|
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
}
|
|
|
|
func (seoPreSliceArticle) TableName() string { return "articles" }
|
|
|
|
type seoPreSliceSite struct {
|
|
ID uint32 `gorm:"primaryKey;autoIncrement"`
|
|
Name string `gorm:"size:100;not null"`
|
|
Key string `gorm:"column:site_key;size:32;not null;uniqueIndex"`
|
|
BaseURL string `gorm:"size:255;not null"`
|
|
DefaultSocialImageID *uint32
|
|
DefaultRobots string `gorm:"size:16;not null"`
|
|
Locale *string `gorm:"size:35"`
|
|
IsDefault bool `gorm:"not null"`
|
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
}
|
|
|
|
func (seoPreSliceSite) TableName() string { return "sites" }
|
|
|
|
type seoLegacyRow struct {
|
|
id uint32
|
|
siteID uint32
|
|
title string
|
|
slug string
|
|
}
|
|
|
|
func openPreSliceDatabase(t *testing.T, rows ...seoLegacyRow) *gorm.DB {
|
|
t.Helper()
|
|
db := openUnmigratedSQLite(t)
|
|
if err := db.AutoMigrate(&seoPreSliceArticle{}); err != nil {
|
|
t.Fatalf("pre-slice migrate: %v", err)
|
|
}
|
|
for _, row := range rows {
|
|
if err := db.Create(&seoPreSliceArticle{ID: row.id, Title: row.title, SiteID: row.siteID, Body: "Body"}).Error; err != nil {
|
|
t.Fatalf("seed %d: %v", row.id, err)
|
|
}
|
|
}
|
|
return db
|
|
}
|
|
|
|
func migrateSeo(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
if err := database.Migrate(db, migrationPublicURL); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
}
|
|
|
|
func storedSlugs(t *testing.T, db *gorm.DB) map[uint32]string {
|
|
t.Helper()
|
|
var rows []entity.Article
|
|
if err := db.Unscoped().Order("id ASC").Find(&rows).Error; err != nil {
|
|
t.Fatalf("read articles: %v", err)
|
|
}
|
|
slugs := map[uint32]string{}
|
|
for _, row := range rows {
|
|
slugs[row.ID] = row.Slug
|
|
}
|
|
return slugs
|
|
}
|
|
|
|
func TestSeoDataMigrationBackfill(t *testing.T) {
|
|
t.Run("Every existing article gets a slug from its title", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "Spring release"}, seoLegacyRow{id: 2, siteID: 1, title: "Autumn notes"})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if got := storedSlugs(t, db); !reflect.DeepEqual(got, map[uint32]string{1: "spring-release", 2: "autumn-notes"}) {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("Conflicts get suffixes in id order within a site", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"}, seoLegacyRow{id: 2, siteID: 1, title: "News"}, seoLegacyRow{id: 3, siteID: 1, title: "News"})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if got := storedSlugs(t, db); !reflect.DeepEqual(got, map[uint32]string{1: "news", 2: "news-2", 3: "news-3"}) {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("A suffix skips slugs that exist", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"}, seoLegacyRow{id: 2, siteID: 1, title: "News 2"}, seoLegacyRow{id: 3, siteID: 1, title: "News"})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
got := storedSlugs(t, db)
|
|
if got[1] != "news" || got[2] != "news-2" || got[3] != "news-3" {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("Slugs are unique per site, not globally", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"}, seoLegacyRow{id: 2, siteID: 2, title: "News"})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if got := storedSlugs(t, db); got[1] != "news" || got[2] != "news" {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("A title with no letter or digit becomes article, suffixed on conflict", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 5, siteID: 1, title: "!!!"}, seoLegacyRow{id: 6, siteID: 1, title: "???"}, seoLegacyRow{id: 7, siteID: 1, title: "..."})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if got := storedSlugs(t, db); !reflect.DeepEqual(got, map[uint32]string{5: "article", 6: "article-2", 7: "article-3"}) {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("Long titles stay within 100 characters", func(t *testing.T) {
|
|
title := strings.Repeat("a", 150)
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: title}, seoLegacyRow{id: 2, siteID: 1, title: title}, seoLegacyRow{id: 3, siteID: 1, title: title})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
seen := map[string]bool{}
|
|
for id, slug := range storedSlugs(t, db) {
|
|
if len(slug) > 100 || !article.IsValidSlug(slug) || seen[slug] {
|
|
t.Errorf("article %d slug %q (%d)", id, slug, len(slug))
|
|
}
|
|
seen[slug] = true
|
|
}
|
|
})
|
|
|
|
t.Run("The migration is idempotent", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"}, seoLegacyRow{id: 2, siteID: 1, title: "News"})
|
|
migrateSeo(t, db)
|
|
first := storedSlugs(t, db)
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if second := storedSlugs(t, db); !reflect.DeepEqual(first, second) {
|
|
t.Errorf("first %v, second %v", first, second)
|
|
}
|
|
})
|
|
|
|
t.Run("An edited slug survives a restart", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"})
|
|
migrateSeo(t, db)
|
|
if err := db.Model(&entity.Article{}).Where("id = ?", 1).UpdateColumn("slug", "custom").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if got := storedSlugs(t, db)[1]; got != "custom" {
|
|
t.Errorf("slug = %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("A database with a partially backfilled table completes", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"}, seoLegacyRow{id: 2, siteID: 1, title: "News"}, seoLegacyRow{id: 3, siteID: 1, title: "Other"})
|
|
migrateSeo(t, db)
|
|
if err := db.Exec("DROP INDEX idx_articles_site_slug").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Model(&entity.Article{}).Where("id = ?", 2).UpdateColumn("slug", "").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Model(&entity.Article{}).Where("id = ?", 1).UpdateColumn("slug", "kept").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrateSeo(t, db)
|
|
|
|
got := storedSlugs(t, db)
|
|
if got[1] != "kept" || got[3] != "other" || got[2] != "news" {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("Soft-deleted legacy articles get a slug that frees the title", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"}, seoLegacyRow{id: 2, siteID: 1, title: "News"})
|
|
if err := db.Delete(&seoPreSliceArticle{}, 1).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrateSeo(t, db)
|
|
|
|
got := storedSlugs(t, db)
|
|
if got[2] != "news" || got[1] != article.DeletedSlug+"1" {
|
|
t.Errorf("slugs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("New site columns get defaults", func(t *testing.T) {
|
|
db := openUnmigratedSQLite(t)
|
|
if err := db.AutoMigrate(&seoPreSliceSite{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&seoPreSliceSite{Name: "Shop", Key: "shop", BaseURL: "https://shop.example.com", DefaultRobots: "index", IsDefault: true}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrateSeo(t, db)
|
|
migrateSeo(t, db)
|
|
|
|
sites := storedSites(t, db)
|
|
if len(sites) != 1 || sites[0].SiteName != "" || sites[0].TitleTemplate != "%title% | %site%" || sites[0].DefaultDescription != "" {
|
|
t.Errorf("sites = %+v", sites)
|
|
}
|
|
})
|
|
|
|
t.Run("Existing articles get neutral SEO defaults", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
found, err := persistence.NewArticleRepository(db).Get(1)
|
|
want := article.Article{ID: 1, Title: "News", Body: "Body", SiteID: 1, Slug: "news", SEODismissed: []string{}, Authors: found.Authors, CreatedAt: found.CreatedAt, UpdatedAt: found.UpdatedAt}
|
|
if err != nil || !reflect.DeepEqual(found, want) {
|
|
t.Errorf("found %+v, %v", found, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The migration creates the per-site unique index", func(t *testing.T) {
|
|
db := openPreSliceDatabase(t, seoLegacyRow{id: 1, siteID: 1, title: "News"})
|
|
|
|
migrateSeo(t, db)
|
|
|
|
if !db.Migrator().HasIndex(&entity.Article{}, "idx_articles_site_slug") {
|
|
t.Fatal("the index is missing")
|
|
}
|
|
duplicate := entity.Article{Title: "Again", SiteID: 1, Slug: "news"}
|
|
if err := db.Create(&duplicate).Error; err == nil {
|
|
t.Error("a duplicate (site_id, slug) was accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSeoDataArticleRepositoryContract(t *testing.T) {
|
|
backends := []contractBackend{
|
|
{name: "sqlite", open: openContractSQLite},
|
|
{name: "mysql", open: openContractMySQL},
|
|
{name: "postgres", open: openContractPostgres},
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
run func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32)
|
|
}{
|
|
{"every new column round-trips with null image ids and the dismissed list", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
images := newContractRepositories(db)
|
|
cover := mustCreateImage(t, images, "cover.png", "hash-1")
|
|
social := mustCreateImage(t, images, "social.png", "hash-2")
|
|
created, err := articles.Create(article.Article{Title: "Spring", Body: "Body", SiteID: siteID, Slug: "spring", Excerpt: "Teaser", SEOTitle: "SEO", SEODescription: "Description", CoverImageID: &cover.ID, CoverImageAlt: "Alt", SocialTitle: "Social", SocialDescription: "Social description", SocialImageID: &social.ID, CanonicalURL: "https://example.com/a", Noindex: true, SEODismissed: []string{"short", "cover_alt"}})
|
|
plain, plainErr := articles.Create(article.Article{Title: "Plain", Body: "Body", SiteID: siteID, Slug: "plain"})
|
|
|
|
read, readErr := articles.Get(created.ID)
|
|
readPlain, _ := articles.Get(plain.ID)
|
|
|
|
if err != nil || readErr != nil || plainErr != nil {
|
|
t.Fatalf("errors: %v %v %v", err, readErr, plainErr)
|
|
}
|
|
if read.Slug != "spring" || read.Excerpt != "Teaser" || read.SEOTitle != "SEO" || read.SEODescription != "Description" || read.CoverImageID == nil || *read.CoverImageID != cover.ID || read.CoverImageAlt != "Alt" ||
|
|
read.SocialTitle != "Social" || read.SocialDescription != "Social description" || read.SocialImageID == nil || *read.SocialImageID != social.ID || read.CanonicalURL != "https://example.com/a" || !read.Noindex ||
|
|
!reflect.DeepEqual(read.SEODismissed, []string{"short", "cover_alt"}) {
|
|
t.Errorf("read = %+v", read)
|
|
}
|
|
if readPlain.CoverImageID != nil || readPlain.SocialImageID != nil || readPlain.Noindex || readPlain.SEODismissed == nil || len(readPlain.SEODismissed) != 0 {
|
|
t.Errorf("plain = %+v", readPlain)
|
|
}
|
|
}},
|
|
{"a second article with the same site and slug is refused, another site is allowed", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
sites := persistence.NewSiteRepository(db)
|
|
other := mustCreateSite(t, sites, "Shop", "shop", false)
|
|
if _, err := articles.Create(article.Article{Title: "One", SiteID: siteID, Slug: "spring"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, duplicate := articles.Create(article.Article{Title: "Two", SiteID: siteID, Slug: "spring"})
|
|
_, elsewhere := articles.Create(article.Article{Title: "Three", SiteID: other.ID, Slug: "spring"})
|
|
rawDuplicate := db.Create(&entity.Article{Title: "Four", SiteID: siteID, Slug: "spring"}).Error
|
|
|
|
if !errors.Is(duplicate, article.ErrSlugTaken) || elsewhere != nil || rawDuplicate == nil {
|
|
t.Errorf("duplicate %v, elsewhere %v, raw %v", duplicate, elsewhere, rawDuplicate)
|
|
}
|
|
}},
|
|
{"update applies the SEO fields, clears the image ids and refuses a taken slug", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
images := newContractRepositories(db)
|
|
cover := mustCreateImage(t, images, "cover.png", "hash-1")
|
|
first, _ := articles.Create(article.Article{Title: "One", SiteID: siteID, Slug: "one"})
|
|
second, _ := articles.Create(article.Article{Title: "Two", SiteID: siteID, Slug: "two"})
|
|
text, flag, codes := "Teaser", true, []string{"short"}
|
|
|
|
updated, err := articles.Update(first.ID, article.Update{Excerpt: &text, Noindex: &flag, SEODismissed: &codes, CoverImageID: article.Nullable[uint32]{Set: true, Value: &cover.ID}})
|
|
cleared, clearErr := articles.Update(first.ID, article.Update{CoverImageID: article.Nullable[uint32]{Set: true}})
|
|
taken := "two"
|
|
_, conflict := articles.Update(first.ID, article.Update{Slug: &taken})
|
|
own := "two"
|
|
_, keeping := articles.Update(second.ID, article.Update{Slug: &own})
|
|
|
|
if err != nil || updated.Excerpt != "Teaser" || !updated.Noindex || !reflect.DeepEqual(updated.SEODismissed, codes) || updated.CoverImageID == nil {
|
|
t.Fatalf("updated = %+v, %v", updated, err)
|
|
}
|
|
if clearErr != nil || cleared.CoverImageID != nil || cleared.Excerpt != "Teaser" {
|
|
t.Errorf("cleared = %+v, %v", cleared, clearErr)
|
|
}
|
|
if !errors.Is(conflict, article.ErrSlugTaken) || keeping != nil {
|
|
t.Errorf("conflict %v, keeping %v", conflict, keeping)
|
|
}
|
|
}},
|
|
{"moving an article re-checks its slug on the target site", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
other := mustCreateSite(t, persistence.NewSiteRepository(db), "Shop", "shop", false)
|
|
moving, _ := articles.Create(article.Article{Title: "One", SiteID: siteID, Slug: "spring"})
|
|
if _, err := articles.Create(article.Article{Title: "Two", SiteID: other.ID, Slug: "spring"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err := articles.Update(moving.ID, article.Update{SiteID: &other.ID})
|
|
|
|
stored, _ := articles.Get(moving.ID)
|
|
if !errors.Is(err, article.ErrSlugTaken) || stored.SiteID != siteID {
|
|
t.Errorf("err %v stored %+v", err, stored)
|
|
}
|
|
}},
|
|
{"slug taken ignores the article itself and a deleted article frees its slug", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
created, _ := articles.Create(article.Article{Title: "One", SiteID: siteID, Slug: "spring"})
|
|
|
|
own, ownErr := articles.SlugTaken(siteID, "spring", created.ID)
|
|
other, otherErr := articles.SlugTaken(siteID, "spring", 0)
|
|
deleteErr := articles.Delete(created.ID)
|
|
afterDelete, afterErr := articles.SlugTaken(siteID, "spring", 0)
|
|
_, recreated := articles.Create(article.Article{Title: "Again", SiteID: siteID, Slug: "spring"})
|
|
|
|
if ownErr != nil || otherErr != nil || afterErr != nil || own || !other || deleteErr != nil || afterDelete || recreated != nil {
|
|
t.Errorf("own %v other %v delete %v after %v recreated %v", own, other, deleteErr, afterDelete, recreated)
|
|
}
|
|
if err := articles.Delete(created.ID); !errors.Is(err, article.ErrArticleNotFound) {
|
|
t.Errorf("second delete: %v", err)
|
|
}
|
|
}},
|
|
{"image references are found, cleared, restored and repointed", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
images := newContractRepositories(db)
|
|
kept := mustCreateImage(t, images, "kept.png", "hash-a")
|
|
copied := mustCreateImage(t, images, "copy.png", "hash-a")
|
|
covered, _ := articles.Create(article.Article{Title: "Covered", SiteID: siteID, Slug: "covered", CoverImageID: &copied.ID})
|
|
shared, _ := articles.Create(article.Article{Title: "Shared", SiteID: siteID, Slug: "shared", CoverImageID: &copied.ID, SocialImageID: &copied.ID})
|
|
articles.Create(article.Article{Title: "Unrelated", SiteID: siteID, Slug: "unrelated"})
|
|
|
|
found, findErr := articles.FindByImage(copied.ID)
|
|
repointed, repointErr := articles.RepointImage([]uint32{copied.ID}, kept.ID)
|
|
references, clearErr := articles.ClearImage(kept.ID)
|
|
afterClear, _ := articles.Get(shared.ID)
|
|
restoreErr := articles.RestoreImage(references, kept.ID)
|
|
afterRestore, _ := articles.Get(shared.ID)
|
|
|
|
if findErr != nil || len(found) != 2 {
|
|
t.Fatalf("found %+v, %v", found, findErr)
|
|
}
|
|
if repointErr != nil || !reflect.DeepEqual(repointed, []uint32{covered.ID, shared.ID}) {
|
|
t.Fatalf("repointed %v, %v", repointed, repointErr)
|
|
}
|
|
if clearErr != nil || !reflect.DeepEqual(references.Cover, []uint32{covered.ID, shared.ID}) || !reflect.DeepEqual(references.Social, []uint32{shared.ID}) || afterClear.CoverImageID != nil || afterClear.SocialImageID != nil {
|
|
t.Errorf("references %+v, %v, after %+v", references, clearErr, afterClear)
|
|
}
|
|
if restoreErr != nil || afterRestore.CoverImageID == nil || *afterRestore.CoverImageID != kept.ID || afterRestore.SocialImageID == nil || *afterRestore.SocialImageID != kept.ID {
|
|
t.Errorf("restore %v, after %+v", restoreErr, afterRestore)
|
|
}
|
|
}},
|
|
{"the site repository stores and updates the SEO columns", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
sites := persistence.NewSiteRepository(db)
|
|
created, err := sites.Create(site.Site{Name: "Shop", Key: "shop", BaseURL: "https://shop.example.com", DefaultRobots: "index", SiteName: "Renew Shop", TitleTemplate: "%title% - %site%", DefaultDescription: "A shop."})
|
|
created.SiteName = "Renew"
|
|
updated, updateErr := sites.Update(created)
|
|
seeded, _ := sites.GetDefault()
|
|
|
|
if err != nil || updateErr != nil || updated.SiteName != "Renew" || updated.TitleTemplate != "%title% - %site%" || updated.DefaultDescription != "A shop." {
|
|
t.Errorf("updated %+v, %v %v", updated, err, updateErr)
|
|
}
|
|
if seeded.TitleTemplate != "%title% | %site%" {
|
|
t.Errorf("seeded template = %q", seeded.TitleTemplate)
|
|
}
|
|
}},
|
|
{"the author repository finds the authors using an avatar", func(t *testing.T, db *gorm.DB, articles *persistence.ArticleRepository, siteID uint32) {
|
|
authors := persistence.NewAuthorRepository(db)
|
|
image := mustCreateImage(t, newContractRepositories(db), "avatar.png", "hash-avatar")
|
|
withAvatar, _ := authors.Create(authorWithAvatar("Camille", "camille", &image.ID))
|
|
authors.Create(authorWithAvatar("Other", "other", nil))
|
|
|
|
found, err := authors.FindByAvatar(image.ID)
|
|
|
|
if err != nil || len(found) != 1 || found[0].ID != withAvatar.ID {
|
|
t.Errorf("found %+v, %v", found, err)
|
|
}
|
|
}},
|
|
}
|
|
for _, backend := range backends {
|
|
t.Run(backend.name, func(t *testing.T) {
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
db := backend.open(t)
|
|
defaultSite, err := persistence.NewSiteRepository(db).GetDefault()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.run(t, db, persistence.NewArticleRepository(db), defaultSite.ID)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func authorWithAvatar(name string, slug string, avatarID *uint32) author.Author {
|
|
return author.Author{Name: name, Slug: slug, AvatarID: avatarID, SocialLinks: []author.SocialLink{}}
|
|
}
|