318 lines
9.9 KiB
Go
318 lines
9.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|