Files
RenewCMS/test/site_support_test.go
T

398 lines
11 KiB
Go

package test
import (
"RenewCMS/api"
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/site"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"io"
"log"
"net/http/httptest"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/go-chi/chi/v5"
)
type mockSiteRepo struct {
sites map[uint32]site.Site
articles *mockArticleRepo
nextID uint32
fail failSwitch
createCalls int
deleteCalls int
deleteReassignments []*uint32
setDefaultCalls int
}
func newMockSiteRepo(articles *mockArticleRepo) *mockSiteRepo {
return &mockSiteRepo{sites: map[uint32]site.Site{}, articles: articles}
}
func (m *mockSiteRepo) seed(id uint32, name string, key string, isDefault bool) site.Site {
seeded := site.Site{ID: id, Name: name, Key: key, BaseURL: "https://" + key + ".example.com", DefaultRobots: site.RobotsIndex, IsDefault: isDefault}
m.sites[id] = seeded
if id > m.nextID {
m.nextID = id
}
return seeded
}
func (m *mockSiteRepo) withCount(s site.Site) site.Site {
s.ArticleCount = 0
for _, a := range m.articles.articles {
if a.SiteID == s.ID {
s.ArticleCount++
}
}
return s
}
func (m *mockSiteRepo) snapshot() map[uint32]site.Site {
copied := make(map[uint32]site.Site, len(m.sites))
for id, s := range m.sites {
copied[id] = s
}
return copied
}
func (m *mockSiteRepo) defaults() []uint32 {
ids := []uint32{}
for id, s := range m.sites {
if s.IsDefault {
ids = append(ids, id)
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return ids
}
func (m *mockSiteRepo) List() ([]site.Site, error) {
if err := m.fail.trigger("List"); err != nil {
return nil, err
}
result := make([]site.Site, 0, len(m.sites))
for _, s := range m.sites {
result = append(result, m.withCount(s))
}
sort.Slice(result, func(i, j int) bool {
if result[i].IsDefault != result[j].IsDefault {
return result[i].IsDefault
}
left, right := strings.ToLower(result[i].Name), strings.ToLower(result[j].Name)
if left != right {
return left < right
}
return result[i].ID < result[j].ID
})
return result, nil
}
func (m *mockSiteRepo) Find(query site.ListQuery) (site.Page, error) {
if err := m.fail.trigger("Find"); err != nil {
return site.Page{}, err
}
all, err := m.List()
if err != nil {
return site.Page{}, err
}
needle := foldArticleSearch(strings.TrimSpace(query.Q))
matching := []site.Site{}
for _, s := range all {
if needle == "" || strings.Contains(foldArticleSearch(s.Name+"\n"+s.Key+"\n"+s.BaseURL), needle) {
matching = append(matching, s)
}
}
if query.Size <= 0 {
return site.Page{Items: matching, Total: len(matching)}, nil
}
start := min((max(query.Page, 1)-1)*query.Size, len(matching))
end := min(start+query.Size, len(matching))
return site.Page{Items: matching[start:end], Total: len(matching)}, nil
}
func (m *mockSiteRepo) Get(id uint32) (site.Site, error) {
if err := m.fail.trigger("Get"); err != nil {
return site.Site{}, err
}
found, ok := m.sites[id]
if !ok {
return site.Site{}, site.ErrSiteNotFound
}
return m.withCount(found), nil
}
func (m *mockSiteRepo) GetByKey(key string) (site.Site, error) {
if err := m.fail.trigger("GetByKey"); err != nil {
return site.Site{}, err
}
for _, s := range m.sites {
if s.Key == key {
return m.withCount(s), nil
}
}
return site.Site{}, site.ErrSiteNotFound
}
func (m *mockSiteRepo) GetDefault() (site.Site, error) {
if err := m.fail.trigger("GetDefault"); err != nil {
return site.Site{}, err
}
for _, id := range m.defaults() {
return m.withCount(m.sites[id]), nil
}
return site.Site{}, site.ErrSiteNotFound
}
func (m *mockSiteRepo) Create(s site.Site) (site.Site, error) {
m.createCalls++
if err := m.fail.trigger("Create"); err != nil {
return site.Site{}, err
}
if s.IsDefault {
for id, other := range m.sites {
other.IsDefault = false
m.sites[id] = other
}
}
m.nextID++
s.ID = m.nextID
m.sites[s.ID] = s
return m.withCount(s), nil
}
func (m *mockSiteRepo) Update(s site.Site) (site.Site, error) {
if err := m.fail.trigger("Update"); err != nil {
return site.Site{}, err
}
current, ok := m.sites[s.ID]
if !ok {
return site.Site{}, site.ErrSiteNotFound
}
current.Name, current.BaseURL, current.DefaultRobots = s.Name, s.BaseURL, s.DefaultRobots
current.Locale, current.DefaultSocialImageID = s.Locale, s.DefaultSocialImageID
current.SiteName, current.TitleTemplate, current.DefaultDescription = s.SiteName, s.TitleTemplate, s.DefaultDescription
m.sites[s.ID] = current
return m.withCount(current), nil
}
func (m *mockSiteRepo) SetDefault(id uint32) (site.Site, error) {
m.setDefaultCalls++
if _, ok := m.sites[id]; !ok {
return site.Site{}, site.ErrSiteNotFound
}
before := m.snapshot()
for otherID, other := range m.sites {
other.IsDefault = false
m.sites[otherID] = other
}
if err := m.fail.trigger("SetDefault"); err != nil {
m.sites = before
return site.Site{}, err
}
flagged := m.sites[id]
flagged.IsDefault = true
m.sites[id] = flagged
return m.withCount(flagged), nil
}
func (m *mockSiteRepo) DeleteMovingArticles(id uint32, reassignTo *uint32) error {
m.deleteCalls++
m.deleteReassignments = append(m.deleteReassignments, reassignTo)
if err := m.fail.trigger("DeleteMovingArticles"); err != nil {
return err
}
target, ok := m.sites[id]
if !ok {
return site.ErrSiteNotFound
}
if target.IsDefault {
return site.ErrDefaultSite
}
if m.withCount(target).ArticleCount > 0 && reassignTo == nil {
return site.ErrSiteHasArticles
}
if reassignTo != nil {
if _, exists := m.sites[*reassignTo]; !exists {
return site.ErrSiteNotFound
}
for articleID, a := range m.articles.articles {
if a.SiteID == id {
a.SiteID = *reassignTo
m.articles.articles[articleID] = a
}
}
}
delete(m.sites, id)
return nil
}
func (m *mockSiteRepo) ClearImage(imageID uint32) ([]uint32, error) {
if err := m.fail.trigger("ClearImage"); err != nil {
return nil, err
}
cleared := []uint32{}
for id, s := range m.sites {
if s.DefaultSocialImageID != nil && *s.DefaultSocialImageID == imageID {
s.DefaultSocialImageID = nil
m.sites[id] = s
cleared = append(cleared, id)
}
}
sort.Slice(cleared, func(i, j int) bool { return cleared[i] < cleared[j] })
return cleared, nil
}
func (m *mockSiteRepo) AssignImage(siteIDs []uint32, imageID uint32) error {
for _, id := range siteIDs {
s := m.sites[id]
assigned := imageID
s.DefaultSocialImageID = &assigned
m.sites[id] = s
}
return nil
}
func (m *mockSiteRepo) RepointImage(fromImageIDs []uint32, toImageID uint32) (int, error) {
if err := m.fail.trigger("RepointImage"); err != nil {
return 0, err
}
updated := 0
for id, s := range m.sites {
if s.DefaultSocialImageID == nil {
continue
}
for _, from := range fromImageIDs {
if *s.DefaultSocialImageID == from {
target := toImageID
s.DefaultSocialImageID = &target
m.sites[id] = s
updated++
break
}
}
}
return updated, nil
}
var _ site.Repository = &mockSiteRepo{}
const (
siteMainID = 1
siteShopID = 2
siteBlogID = 3
)
type siteEnv struct {
*producersEnv
sites *mockSiteRepo
}
func newSiteEnv(t *testing.T) *siteEnv {
t.Helper()
p := newProducersEnv(t)
sites := newMockSiteRepo(p.articles)
recorder := p.audit.recorder
renderer := &fakeBodyRenderer{}
mergeMedia := useCases.NewMergeMediaUseCase(p.images, p.articles, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites, plainAuthors())
c := p.container
c.CreateArticleUseCase = useCases.NewCreateArticleUseCase(p.articles, renderer, recorder, sites, plainAuthors())
c.UpdateArticleUseCase = useCases.NewUpdateArticleUseCase(p.articles, renderer, recorder, sites, plainAuthors(), p.images)
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites)
c.ListMediaUseCase = useCases.NewListMediaUseCase(p.images, p.articles, p.users, newMockVariantFileRepo(), sites, plainAuthors())
c.DeleteMediaUseCase = useCases.NewDeleteMediaUseCase(p.images, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites, plainAuthors(), p.articles)
c.MergeMediaUseCase = mergeMedia
c.GetMediaUsageUseCase = useCases.NewGetMediaUsageUseCase(p.images, p.articles, sites, plainAuthors())
c.RunPurgeUseCase = useCases.NewRunPurgeUseCase(p.images, p.articles, mergeMedia, newMockPurgeSettingsRepo(), func() time.Time { return p.now }, log.New(io.Discard, "", 0), recorder, sites, plainAuthors())
c.ListSitesUseCase = useCases.NewListSitesUseCase(sites)
c.CreateSiteUseCase = useCases.NewCreateSiteUseCase(sites, p.images, recorder)
c.UpdateSiteUseCase = useCases.NewUpdateSiteUseCase(sites, p.images, recorder)
c.SetDefaultSiteUseCase = useCases.NewSetDefaultSiteUseCase(sites, recorder)
c.DeleteSiteUseCase = useCases.NewDeleteSiteUseCase(sites, recorder)
router := chi.NewRouter()
router.Mount("/v1", api.InitBackendRoutes(c, p.settings))
p.router = router
return &siteEnv{producersEnv: p, sites: sites}
}
func newSiteEnvWithMain(t *testing.T) *siteEnv {
t.Helper()
e := newSiteEnv(t)
e.sites.seed(siteMainID, "Main", "main", true)
return e
}
func newSiteEnvWithShop(t *testing.T) *siteEnv {
t.Helper()
e := newSiteEnvWithMain(t)
e.sites.seed(siteShopID, "Shop", "shop", false)
return e
}
func (e *siteEnv) owner(t *testing.T, method string, path string, body string) *httptest.ResponseRecorder {
t.Helper()
return e.as(t, apiKeyOwnerId, method, path, body)
}
func (e *siteEnv) anonymous(t *testing.T, method string, path string, body string) *httptest.ResponseRecorder {
t.Helper()
return e.as(t, 0, method, path, body)
}
func (e *siteEnv) seedArticle(id uint32, siteID uint32, online bool) {
e.articles.articles[id] = article.Article{ID: id, Title: "Article " + strconv.Itoa(int(id)), Body: "Body", IsOnline: online, SiteID: siteID}
}
func (e *siteEnv) articlesOn(siteID uint32) int {
total := 0
for _, a := range e.articles.articles {
if a.SiteID == siteID {
total++
}
}
return total
}
func bodyText(rec *httptest.ResponseRecorder) string {
return strings.TrimSpace(rec.Body.String())
}
func decodeSiteBody(t *testing.T, rec *httptest.ResponseRecorder) site.Site {
t.Helper()
var decoded site.Site
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("decode site %q: %v", rec.Body.String(), err)
}
return decoded
}
func decodeSiteList(t *testing.T, rec *httptest.ResponseRecorder) []site.Site {
t.Helper()
var decoded []site.Site
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("decode sites %q: %v", rec.Body.String(), err)
}
return decoded
}
func expectSiteStatusAndBody(t *testing.T, rec *httptest.ResponseRecorder, status int, body string) {
t.Helper()
if rec.Code != status || bodyText(rec) != body {
t.Fatalf("expected %d %q, got %d %q", status, body, rec.Code, bodyText(rec))
}
}
func stringPointer(value string) *string { return &value }
func idPointer(value uint32) *uint32 { return &value }
func plainSites() *mockSiteRepo {
sites := newMockSiteRepo(newMockArticleRepo())
sites.seed(1, "Default", "default", true)
return sites
}