169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|