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,317 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const everyPendingSite = `{"sites":[{"key":"journal","lang":"en"},{"key":"notes","lang":"en"},{"key":"archive","lang":"pt-BR"},{"key":"gallery","lang":"en-GB"}]}`
|
||||
|
||||
type setupReadBody struct {
|
||||
Sites []struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Locale *string `json:"locale"`
|
||||
ArticleCount int `json:"article_count"`
|
||||
Suggestion *struct {
|
||||
Lang string `json:"lang"`
|
||||
Substituted bool `json:"substituted"`
|
||||
} `json:"suggestion"`
|
||||
} `json:"sites"`
|
||||
Available []struct {
|
||||
Lang string `json:"lang"`
|
||||
} `json:"available"`
|
||||
}
|
||||
|
||||
type setupResultBody struct {
|
||||
Assigned int `json:"assigned"`
|
||||
Sites []struct {
|
||||
Key string `json:"key"`
|
||||
Lang string `json:"lang"`
|
||||
Assigned int `json:"assigned"`
|
||||
} `json:"sites"`
|
||||
}
|
||||
|
||||
func decodeSetupResult(t *testing.T, rec *httptest.ResponseRecorder) setupResultBody {
|
||||
t.Helper()
|
||||
var body setupResultBody
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode %q: %v", rec.Body, err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func (e *localizationEnv) postSetup(t *testing.T, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
return e.owner(t, http.MethodPost, "/v1/language/setup", body)
|
||||
}
|
||||
|
||||
func (e *localizationEnv) unassigned() int {
|
||||
total := 0
|
||||
for _, stored := range e.articles.articles {
|
||||
if stored.Lang == "" {
|
||||
total++
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func TestLanguageSetup(t *testing.T) {
|
||||
t.Run("Read what is pending", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
e.sites.seed(9, "Done", "done", false)
|
||||
e.seedRows(9, localization.Row{Tag: "en", IsDefault: true, Enabled: true, Confirmed: true})
|
||||
|
||||
rec := e.owner(t, http.MethodGet, "/v1/language/setup", "")
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
var body setupReadBody
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if keys := keysOf(decodeFields(t, rec)); !reflect.DeepEqual(keys, []string{"available", "sites"}) {
|
||||
t.Errorf("keys = %v", keys)
|
||||
}
|
||||
keys := []string{}
|
||||
counts := map[string]int{}
|
||||
suggestions := map[string]string{}
|
||||
for _, listed := range body.Sites {
|
||||
keys = append(keys, listed.Key)
|
||||
counts[listed.Key] = listed.ArticleCount
|
||||
if listed.Suggestion != nil {
|
||||
suggestions[listed.Key] = listed.Suggestion.Lang
|
||||
if listed.Suggestion.Substituted {
|
||||
t.Errorf("%s suggestion is substituted", listed.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(keys, []string{"journal", "notes", "archive", "gallery"}) {
|
||||
t.Errorf("sites = %v", keys)
|
||||
}
|
||||
if counts["journal"] != 8 || counts["notes"] != 3 || counts["archive"] != 5 || counts["gallery"] != 0 {
|
||||
t.Errorf("counts = %v", counts)
|
||||
}
|
||||
if _, ok := suggestions["notes"]; ok || suggestions["archive"] != "pt-BR" {
|
||||
t.Errorf("suggestions = %v", suggestions)
|
||||
}
|
||||
if len(body.Available) == 0 {
|
||||
t.Errorf("available is empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A substituted suggestion is flagged", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.sites.seed(2, "Belgium", "belgium", false)
|
||||
be := "fr-BE"
|
||||
stored := e.sites.sites[2]
|
||||
stored.Locale = &be
|
||||
e.sites.sites[2] = stored
|
||||
|
||||
var body setupReadBody
|
||||
_ = json.Unmarshal(e.owner(t, http.MethodGet, "/v1/language/setup", "").Body.Bytes(), &body)
|
||||
|
||||
if len(body.Sites) != 1 || body.Sites[0].Suggestion == nil || body.Sites[0].Suggestion.Lang != "fr" || !body.Sites[0].Suggestion.Substituted {
|
||||
t.Errorf("body = %+v", body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Read access", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
user uint32
|
||||
status int
|
||||
}{
|
||||
{"an anonymous client", 0, http.StatusUnauthorized},
|
||||
{"a reader", readerUserID, http.StatusForbidden},
|
||||
{"an owner", apiKeyOwnerId, http.StatusOK},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
if rec := e.as(t, c.user, http.MethodGet, "/v1/language/setup", ""); rec.Code != c.status {
|
||||
t.Errorf("status %d", rec.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Apply for every pending site", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
|
||||
rec := e.postSetup(t, everyPendingSite)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
result := decodeSetupResult(t, rec)
|
||||
if result.Assigned != 16 || len(result.Sites) != 4 {
|
||||
t.Errorf("result = %+v", result)
|
||||
}
|
||||
for _, id := range []uint32{journalSiteID, notesSiteID, archiveSiteID, gallerySiteID} {
|
||||
if localization.NeedsSetup(e.store.rows[id]) || len(e.store.rows[id]) != 1 || !e.store.rows[id][0].Enabled {
|
||||
t.Errorf("site %d rows = %+v", id, e.store.rows[id])
|
||||
}
|
||||
}
|
||||
if e.unassigned() != 0 || len(e.articles.articles) != 16 {
|
||||
t.Errorf("unassigned %d of %d", e.unassigned(), len(e.articles.articles))
|
||||
}
|
||||
_, fields := e.read(t, "journal")
|
||||
if string(fields["needs_setup"]) != "false" {
|
||||
t.Errorf("needs_setup = %s", fields["needs_setup"])
|
||||
}
|
||||
if e.transactor.runs != 1 {
|
||||
t.Errorf("transactions = %d", e.transactor.runs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The owner may change a suggestion", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
|
||||
rec := e.postSetup(t, `{"sites":[{"key":"journal","lang":"en"},{"key":"notes","lang":"en"},{"key":"archive","lang":"pt"},{"key":"gallery","lang":"en-GB"}]}`)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
rows := e.store.rows[archiveSiteID]
|
||||
if len(rows) != 1 || rows[0].Tag != "pt" || !rows[0].IsDefault || !rows[0].Confirmed {
|
||||
t.Errorf("rows = %+v", rows)
|
||||
}
|
||||
for _, stored := range e.articles.articles {
|
||||
if stored.SiteID == archiveSiteID && stored.Lang != "pt" {
|
||||
t.Errorf("article lang = %q", stored.Lang)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A site with no articles still needs confirming", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
|
||||
rec := e.postSetup(t, `{"sites":[{"key":"journal","lang":"en"},{"key":"notes","lang":"en"},{"key":"archive","lang":"pt-BR"}]}`)
|
||||
|
||||
expectSiteStatusAndBody(t, rec, http.StatusUnprocessableEntity, "setup_incomplete")
|
||||
if e.unassigned() != 16 || !localization.NeedsSetup(e.store.rows[journalSiteID]) {
|
||||
t.Errorf("something was applied")
|
||||
}
|
||||
})
|
||||
|
||||
refused := []struct {
|
||||
name string
|
||||
body string
|
||||
status int
|
||||
code string
|
||||
}{
|
||||
{"notes with lang english", `{"sites":[{"key":"notes","lang":"english"}]}`, 422, "invalid_language"},
|
||||
{"notes with lang xx", `{"sites":[{"key":"notes","lang":"xx"}]}`, 422, "unsupported_language"},
|
||||
{"notes with an empty lang", `{"sites":[{"key":"notes","lang":""}]}`, 422, "invalid_language"},
|
||||
{"the same site key twice", `{"sites":[{"key":"notes","lang":"en"},{"key":"notes","lang":"fr"}]}`, 422, "duplicate_site"},
|
||||
{"a site key nowhere", `{"sites":[{"key":"nowhere","lang":"en"}]}`, 404, "site_not_found"},
|
||||
{"no sites key", `{}`, 400, "The request cannot be processed due to a mismatch in the format of the body."},
|
||||
{"malformed JSON", `{"sites":`, 400, "The request cannot be processed due to a mismatch in the format of the body."},
|
||||
}
|
||||
for _, c := range refused {
|
||||
t.Run("Rows that are refused/"+c.name, func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
|
||||
rec := e.postSetup(t, c.body)
|
||||
|
||||
expectSiteStatusAndBody(t, rec, c.status, c.code)
|
||||
if e.unassigned() != 16 || len(e.store.rows) != 3 {
|
||||
t.Errorf("something changed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Retrying is safe", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
e.postSetup(t, everyPendingSite)
|
||||
rowsBefore, articlesBefore := e.store.snapshot()
|
||||
|
||||
again := e.postSetup(t, `{"sites":[{"key":"journal","lang":"fr"},{"key":"notes","lang":"en"},{"key":"archive","lang":"pt-BR"},{"key":"gallery","lang":"en-GB"}]}`)
|
||||
|
||||
if again.Code != http.StatusOK || decodeSetupResult(t, again).Assigned != 0 {
|
||||
t.Fatalf("again: %d %s", again.Code, again.Body)
|
||||
}
|
||||
if rowsAfter, articlesAfter := e.store.snapshot(); !reflect.DeepEqual(rowsBefore, rowsAfter) || !reflect.DeepEqual(articlesBefore, articlesAfter) {
|
||||
t.Errorf("a retry changed something")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Failure leaves nothing half done", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
e.store.failConfirmAt = 2
|
||||
|
||||
rec := e.postSetup(t, everyPendingSite)
|
||||
|
||||
if rec.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
if e.unassigned() != 16 || !localization.NeedsSetup(e.store.rows[journalSiteID]) {
|
||||
t.Errorf("a site was half done")
|
||||
}
|
||||
if later := e.postSetup(t, everyPendingSite); later.Code != http.StatusOK || decodeSetupResult(t, later).Assigned != 16 {
|
||||
t.Errorf("later: %d %s", later.Code, later.Body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Another setup is running", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
e.sites.busy = true
|
||||
|
||||
rec := e.postSetup(t, everyPendingSite)
|
||||
|
||||
expectSiteStatusAndBody(t, rec, http.StatusConflict, "setup_in_progress")
|
||||
if e.unassigned() != 16 {
|
||||
t.Errorf("something was applied")
|
||||
}
|
||||
e.sites.busy = false
|
||||
if later := e.postSetup(t, everyPendingSite); later.Code != http.StatusOK {
|
||||
t.Errorf("later: %d", later.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Locks are taken in a fixed order", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
|
||||
e.postSetup(t, everyPendingSite)
|
||||
|
||||
if !reflect.DeepEqual(e.sites.tryLocked, [][]uint32{{1, 2, 3, 4}}) {
|
||||
t.Errorf("locks = %v", e.sites.tryLocked)
|
||||
}
|
||||
if !reflect.DeepEqual(e.store.confirmedOrder, []uint32{1, 2, 3, 4}) {
|
||||
t.Errorf("confirm order = %v", e.store.confirmedOrder)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sites created after the migration stay pending", func(t *testing.T) {
|
||||
e := newLocalizationEnv(t)
|
||||
e.seedPendingSites()
|
||||
e.sites.seed(8, "Fresh", "fresh", false)
|
||||
|
||||
var body setupReadBody
|
||||
_ = json.Unmarshal(e.owner(t, http.MethodGet, "/v1/language/setup", "").Body.Bytes(), &body)
|
||||
|
||||
listed := false
|
||||
for _, pending := range body.Sites {
|
||||
listed = listed || pending.Key == "fresh"
|
||||
}
|
||||
if !listed {
|
||||
t.Errorf("fresh site not listed: %+v", body.Sites)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user