339 lines
8.9 KiB
Go
339 lines
8.9 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"errors"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"unicode"
|
|
|
|
"golang.org/x/text/runes"
|
|
"golang.org/x/text/transform"
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
type mockArticleRepo struct {
|
|
articles map[uint32]article.Article
|
|
failUpdateBodyFor map[uint32]error
|
|
nextID uint32
|
|
fail failSwitch
|
|
createCalled bool
|
|
credits map[uint32][]uint32
|
|
authors *mockAuthorRepo
|
|
}
|
|
|
|
func newMockArticleRepo() *mockArticleRepo {
|
|
return &mockArticleRepo{
|
|
articles: make(map[uint32]article.Article),
|
|
credits: make(map[uint32][]uint32),
|
|
}
|
|
}
|
|
|
|
func (m *mockArticleRepo) withAuthors(a article.Article) article.Article {
|
|
ids := m.credits[a.ID]
|
|
if len(ids) == 0 || m.authors == nil {
|
|
return a
|
|
}
|
|
bylines := make([]author.Byline, 0, len(ids))
|
|
for _, id := range ids {
|
|
if found, ok := m.authors.authors[id]; ok {
|
|
bylines = append(bylines, found.Byline())
|
|
}
|
|
}
|
|
a.Authors = bylines
|
|
return a
|
|
}
|
|
|
|
func (m *mockArticleRepo) credit(articleID uint32, authorIDs ...uint32) {
|
|
m.credits[articleID] = append([]uint32{}, authorIDs...)
|
|
}
|
|
|
|
func (m *mockArticleRepo) sortedIds() []uint32 {
|
|
ids := make([]uint32, 0, len(m.articles))
|
|
for id := range m.articles {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
|
return ids
|
|
}
|
|
|
|
func (m *mockArticleRepo) FindByFilters(filters article.Filters) ([]article.Article, error) {
|
|
if err := m.fail.trigger("FindByFilters"); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]article.Article, 0)
|
|
for _, id := range m.sortedIds() {
|
|
a := m.articles[id]
|
|
if filters.IsOnline != nil && a.IsOnline != *filters.IsOnline {
|
|
continue
|
|
}
|
|
result = append(result, a)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) FindPage(filters article.Filters) (article.PageResult, error) {
|
|
if err := m.fail.trigger("FindPage"); err != nil {
|
|
return article.PageResult{}, err
|
|
}
|
|
matching := make([]article.Article, 0)
|
|
for _, a := range m.articles {
|
|
if filters.IsOnline != nil && a.IsOnline != *filters.IsOnline {
|
|
continue
|
|
}
|
|
if filters.SiteID != nil && a.SiteID != *filters.SiteID {
|
|
continue
|
|
}
|
|
if filters.Status == article.StatusOnline && !a.IsOnline {
|
|
continue
|
|
}
|
|
if filters.Status == article.StatusDraft && a.IsOnline {
|
|
continue
|
|
}
|
|
if filters.Q != "" && !strings.Contains(foldArticleSearch(a.Title), foldArticleSearch(filters.Q)) {
|
|
continue
|
|
}
|
|
matching = append(matching, a)
|
|
}
|
|
sort.Slice(matching, func(i, j int) bool { return articleSortsBefore(matching[i], matching[j], filters.Sort) })
|
|
start := min((filters.Page-1)*filters.Size, len(matching))
|
|
end := min(start+filters.Size, len(matching))
|
|
items := make([]article.Article, 0)
|
|
for _, a := range matching[start:end] {
|
|
items = append(items, m.withAuthors(a))
|
|
}
|
|
return article.PageResult{Items: items, Total: len(matching)}, nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) CountByVisibility(onlineOnly bool) (article.Counts, error) {
|
|
if err := m.fail.trigger("CountByVisibility"); err != nil {
|
|
return article.Counts{}, err
|
|
}
|
|
counts := article.Counts{}
|
|
for _, a := range m.articles {
|
|
if a.IsOnline {
|
|
counts.Online++
|
|
} else if !onlineOnly {
|
|
counts.Draft++
|
|
}
|
|
}
|
|
counts.All = counts.Online + counts.Draft
|
|
return counts, nil
|
|
}
|
|
|
|
func articleSortsBefore(left, right article.Article, order article.Sort) bool {
|
|
if order.Field == "" {
|
|
order = article.DefaultSort()
|
|
}
|
|
var comparison int
|
|
switch order.Field {
|
|
case article.SortByTitle:
|
|
comparison = strings.Compare(strings.ToLower(left.Title), strings.ToLower(right.Title))
|
|
case article.SortByStatus:
|
|
if left.IsOnline != right.IsOnline {
|
|
comparison = 1
|
|
if !left.IsOnline {
|
|
comparison = -1
|
|
}
|
|
}
|
|
case article.SortByCreatedAt:
|
|
comparison = left.CreatedAt.Compare(right.CreatedAt)
|
|
default:
|
|
comparison = left.UpdatedAt.Compare(right.UpdatedAt)
|
|
}
|
|
if order.Direction == article.SortDesc {
|
|
comparison = -comparison
|
|
}
|
|
if comparison != 0 {
|
|
return comparison < 0
|
|
}
|
|
return left.ID > right.ID
|
|
}
|
|
|
|
func foldArticleSearch(value string) string {
|
|
folded, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), value)
|
|
if err != nil {
|
|
return strings.ToLower(value)
|
|
}
|
|
return strings.ToLower(folded)
|
|
}
|
|
|
|
func (m *mockArticleRepo) Get(id uint32) (article.Article, error) {
|
|
if err := m.fail.trigger("Get"); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
a, ok := m.articles[id]
|
|
if !ok {
|
|
return article.Article{}, article.ErrArticleNotFound
|
|
}
|
|
return m.withAuthors(a), nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) GetByName(name string) (article.Article, error) {
|
|
if err := m.fail.trigger("GetByName"); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
for _, id := range m.sortedIds() {
|
|
a := m.articles[id]
|
|
if a.Title == name {
|
|
return a, nil
|
|
}
|
|
}
|
|
return article.Article{}, article.ErrArticleNotFound
|
|
}
|
|
|
|
func (m *mockArticleRepo) GetAll() []article.Article {
|
|
result := make([]article.Article, 0)
|
|
for _, id := range m.sortedIds() {
|
|
result = append(result, m.articles[id])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m *mockArticleRepo) Create(a article.Article) (article.Article, error) {
|
|
m.createCalled = true
|
|
if err := m.fail.trigger("Create"); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
m.nextID++
|
|
a.ID = m.nextID
|
|
m.articles[a.ID] = a
|
|
if len(a.Authors) > 0 {
|
|
ids := make([]uint32, 0, len(a.Authors))
|
|
for _, byline := range a.Authors {
|
|
ids = append(ids, byline.ID)
|
|
}
|
|
m.credits[a.ID] = ids
|
|
}
|
|
return m.withAuthors(a), nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) UpdateBody(id uint32, body string) (article.Article, error) {
|
|
if err := m.fail.trigger("UpdateBody"); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
if err, ok := m.failUpdateBodyFor[id]; ok {
|
|
return article.Article{}, err
|
|
}
|
|
a, ok := m.articles[id]
|
|
if !ok {
|
|
return article.Article{}, article.ErrArticleNotFound
|
|
}
|
|
a.Body = body
|
|
m.articles[id] = a
|
|
return m.withAuthors(a), nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) Update(id uint32, update article.Update) (article.Article, error) {
|
|
if err := m.fail.trigger("Update"); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
a, ok := m.articles[id]
|
|
if !ok {
|
|
return article.Article{}, article.ErrArticleNotFound
|
|
}
|
|
if update.Body != nil {
|
|
if err, failing := m.failUpdateBodyFor[id]; failing {
|
|
return article.Article{}, err
|
|
}
|
|
a.Body = *update.Body
|
|
}
|
|
if update.SiteID != nil {
|
|
a.SiteID = *update.SiteID
|
|
}
|
|
if update.AuthorIDs != nil {
|
|
m.credits[id] = append([]uint32{}, *update.AuthorIDs...)
|
|
}
|
|
m.articles[id] = a
|
|
return m.withAuthors(a), nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) UpdateIsOnline(id uint32, isOnline bool) (article.Article, error) {
|
|
if err := m.fail.trigger("UpdateIsOnline"); err != nil {
|
|
return article.Article{}, err
|
|
}
|
|
a, ok := m.articles[id]
|
|
if !ok {
|
|
return article.Article{}, article.ErrArticleNotFound
|
|
}
|
|
a.IsOnline = isOnline
|
|
m.articles[id] = a
|
|
return m.withAuthors(a), nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) Delete(id uint32) error {
|
|
if err := m.fail.trigger("Delete"); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := m.articles[id]; !ok {
|
|
return article.ErrArticleNotFound
|
|
}
|
|
delete(m.articles, id)
|
|
delete(m.credits, id)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockArticleRepo) FindByBodyContaining(filename string) ([]article.Article, error) {
|
|
if err := m.fail.trigger("FindByBodyContaining"); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]article.Article, 0)
|
|
for _, id := range m.sortedIds() {
|
|
a := m.articles[id]
|
|
if strings.Contains(a.Body, filename) {
|
|
result = append(result, a)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
var _ article.Repository = &mockArticleRepo{}
|
|
|
|
func TestCreateArticle(t *testing.T) {
|
|
repo := newMockArticleRepo()
|
|
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors())
|
|
|
|
t.Run("Valid Title", func(t *testing.T) {
|
|
cmd := useCases.CreateArticleCommand{Title: "Valid Title", Body: "Content"}
|
|
res, err := uc.CreateArticle(testCtx, cmd)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if res.Title != cmd.Title {
|
|
t.Errorf("Expected %s, got %s", cmd.Title, res.Title)
|
|
}
|
|
})
|
|
|
|
t.Run("Short Title", func(t *testing.T) {
|
|
cmd := useCases.CreateArticleCommand{Title: "Ab", Body: "Content"}
|
|
_, err := uc.CreateArticle(testCtx, cmd)
|
|
if !errors.Is(err, article.ErrInvalidTitle) {
|
|
t.Errorf("Expected ErrInvalidTitle")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetOnlineArticle(t *testing.T) {
|
|
repo := newMockArticleRepo()
|
|
repo.articles[1] = article.Article{ID: 1, IsOnline: true}
|
|
repo.articles[2] = article.Article{ID: 2, IsOnline: false}
|
|
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
|
|
|
t.Run("Article Online", func(t *testing.T) {
|
|
_, err := uc.GetOnlineArticle(1, article.BodyFormatHTML)
|
|
if err != nil {
|
|
t.Errorf("Expected article to be found")
|
|
}
|
|
})
|
|
|
|
t.Run("Article Offline", func(t *testing.T) {
|
|
_, err := uc.GetOnlineArticle(2, article.BodyFormatHTML)
|
|
if !errors.Is(err, article.ErrArticleNotFound) {
|
|
t.Errorf("Offline article should return not found")
|
|
}
|
|
})
|
|
}
|