252 lines
8.1 KiB
Go
252 lines
8.1 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/audit"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func (e *localizationEnv) putJournal(t *testing.T, revision string, omit ...string) int {
|
|
t.Helper()
|
|
body := languagesBody(revision, "en", journalEntries(nil, omit...)...)
|
|
return e.owner(t, http.MethodPut, "/v1/site/journal/languages", body).Code
|
|
}
|
|
|
|
func decodeInto(t *testing.T, rec *httptest.ResponseRecorder, target any) bool {
|
|
t.Helper()
|
|
return json.Unmarshal(rec.Body.Bytes(), target) == nil
|
|
}
|
|
|
|
func TestLanguageLockContention(t *testing.T) {
|
|
t.Run("PUT answers 409 languages_changed when the site row cannot be locked", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
revision := e.revision(t, "journal")
|
|
rowsBefore, _ := e.store.snapshot()
|
|
e.sites.busy = true
|
|
|
|
rec := e.owner(t, http.MethodPut, "/v1/site/journal/languages", languagesBody(revision, "en", journalEntries(nil, "en-US")...))
|
|
|
|
expectSiteStatusAndBody(t, rec, http.StatusConflict, "languages_changed")
|
|
if rowsAfter, _ := e.store.snapshot(); !reflect.DeepEqual(rowsBefore, rowsAfter) || e.store.replaceCalls != 0 {
|
|
t.Errorf("something was written")
|
|
}
|
|
})
|
|
|
|
t.Run("A contention failure is recorded as a failed activity", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
revision := e.revision(t, "journal")
|
|
e.sites.busy = true
|
|
|
|
e.putJournal(t, revision, "en-US")
|
|
|
|
assertProducerFailure(t, e.single(t), "The languages changed since they were read.")
|
|
})
|
|
}
|
|
|
|
func TestLanguageSetupLocksUntilStable(t *testing.T) {
|
|
t.Run("A site created between the first read and the lock is locked too and has to be listed", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedPendingSites()
|
|
e.sites.afterTryLock = func() { e.seedSite(8, "Fresh", "fresh", nil, nil, 2) }
|
|
|
|
rec := e.postSetup(t, everyPendingSite)
|
|
|
|
expectSiteStatusAndBody(t, rec, http.StatusUnprocessableEntity, "setup_incomplete")
|
|
if !reflect.DeepEqual(e.sites.tryLocked, [][]uint32{{1, 2, 3, 4}, {8}}) {
|
|
t.Errorf("locks = %v", e.sites.tryLocked)
|
|
}
|
|
if len(e.store.confirmedOrder) != 0 {
|
|
t.Errorf("confirmed = %v", e.store.confirmedOrder)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLanguageSetupSkippedChoices(t *testing.T) {
|
|
t.Run("Choices for sites that already have a language are reported, not dropped", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
e.seedSite(notesSiteID, "Notes", "notes", nil, nil, 3)
|
|
|
|
rec := e.postSetup(t, `{"sites":[{"key":"notes","lang":"fr"},{"key":"journal","lang":"fr"}]}`)
|
|
|
|
var body struct {
|
|
Assigned int `json:"assigned"`
|
|
Skipped []string `json:"skipped"`
|
|
}
|
|
if rec.Code != http.StatusOK || !decodeInto(t, rec, &body) {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
|
}
|
|
if body.Assigned != 3 || !reflect.DeepEqual(body.Skipped, []string{"journal"}) {
|
|
t.Errorf("body = %+v", body)
|
|
}
|
|
if view := e.revision(t, "journal"); view == "" || e.store.rows[journalSiteID][0].Tag != "en" {
|
|
t.Errorf("journal changed")
|
|
}
|
|
})
|
|
|
|
t.Run("A setup with nothing skipped lists an empty skipped array", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedPendingSites()
|
|
|
|
rec := e.postSetup(t, everyPendingSite)
|
|
|
|
if fields := decodeFields(t, rec); string(fields["skipped"]) != "[]" {
|
|
t.Errorf("skipped = %s", fields["skipped"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLanguageInUseCountsSoftDeletedVersions(t *testing.T) {
|
|
t.Run("A language with only soft-deleted versions cannot be removed", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
e.store.trashed[journalSiteID] = map[string]int{"en-US": 1}
|
|
revision := e.revision(t, "journal")
|
|
|
|
rec := e.owner(t, http.MethodPut, "/v1/site/journal/languages", languagesBody(revision, "en", journalEntries(nil, "en-US")...))
|
|
|
|
expectSiteStatusAndBody(t, rec, http.StatusConflict, "language_in_use")
|
|
})
|
|
|
|
t.Run("Soft-deleted versions are not shown in the version count", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
e.store.trashed[journalSiteID] = map[string]int{"en-US": 1}
|
|
|
|
view := decodeLanguagesView(t, e.owner(t, http.MethodGet, "/v1/site/journal/languages", ""))
|
|
|
|
for _, entry := range view.Languages {
|
|
if entry.Lang == "en-US" && entry.VersionCount != 0 {
|
|
t.Errorf("version_count = %d", entry.VersionCount)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLanguageActivity(t *testing.T) {
|
|
t.Run("A changed language set is recorded once", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
revision := e.revision(t, "journal")
|
|
|
|
if status := e.putJournal(t, revision, "en-US"); status != http.StatusOK {
|
|
t.Fatalf("status %d", status)
|
|
}
|
|
|
|
event := e.single(t)
|
|
assertProducerEvent(t, event, "site.languages_edit", audit.CategorySites, audit.OutcomeSuccess)
|
|
assertProducerTarget(t, event, "site", "1", "Journal")
|
|
assertProducerOwnerOrigin(t, event)
|
|
if len(event.Changes) != 1 || event.Changes[0].Field != audit.FieldLanguages {
|
|
t.Errorf("changes = %s", changesJSON(t, event))
|
|
}
|
|
})
|
|
|
|
t.Run("A default change is recorded as its own field", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
revision := e.revision(t, "journal")
|
|
|
|
body := languagesBody(revision, "fr", journalEntries(nil)...)
|
|
if rec := e.owner(t, http.MethodPut, "/v1/site/journal/languages", body); rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
|
}
|
|
|
|
event := e.single(t)
|
|
if len(event.Changes) < 1 || event.Changes[0].Field != audit.FieldDefaultLanguage || *event.Changes[0].Before != "en" || *event.Changes[0].After != "fr" {
|
|
t.Errorf("changes = %s", changesJSON(t, event))
|
|
}
|
|
})
|
|
|
|
t.Run("A PUT that changes nothing records nothing", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
|
|
if status := e.putJournal(t, e.revision(t, "journal")); status != http.StatusOK {
|
|
t.Fatalf("status %d", status)
|
|
}
|
|
|
|
e.noEvent(t)
|
|
})
|
|
|
|
t.Run("A refused PUT is recorded as failed", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
|
|
e.putJournal(t, "stale")
|
|
|
|
assertProducerFailure(t, e.single(t), "The languages changed since they were read.")
|
|
})
|
|
|
|
t.Run("A read records nothing", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedJournal()
|
|
|
|
e.read(t, "journal")
|
|
|
|
e.noEvent(t)
|
|
})
|
|
|
|
t.Run("A setup is recorded once per confirmed site", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedPendingSites()
|
|
|
|
if rec := e.postSetup(t, everyPendingSite); rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
|
}
|
|
|
|
events := e.events(t)
|
|
if len(events) != 4 {
|
|
t.Fatalf("events = %d", len(events))
|
|
}
|
|
for _, event := range events {
|
|
assertProducerEvent(t, event, "site.language_setup", audit.CategorySites, audit.OutcomeSuccess)
|
|
if len(event.Changes) != 1 || event.Changes[0].Field != audit.FieldDefaultLanguage {
|
|
t.Errorf("changes = %s", changesJSON(t, event))
|
|
}
|
|
}
|
|
if events[0].Params[audit.ParamAssigned] == nil {
|
|
t.Errorf("params = %v", events[0].Params)
|
|
}
|
|
wantLang := map[string]string{"Journal": "en", "Notes": "en", "Archive": "pt-BR", "Gallery": "en-GB"}
|
|
wantAssigned := map[string]int{"Journal": 8, "Notes": 3, "Archive": 5, "Gallery": 0}
|
|
for _, event := range events {
|
|
lang, ok := wantLang[event.Target.Name]
|
|
if !ok {
|
|
t.Fatalf("target = %q", event.Target.Name)
|
|
}
|
|
if after := event.Changes[0].After; after == nil || *after != lang {
|
|
t.Errorf("%s change = %s", event.Target.Name, changesJSON(t, event))
|
|
}
|
|
if got := fmt.Sprint(event.Params[audit.ParamAssigned]); got != fmt.Sprint(wantAssigned[event.Target.Name]) {
|
|
t.Errorf("%s assigned = %s", event.Target.Name, got)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("A refused setup is recorded once as failed", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedPendingSites()
|
|
|
|
e.postSetup(t, `{"sites":[{"key":"journal","lang":"en"}]}`)
|
|
|
|
assertProducerFailure(t, e.single(t), "Every site that needs a language must be listed.")
|
|
})
|
|
|
|
t.Run("A setup refused by contention is recorded as failed", func(t *testing.T) {
|
|
e := newLocalizationEnv(t)
|
|
e.seedPendingSites()
|
|
e.sites.busy = true
|
|
|
|
e.postSetup(t, everyPendingSite)
|
|
|
|
assertProducerFailure(t, e.single(t), "Another change is running on this site. Please try again.")
|
|
})
|
|
}
|