373 lines
9.9 KiB
Go
373 lines
9.9 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) 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
|
|
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)
|
|
c := p.container
|
|
c.CreateArticleUseCase = useCases.NewCreateArticleUseCase(p.articles, renderer, recorder, sites)
|
|
c.UpdateArticleUseCase = useCases.NewUpdateArticleUseCase(p.articles, renderer, recorder, sites)
|
|
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites)
|
|
c.ListMediaUseCase = useCases.NewListMediaUseCase(p.images, p.articles, p.users, newMockVariantFileRepo(), sites)
|
|
c.DeleteMediaUseCase = useCases.NewDeleteMediaUseCase(p.images, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites)
|
|
c.MergeMediaUseCase = mergeMedia
|
|
c.RunPurgeUseCase = useCases.NewRunPurgeUseCase(p.images, p.articles, mergeMedia, newMockPurgeSettingsRepo(), func() time.Time { return p.now }, log.New(io.Discard, "", 0), recorder, sites)
|
|
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
|
|
}
|