Files
RenewCMS/test/article_create_site_http_test.go
T

248 lines
11 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("Without a site the article is created on the default site, which the activity names", func(t *testing.T) {
for _, body := range []string{`{"title":"Plain"}`, `{"title":"Plain","site":null}`} {
t.Run(body, func(t *testing.T) {
w := newCreateSiteWorld(t)
if _, err := w.sites.SetDefault(storyAtelier); err != nil {
t.Fatal(err)
}
created := createdTranslation(t, w, w.send(storyWriter, http.MethodPost, createSitePath, body))
if created.SiteID != storyAtelier || created.Lang != "fr" {
t.Errorf("created on site %d in %q, the default Atelier in fr expected", created.SiteID, created.Lang)
}
events := w.eventsOf(audit.ActionArticleCreate)
if len(events) != 1 || events[0].Outcome != audit.OutcomeSuccess || events[0].Params[audit.ParamSite] != "atelier" {
t.Errorf("one article.create naming the default site expected: %+v", events)
}
})
}
})
t.Run("An unknown site key records the plain reason That site does not exist.", func(t *testing.T) {
w := newCreateSiteWorld(t)
w.send(storyWriter, http.MethodPost, createSitePath, `{"title":"Plain","site":"missing"}`).expect(t, http.StatusNotFound, apperr.SiteNotFoundErrorMessage)
events := w.eventsOf(audit.ActionArticleCreate)
if len(events) != 1 || events[0].FailureReason == nil || *events[0].FailureReason != "That site does not exist." {
t.Fatalf("one failed article.create with the site reason 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)
}
})
}