feat: Authors feature backend (and upgraded deprecated mods)
This commit is contained in:
1 parent
a11338094e
commit
b34a173d83
103 files changed
+6454
-240
No files matched your search
@@ -0,0 +1,168 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestAuthorSlugGeneration(t *testing.T) {
|
||||
cases := []struct{ name, slug string }{
|
||||
{"Camille Roche", "camille-roche"},
|
||||
{"Inès Duarte", "ines-duarte"},
|
||||
{"Zoë Œuvre Łukasz", "zoe-oeuvre-lukasz"},
|
||||
{"Straße Müller", "strasse-muller"},
|
||||
{"Dr. O'Neil Smith", "dr-o-neil-smith"},
|
||||
{" --Marta-- ", "marta"},
|
||||
{"!!!", "author"},
|
||||
{"", "author"},
|
||||
{strings.Repeat("ab ", 60), strings.TrimRight(strings.Repeat("ab-", 60)[:100], "-")},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := author.GenerateSlug(c.name)
|
||||
if got != c.slug || !author.IsValidSlug(got) {
|
||||
t.Errorf("slug = %q, expected %q", got, c.slug)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("The suffix always fits in 100 characters", func(t *testing.T) {
|
||||
base := strings.Repeat("a", 100)
|
||||
for _, attempt := range []int{2, 9, 10, 99, 100} {
|
||||
got := author.SlugWithSuffix(base, attempt)
|
||||
if utf8.RuneCountInString(got) > 100 || !author.IsValidSlug(got) || !strings.HasSuffix(got, "-"+itoaForTest(attempt)) {
|
||||
t.Errorf("attempt %d: %q", attempt, got)
|
||||
}
|
||||
}
|
||||
if author.SlugWithSuffix("marta", 1) != "marta" {
|
||||
t.Errorf("the first attempt must keep the base slug")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func itoaForTest(value int) string {
|
||||
digits := ""
|
||||
for value > 0 {
|
||||
digits = string(rune('0'+value%10)) + digits
|
||||
value /= 10
|
||||
}
|
||||
return digits
|
||||
}
|
||||
|
||||
func TestAuthorValueRules(t *testing.T) {
|
||||
t.Run("Names are counted in runes after trimming", func(t *testing.T) {
|
||||
for name, valid := range map[string]bool{"": false, " ": false, "a": true, strings.Repeat("é", 100): true, strings.Repeat("é", 101): false, " a ": true} {
|
||||
if author.IsValidName(name) != valid {
|
||||
t.Errorf("%q valid = %v", name, !valid)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The bio is counted in runes", func(t *testing.T) {
|
||||
if author.ValidateBio(strings.Repeat("é", 280)) != nil || !errors.Is(author.ValidateBio(strings.Repeat("é", 281)), author.ErrBioTooLong) {
|
||||
t.Errorf("bio rule broken")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("URLs must be https with a host", func(t *testing.T) {
|
||||
for raw, valid := range map[string]bool{"https://a.example": true, "http://a.example": false, "a.example": false, "https://": false, "": false, "https://a.example/" + strings.Repeat("a", 2048): false} {
|
||||
if (author.ValidateURL(raw) == nil) != valid {
|
||||
t.Errorf("%q valid = %v", raw, !valid)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An empty website means none", func(t *testing.T) {
|
||||
empty := ""
|
||||
if got, err := author.NormalizeWebsite(&empty); got != nil || err != nil {
|
||||
t.Errorf("got %v %v", got, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("At most five links", func(t *testing.T) {
|
||||
link := author.SocialLink{Type: "x", URL: "https://x.com/a"}
|
||||
if author.ValidateLinks([]author.SocialLink{link, link, link, link, link}) != nil || !errors.Is(author.ValidateLinks([]author.SocialLink{link, link, link, link, link, link}), author.ErrTooManyLinks) {
|
||||
t.Errorf("link count rule broken")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorUseCases(t *testing.T) {
|
||||
t.Run("The own author falls back to later slugs when the repository reports a race", func(t *testing.T) {
|
||||
users := newMockUserRepo()
|
||||
users.users[1] = userNamed(1, "marta", "Marta Alves")
|
||||
authors := plainAuthors()
|
||||
authors.fail.failNextWith("Create", author.ErrSlugTaken)
|
||||
uc := useCases.NewCreateMyAuthorUseCase(authors, users, discardRecorder)
|
||||
|
||||
created, err := uc.Create(context.Background(), 1)
|
||||
|
||||
if err != nil || created.Slug != "marta-alves-2" {
|
||||
t.Errorf("created %+v %v", created, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Deleting an author without articles does not need a destination", func(t *testing.T) {
|
||||
authors := plainAuthors()
|
||||
authors.seed(2, "B", "b")
|
||||
uc := useCases.NewDeleteAuthorUseCase(authors, discardRecorder)
|
||||
|
||||
err := uc.Delete(context.Background(), 2, useCases.Reassignment{Mode: useCases.ReassignMalformed})
|
||||
|
||||
if err != nil || len(authors.deleteCalls) != 1 || authors.deleteCalls[0].reassignTo != nil {
|
||||
t.Errorf("err %v calls %+v", err, authors.deleteCalls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A caller without author rights cannot touch another author", func(t *testing.T) {
|
||||
users := newMockUserRepo()
|
||||
authors := plainAuthors()
|
||||
authors.seed(2, "B", "b")
|
||||
uc := useCases.NewUpdateAuthorUseCase(authors, users, newMockImageRepo(), discardRecorder)
|
||||
bio := "x"
|
||||
|
||||
_, err := uc.Update(context.Background(), 2, useCases.AuthorCaller{UserID: 9, Role: "user"}, author.Patch{Bio: &bio})
|
||||
|
||||
if !errors.Is(err, author.ErrAuthorRights) {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorDomainBoundaries(t *testing.T) {
|
||||
t.Run("A base slug that exactly fits is kept as is before the suffix", func(t *testing.T) {
|
||||
base := strings.Repeat("a", 97) + "-"
|
||||
|
||||
got := author.SlugWithSuffix(base, 2)
|
||||
|
||||
if got != base+"-2" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A url of exactly the maximum length is accepted and one more is refused", func(t *testing.T) {
|
||||
prefix := "https://example.com/"
|
||||
exact := prefix + strings.Repeat("a", author.MaxURLLength-len(prefix))
|
||||
|
||||
if err := author.ValidateURL(exact); err != nil {
|
||||
t.Errorf("exact length refused: %v", err)
|
||||
}
|
||||
if err := author.ValidateURL(exact + "a"); !errors.Is(err, author.ErrInvalidURL) {
|
||||
t.Errorf("over length accepted: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A byline always carries a links array", func(t *testing.T) {
|
||||
bare := author.Author{ID: 1, Name: "A", Slug: "a"}.Byline()
|
||||
kept := author.Author{ID: 1, SocialLinks: []author.SocialLink{{Type: "x", URL: "https://x.com/a"}}}.Byline()
|
||||
|
||||
if bare.SocialLinks == nil || len(bare.SocialLinks) != 0 || len(kept.SocialLinks) != 1 {
|
||||
t.Errorf("bare %+v kept %+v", bare, kept)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user