146 lines
5.2 KiB
Go
146 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|