feat: site languages, setup and site URL settings

This commit is contained in:
Florian Sylvain committed 2026-11-01 22:13:02 +01:00
1 parent 0806733d5c
commit d9158025d4
106 files changed
+10184 -56

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
package test
import (
languagehandler "RenewCMS/api/handlers/language"
"RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/site"
entity "RenewCMS/internal/infrastructure/persistence/models"
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
"RenewCMS/internal/infrastructure/useCases"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
func newLanguageRouter(db *gorm.DB) http.Handler {
validator.InitValidator()
sites := persistence.NewSiteRepository(db)
reads := persistence.NewLocalizationRepository(db)
transactor := persistence.NewTransactor(db)
siteLanguages := languagehandler.NewSiteLanguagesHandler(useCases.NewGetSiteLanguagesUseCase(sites, reads), useCases.NewUpdateSiteLanguagesUseCase(transactor, discardRecorder))
setup := languagehandler.NewSetupHandler(useCases.NewGetLanguageSetupUseCase(sites, reads), useCases.NewApplyLanguageSetupUseCase(transactor, discardRecorder))
router := chi.NewRouter()
router.Put("/site/{key}/languages", siteLanguages.Update)
router.Post("/language/setup", setup.Apply)
return router
}
func sendTo(router http.Handler, method string, path string, body string) int {
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(method, path, strings.NewReader(body)))
return rec.Code
}
func runTogether(jobs ...func()) {
var group sync.WaitGroup
start := make(chan struct{})
for _, job := range jobs {
group.Add(1)
go func() {
defer group.Done()
<-start
job()
}()
}
close(start)
group.Wait()
}
func TestLanguageConcurrencyOnSQLite(t *testing.T) {
t.Run("PUT languages racing a site creation answers 200 or 409, never 500, and keeps one default", func(t *testing.T) {
db := openContractSQLite(t)
journal := seedLocalizedSite(t, db, "journal", []entity.SiteLanguage{{Lang: "en", IsDefault: true, Enabled: true, Confirmed: true}}, map[string][]entity.Article{"en": {{Title: "a", IsOnline: true}}})
router := newLanguageRouter(db)
read := useCases.NewGetSiteLanguagesUseCase(persistence.NewSiteRepository(db), persistence.NewLocalizationRepository(db))
sites := persistence.NewSiteRepository(db)
extra := []string{"fr", "de", "es", "ar", "pt-BR", "en-GB"}
for round, tag := range extra {
view, err := read.Get("journal")
if err != nil {
t.Fatal(err)
}
body := languagesBody(view.Revision, "en", entryJSON("en", true, "null"), entryJSON(tag, true, "null"))
status := 0
var createErr error
runTogether(
func() { status = sendTo(router, http.MethodPut, "/site/journal/languages", body) },
func() {
key := fmt.Sprintf("race-%d", round)
_, createErr = sites.Create(site.Site{Name: key, Key: key, BaseURL: "https://" + key + ".example.com", DefaultRobots: "index"})
},
)
if status != http.StatusOK && status != http.StatusConflict {
t.Errorf("round %d: PUT status %d", round, status)
}
if createErr != nil {
t.Logf("round %d: create refused: %v", round, createErr)
}
defaults := 0
for _, row := range storedLanguageRows(t, db, journal.ID) {
if row.IsDefault {
defaults++
}
}
if defaults != 1 {
t.Errorf("round %d: %d defaults", round, defaults)
}
}
})
t.Run("Two simultaneous setups answer 200 or 409, at least one 200, and assign every article exactly once", func(t *testing.T) {
for round := 0; round < 5; round++ {
db := openContractSQLite(t)
first := seedLocalizedSite(t, db, "journal", nil, map[string][]entity.Article{"": {{Title: "a"}, {Title: "deleted"}}})
second := seedLocalizedSite(t, db, "notes", nil, map[string][]entity.Article{"": {{Title: "b"}, {Title: "c"}}})
router := newLanguageRouter(db)
body := `{"sites":[{"key":"default","lang":"en"},{"key":"journal","lang":"en"},{"key":"notes","lang":"fr"}]}`
statuses := make([]int, 2)
runTogether(
func() { statuses[0] = sendTo(router, http.MethodPost, "/language/setup", body) },
func() { statuses[1] = sendTo(router, http.MethodPost, "/language/setup", body) },
)
accepted := 0
for _, status := range statuses {
switch status {
case http.StatusOK:
accepted++
case http.StatusConflict:
default:
t.Errorf("round %d: status %d", round, status)
}
}
if accepted == 0 {
t.Errorf("round %d: no setup succeeded: %v", round, statuses)
}
if accepted == 2 {
t.Logf("round %d: the setups were serialized", round)
}
if sendTo(router, http.MethodPost, "/language/setup", body) != http.StatusOK {
t.Errorf("round %d: a later retry did not succeed", round)
}
var unassigned int64
db.Unscoped().Model(&entity.Article{}).Where("lang = ?", "").Count(&unassigned)
if unassigned != 0 {
t.Errorf("round %d: %d articles unassigned", round, unassigned)
}
for siteID, tag := range map[uint32]string{first.ID: "en", second.ID: "fr"} {
rows := storedLanguageRows(t, db, siteID)
if len(rows) != 1 || rows[0].Lang != tag || !rows[0].IsDefault || !rows[0].Confirmed {
t.Errorf("round %d: rows of site %d = %+v", round, siteID, rows)
}
}
var mislabeled int64
db.Unscoped().Model(&entity.Article{}).Where("site_id = ? AND lang <> ?", second.ID, "fr").Count(&mislabeled)
if mislabeled != 0 {
t.Errorf("round %d: %d articles with the wrong language", round, mislabeled)
}
}
})
}