refac: missing mutation tests, spa specs, checks shown to pass, touch targets, catalogue entry, production code
This commit is contained in:
1 parent
7c15206170
commit
3e02e14da4
48 files changed
+2173
-83
No files matched your search
@@ -0,0 +1,229 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/transaction"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const sqliteBusyCode = 5
|
||||
|
||||
type sqliteBusyError struct{}
|
||||
|
||||
func (sqliteBusyError) Error() string {
|
||||
return "database is busy"
|
||||
}
|
||||
|
||||
func (sqliteBusyError) Code() int {
|
||||
return sqliteBusyCode
|
||||
}
|
||||
|
||||
func onArticleUpdate(t *testing.T, db *gorm.DB, name string, hook func(*gorm.DB)) {
|
||||
t.Helper()
|
||||
if err := db.Callback().Update().Before("gorm:update").Register(name, hook); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Callback().Update().Remove(name) })
|
||||
}
|
||||
|
||||
func onArticleQuery(t *testing.T, db *gorm.DB, name string, hook func(*gorm.DB)) {
|
||||
t.Helper()
|
||||
if err := db.Callback().Query().After("gorm:query").Register(name, hook); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Callback().Query().Remove(name) })
|
||||
}
|
||||
|
||||
type articleEdgesEnv struct {
|
||||
db *gorm.DB
|
||||
articles *persistence.ArticleRepository
|
||||
siteID uint32
|
||||
}
|
||||
|
||||
func newArticleEdgesEnv(t *testing.T) *articleEdgesEnv {
|
||||
t.Helper()
|
||||
db := openContractSQLite(t)
|
||||
return &articleEdgesEnv{db: db, articles: persistence.NewArticleRepository(db), siteID: storageDefaultSite(t, db).ID}
|
||||
}
|
||||
|
||||
func (e *articleEdgesEnv) create(t *testing.T, lang string, slug string, online bool) domainArticle.Article {
|
||||
t.Helper()
|
||||
created, err := e.articles.Create(newStorageArticle(e.siteID, lang, slug, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if online {
|
||||
if created, err = e.articles.UpdateIsOnline(created.ID, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
func TestArticleRepositoryLanguageScope(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
e.create(t, "fr", "bonjour", true)
|
||||
e.create(t, "en", "hello", true)
|
||||
|
||||
t.Run("An empty language list matches nothing", func(t *testing.T) {
|
||||
page, err := e.articles.FindPage(domainArticle.Filters{Langs: &[]string{}, Unpaged: true})
|
||||
if err != nil || page.Total != 0 || len(page.Items) != 0 {
|
||||
t.Fatalf("page = %+v, err = %v", page, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A language list keeps only its languages", func(t *testing.T) {
|
||||
page, err := e.articles.FindPage(domainArticle.Filters{Langs: &[]string{"fr"}, Unpaged: true})
|
||||
if err != nil || page.Total != 1 || len(page.Items) != 1 || page.Items[0].Lang != "fr" {
|
||||
t.Fatalf("page = %+v, err = %v", page, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArticleRepositoryCountVersions(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
e.create(t, "fr", "en-ligne", true)
|
||||
draft := e.create(t, "fr", "brouillon", false)
|
||||
e.create(t, "en", "other", false)
|
||||
online := true
|
||||
stranger := draft.CreatedBy + 1
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
filters domainArticle.Filters
|
||||
want domainArticle.Counts
|
||||
}{
|
||||
{"every version of the language", domainArticle.Filters{Lang: "fr"}, domainArticle.Counts{All: 2, Online: 1, Draft: 1}},
|
||||
{"online only skips the drafts", domainArticle.Filters{Lang: "fr", IsOnline: &online}, domainArticle.Counts{All: 1, Online: 1}},
|
||||
{"drafts are counted for their reader only", domainArticle.Filters{Lang: "fr", VisibleTo: &stranger}, domainArticle.Counts{All: 1, Online: 1}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
counts, err := e.articles.CountVersions(c.filters)
|
||||
if err != nil || counts != c.want {
|
||||
t.Fatalf("counts = %+v, err = %v, want %+v", counts, err, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleRepositorySlugTaken(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
owner := e.create(t, "fr", "bonjour", false)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
slug string
|
||||
exceptID uint32
|
||||
want bool
|
||||
}{
|
||||
{"a slug in use is taken", "bonjour", 0, true},
|
||||
{"a free slug is not taken", "salut", 0, false},
|
||||
{"the slug of the article itself is not taken", "bonjour", owner.ID, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
taken, err := e.articles.SlugTaken(e.siteID, "fr", c.slug, c.exceptID)
|
||||
if err != nil || taken != c.want {
|
||||
t.Fatalf("taken = %v, err = %v", taken, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleRepositoryUpdateRetriesLockContention(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
failures int
|
||||
wantErr bool
|
||||
attempts int
|
||||
}{
|
||||
{"a write that is busy twice succeeds on the third attempt", 2, false, 3},
|
||||
{"a write that stays busy gives up after five attempts", 1000, true, 5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
created := e.create(t, "fr", "bonjour", false)
|
||||
attempts := 0
|
||||
onArticleUpdate(t, e.db, "test:busy_article_writes", func(tx *gorm.DB) {
|
||||
if tx.Statement.Table != "articles" {
|
||||
return
|
||||
}
|
||||
attempts++
|
||||
if attempts <= c.failures {
|
||||
_ = tx.AddError(sqliteBusyError{})
|
||||
}
|
||||
})
|
||||
|
||||
updated, err := e.articles.Update(created.ID, domainArticle.Update{Excerpt: localeOf("Renamed")})
|
||||
|
||||
if c.wantErr != (err != nil) || attempts != c.attempts {
|
||||
t.Fatalf("err = %v after %d attempts", err, attempts)
|
||||
}
|
||||
if !c.wantErr && updated.Excerpt != "Renamed" {
|
||||
t.Fatalf("updated = %+v", updated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleRepositoryUpdateGivesUpOnASiteThatKeepsMoving(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
created := e.create(t, "fr", "bonjour", false)
|
||||
probes := 0
|
||||
onArticleQuery(t, e.db, "test:moving_article_site", func(tx *gorm.DB) {
|
||||
if tx.Statement.Table != "articles" || len(tx.Statement.Selects) != 2 {
|
||||
return
|
||||
}
|
||||
probes++
|
||||
_ = tx.Session(&gorm.Session{NewDB: true, SkipHooks: true}).Model(&entity.Article{}).Where("id = ?", created.ID).UpdateColumn("site_id", uint32(probes+1000)).Error
|
||||
})
|
||||
|
||||
_, err := e.articles.Update(created.ID, domainArticle.Update{Excerpt: localeOf("Renamed")})
|
||||
|
||||
if !errors.Is(err, transaction.ErrLockContention) || probes != 5 {
|
||||
t.Fatalf("err = %v after %d probes", err, probes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleRepositoryAuthorOnlyChangeIsAnEdit(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
created := e.create(t, "fr", "bonjour", false)
|
||||
writer := entity.Author{Name: "Ada", Slug: "ada", SocialLinks: "[]"}
|
||||
if err := e.db.Create(&writer).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credited := []uint32{writer.ID}
|
||||
|
||||
updated, err := e.articles.Update(created.ID, domainArticle.Update{AuthorIDs: &credited})
|
||||
if err != nil || updated.EditRevision != created.EditRevision+1 || updated.ContentRevision != created.ContentRevision {
|
||||
t.Fatalf("after crediting: %+v, err = %v", updated, err)
|
||||
}
|
||||
|
||||
again, err := e.articles.Update(created.ID, domainArticle.Update{AuthorIDs: &credited})
|
||||
if err != nil || again.EditRevision != updated.EditRevision {
|
||||
t.Fatalf("the same authors bumped the revision: %+v, err = %v", again, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleRepositoryUnrelatedUniqueViolationIsNotALanguageClash(t *testing.T) {
|
||||
e := newArticleEdgesEnv(t)
|
||||
created := e.create(t, "fr", "bonjour", false)
|
||||
writer := entity.Author{Name: "Ada", Slug: "ada", SocialLinks: "[]"}
|
||||
if err := e.db.Create(&writer).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
twice := []uint32{writer.ID, writer.ID}
|
||||
|
||||
_, err := e.articles.Update(created.ID, domainArticle.Update{AuthorIDs: &twice})
|
||||
|
||||
if err == nil || errors.Is(err, domainArticle.ErrLanguageTaken) || errors.Is(err, domainArticle.ErrSlugTaken) {
|
||||
t.Fatalf("err = %v, the duplicate credit must surface as itself", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user