216 lines
9.3 KiB
Go
216 lines
9.3 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/apperr"
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/audit"
|
|
"net/http"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
createSiteNotes uint32 = 3
|
|
createSiteArchive uint32 = 4
|
|
createSiteFresh uint32 = 5
|
|
createSitePath = "/v1/article/"
|
|
)
|
|
|
|
func newCreateSiteWorld(t *testing.T) *storyWorld {
|
|
t.Helper()
|
|
w := newTranslationWorld(t)
|
|
w.sites.seed(createSiteNotes, "Notes", "notes", false)
|
|
w.sites.seed(createSiteArchive, "Archive", "archive", false)
|
|
w.sites.seed(createSiteFresh, "Fresh", "fresh", false)
|
|
w.rows.rows[storyAtelier] = storyRows("fr", []string{"en", "fr", "de"}, nil)
|
|
w.rows.rows[createSiteNotes] = storyRows("en", []string{"en"}, nil)
|
|
w.rows.rows[createSiteArchive] = storyRows("en", []string{"en"}, nil)
|
|
return w
|
|
}
|
|
|
|
func fieldNames(response storyResponse, t *testing.T) []string {
|
|
t.Helper()
|
|
names := []string{}
|
|
for name := range response.fields(t) {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func TestArticleCreateSiteHTTP(t *testing.T) {
|
|
t.Run("The site decides where the new article is created", func(t *testing.T) {
|
|
cases := []struct {
|
|
body string
|
|
siteID uint32
|
|
lang string
|
|
}{
|
|
{`{"title":"Plain","body":"x"}`, storyJournal, "en"},
|
|
{`{"title":"Plain","site":"journal"}`, storyJournal, "en"},
|
|
{`{"title":"Neuf","lang":"fr","site":"atelier"}`, storyAtelier, "fr"},
|
|
{`{"title":"Plain","site":"atelier"}`, storyAtelier, "fr"},
|
|
{`{"title":"Plain","site":null}`, storyJournal, "en"},
|
|
{`{"title":"Plain","site":"notes","lang":"en"}`, createSiteNotes, "en"},
|
|
}
|
|
baseline := fieldNames(newCreateSiteWorld(t).send(storyWriter, http.MethodPost, createSitePath, `{"title":"Plain"}`), t)
|
|
for _, c := range cases {
|
|
t.Run(c.body, func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
before := w.snapshot()
|
|
|
|
response := w.send(storyWriter, http.MethodPost, createSitePath, c.body)
|
|
|
|
created := createdTranslation(t, w, response)
|
|
if created.SiteID != c.siteID || created.Lang != c.lang || created.IsOnline || created.CreatedBy != storyWriter || created.StoryID == storyStory {
|
|
t.Errorf("unexpected article: %+v", created)
|
|
}
|
|
if got := fieldNames(response, t); !reflect.DeepEqual(got, baseline) {
|
|
t.Errorf("the response keys changed: %v vs %v", got, baseline)
|
|
}
|
|
w.expectUnchanged(before, created.ID)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Refused target sites", func(t *testing.T) {
|
|
cases := []struct {
|
|
caller uint32
|
|
body string
|
|
status int
|
|
message string
|
|
event bool
|
|
}{
|
|
{storyWriter, `{"title":"Plain","site":"missing"}`, 404, apperr.SiteNotFoundErrorMessage, true},
|
|
{storyWriter, `{"title":"Plain","site":"Bad Key!"}`, 400, apperr.InvalidSiteKeyErrorMessage, false},
|
|
{storyWriter, `{"title":"Plain","site":""}`, 400, apperr.InvalidSiteKeyErrorMessage, false},
|
|
{storyWriter, `{"title":"Plain","site":"atelier","lang":"es"}`, 422, apperr.LanguageNotEnabledErrorMessage, true},
|
|
{storyWriter, `{"title":"Plain","site":"notes","lang":"fr"}`, 422, apperr.LanguageNotEnabledErrorMessage, true},
|
|
{storyWriter, `{"title":"Plain","site":"journal","lang":"es"}`, 409, apperr.LanguageUnavailableErrorMessage, true},
|
|
{storyWriter, `{"title":"Plain","site":"atelier","lang":"pt"}`, 422, apperr.LanguageNotEnabledErrorMessage, true},
|
|
{storyWriter, `{"title":"Plain","site":"atelier","lang":"xx"}`, 422, apperr.UnsupportedLanguageErrorMessage, true},
|
|
{storyWriter, `{"title":"Plain","site":"atelier","lang":"english"}`, 422, apperr.InvalidLanguageErrorMessage, true},
|
|
{storyWriter, `{"title":"ab","site":"atelier"}`, 400, "", false},
|
|
{storyReader, `{"title":"Plain","site":"atelier"}`, 403, apperr.MissingPermissionErrorMessage, false},
|
|
{storyAnon, `{"title":"Plain","site":"atelier"}`, 401, "You must be logged in to access this resource.", false},
|
|
{storyWriter, `{"title":"Plain","site":"fresh","lang":"fr"}`, 409, apperr.SetupRequiredErrorMessage, true},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.body, func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
before := w.snapshot()
|
|
|
|
response := w.send(c.caller, http.MethodPost, createSitePath, c.body)
|
|
|
|
if response.Code != c.status || (c.message != "" && strings.TrimSpace(response.text()) != c.message) {
|
|
t.Fatalf("expected %d %q, got %d %q", c.status, c.message, response.Code, response.text())
|
|
}
|
|
if strings.HasPrefix(strings.TrimSpace(response.text()), "{") {
|
|
t.Errorf("a plain text body expected: %q", response.text())
|
|
}
|
|
w.expectUnchanged(before)
|
|
events := w.eventsOf(audit.ActionArticleCreate)
|
|
if c.event && (len(events) != 1 || events[0].Outcome != audit.OutcomeFailed || events[0].FailureReason == nil || *events[0].FailureReason == "") {
|
|
t.Errorf("one failed article.create event with a plain reason expected: %+v", events)
|
|
}
|
|
if !c.event && c.status != http.StatusForbidden && len(events) != 0 {
|
|
t.Errorf("no event expected: %+v", events)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("The language rules are those of the chosen site", func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
|
|
atelier := w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Neuf","lang":"fr","site":"atelier"}`)
|
|
journal := w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Neuf","lang":"es","site":"journal"}`)
|
|
|
|
atelier.expect(t, http.StatusCreated, "")
|
|
journal.expect(t, http.StatusConflict, apperr.LanguageUnavailableErrorMessage)
|
|
})
|
|
|
|
t.Run("The slug is free within the chosen site and language", func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
w.put(article.Article{ID: 90, SiteID: storyJournal, Lang: "fr", Slug: "couts", Title: "Couts"})
|
|
w.put(article.Article{ID: 91, SiteID: storyAtelier, Lang: "fr", Slug: "couts", Title: "Couts"})
|
|
w.put(article.Article{ID: 92, SiteID: storyAtelier, Lang: "en", Slug: "frais", Title: "Frais"})
|
|
|
|
clash := createdTranslation(t, w, w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Couts","lang":"fr","site":"atelier"}`))
|
|
otherSite := w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Couts","lang":"en","site":"atelier"}`)
|
|
plainJournal := w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Frais","lang":"fr","site":"atelier"}`)
|
|
notClashing := w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Couts","lang":"en","site":"journal"}`)
|
|
|
|
if clash.Slug != "couts-2" {
|
|
t.Errorf("expected couts-2, got %q", clash.Slug)
|
|
}
|
|
if got := createdTranslation(t, w, otherSite).Slug; got != "couts" {
|
|
t.Errorf("another language must keep the plain slug, got %q", got)
|
|
}
|
|
if got := createdTranslation(t, w, plainJournal).Slug; got != "frais" {
|
|
t.Errorf("another language must keep the plain slug, got %q", got)
|
|
}
|
|
if got := createdTranslation(t, w, notClashing).Slug; got != "couts" {
|
|
t.Errorf("another site must keep the plain slug, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("A site together with a translation is refused", func(t *testing.T) {
|
|
bodies := []string{
|
|
`{"title":"Couts","lang":"fr","story_id":5,"site":"atelier"}`,
|
|
`{"title":"Couts","lang":"fr","story_id":5,"source_article_id":10,"copy":true,"site":"journal"}`,
|
|
`{"title":"Couts","site":"atelier","source_article_id":10}`,
|
|
`{"title":"Couts","site":"atelier","copy":true}`,
|
|
}
|
|
for _, body := range bodies {
|
|
t.Run(body, func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
before := w.snapshot()
|
|
|
|
response := w.send(storyWriter, http.MethodPost, createSitePath, body)
|
|
|
|
response.expect(t, http.StatusBadRequest, apperr.BodyErrorMessage)
|
|
w.expectUnchanged(before)
|
|
if len(w.recorder.events) != 0 {
|
|
t.Errorf("the translation use case must not be reached")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("A translation keeps the site of its source", func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
|
|
response := w.send(storyWriter, http.MethodPost, translationsPath, `{"lang":"fr","title":"Couts","copy":true,"site":"atelier"}`)
|
|
|
|
if created := createdTranslation(t, w, response); created.SiteID != storyJournal {
|
|
t.Errorf("the translation must stay in site 1: %+v", created)
|
|
}
|
|
})
|
|
|
|
t.Run("Visibility and activity", func(t *testing.T) {
|
|
w := newCreateSiteWorld(t)
|
|
|
|
created := createdTranslation(t, w, w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Neuf","lang":"fr","site":"atelier"}`))
|
|
|
|
anonymous := w.send(storyAnon, http.MethodGet, createSitePath+idText(created.ID), "")
|
|
anonymous.expect(t, http.StatusNotFound, apperr.ArticleNotFoundErrorMessage)
|
|
for _, key := range []string{"journal", "atelier"} {
|
|
public := w.send(storyAnon, http.MethodGet, createSitePath+"?site="+key, "")
|
|
if strings.Contains(public.text(), `"Neuf"`) {
|
|
t.Errorf("the offline article must not be public on %s", key)
|
|
}
|
|
}
|
|
atelier := w.send(storyWriter, http.MethodGet, createSitePath+"?site=atelier", "")
|
|
journal := w.send(storyWriter, http.MethodGet, createSitePath+"?site=journal", "")
|
|
if !strings.Contains(atelier.text(), `"Neuf"`) || strings.Contains(journal.text(), `"Neuf"`) {
|
|
t.Errorf("only atelier lists the new article: %d %s / %d %s", atelier.Code, atelier.text(), journal.Code, journal.text())
|
|
}
|
|
events := w.eventsOf(audit.ActionArticleCreate)
|
|
if len(events) != 1 || events[0].Outcome != audit.OutcomeSuccess || events[0].Params[audit.ParamSite] != "atelier" || events[0].Target.ID != idText(created.ID) {
|
|
t.Errorf("one success event with the site key expected: %+v", events)
|
|
}
|
|
})
|
|
}
|