feat: site languages, setup and site URL settings
This commit is contained in:
1 parent
0806733d5c
commit
d9158025d4
106 files changed
+10184
-56
No files matched your search
@@ -0,0 +1,328 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api"
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/language"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/transaction"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
journalSiteID = 1
|
||||
notesSiteID = 2
|
||||
archiveSiteID = 3
|
||||
gallerySiteID = 4
|
||||
readerUserID = 7
|
||||
)
|
||||
|
||||
var errInjectedConfirm = errors.New("injected confirm failure")
|
||||
|
||||
type fakeLocalizationStore struct {
|
||||
articles *mockArticleRepo
|
||||
rows map[uint32][]localization.Row
|
||||
trashed map[uint32]map[string]int
|
||||
failConfirmAt int
|
||||
confirmCalls int
|
||||
replaceCalls int
|
||||
nextArticleID uint32
|
||||
confirmedOrder []uint32
|
||||
}
|
||||
|
||||
var _ localization.Repository = &fakeLocalizationStore{}
|
||||
|
||||
func newFakeLocalizationStore(articles *mockArticleRepo) *fakeLocalizationStore {
|
||||
return &fakeLocalizationStore{articles: articles, rows: map[uint32][]localization.Row{}, trashed: map[uint32]map[string]int{}, nextArticleID: 100}
|
||||
}
|
||||
|
||||
func (f *fakeLocalizationStore) Rows(siteID uint32) ([]localization.Row, error) {
|
||||
return slices.Clone(f.rows[siteID]), nil
|
||||
}
|
||||
|
||||
func (f *fakeLocalizationStore) ConfirmedSites() (map[uint32]bool, error) {
|
||||
confirmed := map[uint32]bool{}
|
||||
for siteID, rows := range f.rows {
|
||||
if !localization.NeedsSetup(rows) {
|
||||
confirmed[siteID] = true
|
||||
}
|
||||
}
|
||||
return confirmed, nil
|
||||
}
|
||||
|
||||
func (f *fakeLocalizationStore) VersionCounts(siteID uint32) (map[string]localization.VersionCount, error) {
|
||||
counts := map[string]localization.VersionCount{}
|
||||
for _, stored := range f.articles.articles {
|
||||
if stored.SiteID != siteID {
|
||||
continue
|
||||
}
|
||||
count := counts[stored.Lang]
|
||||
count.Total++
|
||||
if stored.IsOnline {
|
||||
count.Published++
|
||||
}
|
||||
counts[stored.Lang] = count
|
||||
}
|
||||
for lang, trashed := range f.trashed[siteID] {
|
||||
count := counts[lang]
|
||||
count.Trashed = trashed
|
||||
counts[lang] = count
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (f *fakeLocalizationStore) Replace(siteID uint32, rows []localization.Row) error {
|
||||
f.replaceCalls++
|
||||
f.rows[siteID] = slices.Clone(rows)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLocalizationStore) ConfirmSetup(siteID uint32, tag string) error {
|
||||
f.confirmCalls++
|
||||
if f.failConfirmAt == f.confirmCalls {
|
||||
return errInjectedConfirm
|
||||
}
|
||||
f.confirmedOrder = append(f.confirmedOrder, siteID)
|
||||
f.rows[siteID] = []localization.Row{{Tag: tag, IsDefault: true, Enabled: true, Confirmed: true}}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLocalizationStore) snapshot() (map[uint32][]localization.Row, map[uint32]article.Article) {
|
||||
rows := make(map[uint32][]localization.Row, len(f.rows))
|
||||
for siteID, stored := range f.rows {
|
||||
rows[siteID] = slices.Clone(stored)
|
||||
}
|
||||
return rows, maps.Clone(f.articles.articles)
|
||||
}
|
||||
|
||||
type rollbackLocalizationTransactor struct {
|
||||
store *fakeLocalizationStore
|
||||
sites site.Repository
|
||||
runs int
|
||||
}
|
||||
|
||||
var _ transaction.Transactor = &rollbackLocalizationTransactor{}
|
||||
|
||||
func (r *rollbackLocalizationTransactor) Run(ctx context.Context, work func(transaction.Repositories) error) error {
|
||||
r.runs++
|
||||
rows, articles := r.store.snapshot()
|
||||
err := work(transaction.Repositories{Articles: r.store.articles, Sites: r.sites, Localization: r.store})
|
||||
if err != nil {
|
||||
r.store.rows = rows
|
||||
r.store.articles.articles = articles
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type patternSiteRepo struct {
|
||||
*mockSiteRepo
|
||||
}
|
||||
|
||||
func (p patternSiteRepo) Update(s site.Site) (site.Site, error) {
|
||||
saved, err := p.mockSiteRepo.Update(s)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
stored := p.mockSiteRepo.sites[s.ID]
|
||||
stored.PublicURLPattern = s.PublicURLPattern
|
||||
p.mockSiteRepo.sites[s.ID] = stored
|
||||
return p.mockSiteRepo.withCount(stored), nil
|
||||
}
|
||||
|
||||
type localizationEnv struct {
|
||||
*siteEnv
|
||||
store *fakeLocalizationStore
|
||||
transactor *rollbackLocalizationTransactor
|
||||
nextID uint32
|
||||
}
|
||||
|
||||
func newLocalizationEnv(t *testing.T) *localizationEnv {
|
||||
t.Helper()
|
||||
e := newSiteEnv(t)
|
||||
store := newFakeLocalizationStore(e.articles)
|
||||
sites := patternSiteRepo{e.sites}
|
||||
transactor := &rollbackLocalizationTransactor{store: store, sites: sites}
|
||||
c := e.container
|
||||
c.ListSitesUseCase = useCases.NewListSitesUseCase(sites)
|
||||
c.UpdateSiteUseCase = useCases.NewUpdateSiteUseCase(sites, e.images, e.audit.recorder)
|
||||
c.GetSiteLanguagesUseCase = useCases.NewGetSiteLanguagesUseCase(sites, store)
|
||||
c.UpdateSiteLanguagesUseCase = useCases.NewUpdateSiteLanguagesUseCase(transactor, e.audit.recorder)
|
||||
c.GetLanguageSetupUseCase = useCases.NewGetLanguageSetupUseCase(sites, store)
|
||||
c.ApplyLanguageSetupUseCase = useCases.NewApplyLanguageSetupUseCase(transactor, e.audit.recorder)
|
||||
router := chi.NewRouter()
|
||||
router.Mount("/v1", api.InitBackendRoutes(c, e.settings))
|
||||
e.router = router
|
||||
e.addUser(readerUserID, writerRoleID, true)
|
||||
return &localizationEnv{siteEnv: e, store: store, transactor: transactor, nextID: 1}
|
||||
}
|
||||
|
||||
func (e *localizationEnv) addArticles(siteID uint32, lang string, total int, published int) {
|
||||
for index := 0; index < total; index++ {
|
||||
e.store.nextArticleID++
|
||||
e.articles.articles[e.store.nextArticleID] = article.Article{ID: e.store.nextArticleID, Title: "Article " + lang, SiteID: siteID, Lang: lang, IsOnline: index < published}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *localizationEnv) seedRows(siteID uint32, rows ...localization.Row) {
|
||||
e.store.rows[siteID] = rows
|
||||
}
|
||||
|
||||
func (e *localizationEnv) seedJournal() {
|
||||
e.sites.seed(journalSiteID, "Journal", "journal", true)
|
||||
english := "en"
|
||||
site := e.sites.sites[journalSiteID]
|
||||
site.Locale = &english
|
||||
e.sites.sites[journalSiteID] = site
|
||||
frenchSocial := "fr_FR"
|
||||
e.seedRows(journalSiteID,
|
||||
localization.Row{Tag: "en", IsDefault: true, Enabled: true, Confirmed: true},
|
||||
localization.Row{Tag: "fr", Enabled: true, Confirmed: true, SocialLocale: &frenchSocial},
|
||||
localization.Row{Tag: "de", Enabled: true},
|
||||
localization.Row{Tag: "ar", Enabled: true},
|
||||
localization.Row{Tag: "es"},
|
||||
localization.Row{Tag: "en-US"},
|
||||
)
|
||||
e.addArticles(journalSiteID, "en", 7, 6)
|
||||
e.addArticles(journalSiteID, "fr", 4, 2)
|
||||
e.addArticles(journalSiteID, "de", 4, 3)
|
||||
e.addArticles(journalSiteID, "ar", 1, 0)
|
||||
e.addArticles(journalSiteID, "es", 1, 0)
|
||||
}
|
||||
|
||||
func (e *localizationEnv) seedPendingSites() {
|
||||
e.seedJournalPending()
|
||||
e.seedSite(notesSiteID, "Notes", "notes", nil, nil, 3)
|
||||
pt := "pt-BR"
|
||||
e.seedSite(archiveSiteID, "Archive", "archive", &pt, &localization.Row{Tag: "pt-BR", IsDefault: true, Enabled: true}, 5)
|
||||
gb := "en-GB"
|
||||
e.seedSite(gallerySiteID, "Gallery", "gallery", &gb, &localization.Row{Tag: "en-GB", IsDefault: true, Enabled: true}, 0)
|
||||
}
|
||||
|
||||
func (e *localizationEnv) seedJournalPending() {
|
||||
en := "en"
|
||||
e.seedSite(journalSiteID, "Journal", "journal", &en, &localization.Row{Tag: "en", IsDefault: true, Enabled: true}, 8)
|
||||
}
|
||||
|
||||
func (e *localizationEnv) seedSite(id uint32, name string, key string, locale *string, row *localization.Row, articles int) {
|
||||
e.sites.seed(id, name, key, id == journalSiteID)
|
||||
stored := e.sites.sites[id]
|
||||
stored.Locale = locale
|
||||
e.sites.sites[id] = stored
|
||||
if row != nil {
|
||||
e.seedRows(id, *row)
|
||||
}
|
||||
e.addArticles(id, language.Unassigned, articles, 0)
|
||||
}
|
||||
|
||||
func (e *localizationEnv) read(t *testing.T, key string) (int, map[string]json.RawMessage) {
|
||||
t.Helper()
|
||||
rec := e.owner(t, http.MethodGet, "/v1/site/"+key+"/languages", "")
|
||||
if rec.Code != http.StatusOK {
|
||||
return rec.Code, nil
|
||||
}
|
||||
return rec.Code, decodeFields(t, rec)
|
||||
}
|
||||
|
||||
func decodeFields(t *testing.T, rec *httptest.ResponseRecorder) map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &fields); err != nil {
|
||||
t.Fatalf("decode %q: %v", rec.Body.String(), err)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func (e *localizationEnv) revision(t *testing.T, key string) string {
|
||||
t.Helper()
|
||||
_, fields := e.read(t, key)
|
||||
var revision string
|
||||
if err := json.Unmarshal(fields["revision"], &revision); err != nil {
|
||||
t.Fatalf("revision: %v", err)
|
||||
}
|
||||
return revision
|
||||
}
|
||||
|
||||
type languagesView struct {
|
||||
Revision string `json:"revision"`
|
||||
Default *string `json:"default"`
|
||||
NeedsSetup bool `json:"needs_setup"`
|
||||
Languages []struct {
|
||||
Lang string `json:"lang"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
VersionCount int `json:"version_count"`
|
||||
PublishedCount int `json:"published_count"`
|
||||
SocialLocale *string `json:"social_locale"`
|
||||
State string `json:"social_locale_state"`
|
||||
} `json:"languages"`
|
||||
Available []struct {
|
||||
Lang string `json:"lang"`
|
||||
Substitute *string `json:"substitute"`
|
||||
} `json:"available"`
|
||||
}
|
||||
|
||||
func decodeLanguagesView(t *testing.T, rec *httptest.ResponseRecorder) languagesView {
|
||||
t.Helper()
|
||||
var view languagesView
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
|
||||
t.Fatalf("decode %q: %v", rec.Body.String(), err)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func (v languagesView) tags() []string {
|
||||
tags := []string{}
|
||||
for _, entry := range v.Languages {
|
||||
tags = append(tags, entry.Lang)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func keysOf(fields map[string]json.RawMessage) []string {
|
||||
keys := slices.Sorted(maps.Keys(fields))
|
||||
return keys
|
||||
}
|
||||
|
||||
func languagesBody(revision string, defaultTag string, entries ...string) string {
|
||||
return `{"revision":"` + revision + `","default":"` + defaultTag + `","languages":[` + strings.Join(entries, ",") + `]}`
|
||||
}
|
||||
|
||||
func entryJSON(lang string, enabled bool, social string) string {
|
||||
enabledText := "false"
|
||||
if enabled {
|
||||
enabledText = "true"
|
||||
}
|
||||
return `{"lang":"` + lang + `","enabled":` + enabledText + `,"social_locale":` + social + `}`
|
||||
}
|
||||
|
||||
func journalEntries(overrides map[string]string, omit ...string) []string {
|
||||
base := []struct {
|
||||
lang string
|
||||
enabled bool
|
||||
social string
|
||||
}{{"en", true, "null"}, {"fr", true, `"fr_FR"`}, {"de", true, "null"}, {"ar", true, "null"}, {"es", false, "null"}, {"en-US", false, "null"}}
|
||||
entries := []string{}
|
||||
for _, row := range base {
|
||||
if slices.Contains(omit, row.lang) {
|
||||
continue
|
||||
}
|
||||
if replacement, ok := overrides[row.lang]; ok {
|
||||
entries = append(entries, replacement)
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entryJSON(row.lang, row.enabled, row.social))
|
||||
}
|
||||
return entries
|
||||
}
|
||||
Reference in new issue
Block a user