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) } }) }