package test import ( "RenewCMS/api/pkg/apperr" "RenewCMS/internal/domain/site" "net/http" "reflect" "strconv" "strings" "testing" ) const ( siteOwnerOnlyBody = "Only the owner can do this." siteSessionOnlyBody = "This action needs a real session." ) func TestSitesReading(t *testing.T) { t.Run("The list puts the default site first and then orders by name", func(t *testing.T) { e := newSiteEnv(t) e.sites.seed(4, "Zeta", "zeta", false) e.sites.seed(5, "Alpha", "alpha", false) e.sites.seed(6, "Main", "main", true) e.seedArticle(1, 5, true) e.seedArticle(2, 5, false) rec := e.owner(t, http.MethodGet, "/v1/site/", "") if rec.Code != http.StatusOK { t.Fatalf("status %d", rec.Code) } listed := decodeSiteList(t, rec) names := []string{} for _, s := range listed { names = append(names, s.Name) } if !reflect.DeepEqual(names, []string{"Main", "Alpha", "Zeta"}) { t.Fatalf("order = %v", names) } if listed[1].ArticleCount != 2 || listed[0].ArticleCount != 0 { t.Errorf("article_count = %+v", listed) } if !strings.Contains(rec.Body.String(), `"article_count"`) { t.Errorf("article_count missing: %s", rec.Body.String()) } }) t.Run("Who can read the list", func(t *testing.T) { cases := []struct { credentials string send func(e *siteEnv, t *testing.T) int status int }{ {"the owner's jwt cookie", func(e *siteEnv, t *testing.T) int { return e.as(t, apiKeyOwnerId, http.MethodGet, "/v1/site/", "").Code }, http.StatusOK}, {"a verified admin's jwt cookie", func(e *siteEnv, t *testing.T) int { return e.as(t, apiKeyDevId, http.MethodGet, "/v1/site/", "").Code }, http.StatusOK}, {"a valid API key", func(e *siteEnv, t *testing.T) int { raw, _ := e.issueKey(apiKeyDevId, "Deploy bot") return e.asKey(t, raw, http.MethodGet, "/v1/site/", "").Code }, http.StatusOK}, {"an unverified account's jwt cookie", func(e *siteEnv, t *testing.T) int { return e.as(t, apiKeyQuinnId, http.MethodGet, "/v1/site/", "").Code }, http.StatusForbidden}, {"no credentials", func(e *siteEnv, t *testing.T) int { return e.anonymous(t, http.MethodGet, "/v1/site/", "").Code }, http.StatusUnauthorized}, } for _, c := range cases { t.Run(c.credentials, func(t *testing.T) { e := newSiteEnvWithMain(t) if got := c.send(e, t); got != c.status { t.Errorf("status = %d, expected %d", got, c.status) } }) } }) t.Run("A site without image or locale returns nulls", func(t *testing.T) { e := newSiteEnvWithMain(t) e.sites.seed(siteBlogID, "Blog", "blog", false) rec := e.owner(t, http.MethodGet, "/v1/site/", "") if !strings.Contains(rec.Body.String(), `"default_social_image_id":null`) || !strings.Contains(rec.Body.String(), `"locale":null`) { t.Errorf("expected nulls in %s", rec.Body.String()) } }) } func TestSitesCreating(t *testing.T) { const shopBody = `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}` t.Run("The owner creates a site", func(t *testing.T) { e := newSiteEnvWithMain(t) body := `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "default_robots": "noindex", "locale": "fr-CA"}` rec := e.owner(t, http.MethodPost, "/v1/site/", body) if rec.Code != http.StatusCreated { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } created := decodeSiteBody(t, rec) want := site.Site{ID: created.ID, Name: "Shop", Key: "shop", BaseURL: "https://shop.example.com", DefaultRobots: "noindex", Locale: stringPointer("fr-CA"), TitleTemplate: site.DefaultTitleTemplate} if created.ID == 0 || !reflect.DeepEqual(created, want) { t.Errorf("created = %+v", created) } if stored, ok := e.sites.sites[created.ID]; !ok || stored.Key != "shop" { t.Errorf("site not stored: %+v", e.sites.sites) } }) t.Run("Optional fields default", func(t *testing.T) { e := newSiteEnvWithMain(t) created := decodeSiteBody(t, e.owner(t, http.MethodPost, "/v1/site/", shopBody)) if created.DefaultRobots != "index" || created.Locale != nil || created.DefaultSocialImageID != nil || created.IsDefault { t.Errorf("created = %+v", created) } }) t.Run("A new site can become the default at creation", func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "is_default": true}`) created := decodeSiteBody(t, rec) if rec.Code != http.StatusCreated || !created.IsDefault || e.sites.sites[siteMainID].IsDefault { t.Errorf("shop %+v, main %+v", created, e.sites.sites[siteMainID]) } if defaults := e.sites.defaults(); !reflect.DeepEqual(defaults, []uint32{created.ID}) { t.Errorf("defaults = %v", defaults) } }) t.Run("The base URL is normalised", func(t *testing.T) { cases := [][2]string{ {"https://shop.example.com/", "https://shop.example.com"}, {"https://Shop.Example.com", "https://shop.example.com"}, {"http://localhost:5173/", "http://localhost:5173"}, } for _, c := range cases { t.Run(c[0], func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "`+c[0]+`"}`) if got := decodeSiteBody(t, rec).BaseURL; rec.Code != http.StatusCreated || got != c[1] { t.Errorf("status %d, base_url %q, expected %q", rec.Code, got, c[1]) } }) } }) t.Run("The base URL must be https except loopback and carry no path", func(t *testing.T) { cases := []struct { sent string status int body string }{ {"https://shop.example.com", 201, ""}, {"http://localhost:8080", 201, ""}, {"http://127.0.0.1:8080", 201, ""}, {"http://[::1]:8080", 201, ""}, {"http://shop.example.com", 400, "invalid_site_url"}, {"https://shop.example.com/blog", 400, "invalid_site_url"}, {"https://shop.example.com/?a=1", 400, "invalid_site_url"}, {"https://user@shop.example.com", 400, "invalid_site_url"}, {"ftp://shop.example.com", 400, "invalid_site_url"}, {"shop.example.com", 400, "invalid_site_url"}, } for _, c := range cases { t.Run(c.sent, func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "`+c.sent+`"}`) if rec.Code != c.status { t.Fatalf("status = %d, expected %d: %s", rec.Code, c.status, rec.Body.String()) } if c.body != "" && bodyText(rec) != c.body { t.Errorf("body = %q, expected %q", bodyText(rec), c.body) } }) } }) t.Run("The key format", func(t *testing.T) { cases := []struct { key string status int }{ {"shop", 201}, {"my-shop-2", 201}, {"ab", 201}, {"a", 400}, {"Shop", 400}, {"-shop", 400}, {"shop-", 400}, {"my--shop", 400}, {"my_shop", 400}, {strings.Repeat("a", 33), 400}, {"", 400}, {strings.Repeat("a", 32), 201}, } for _, c := range cases { t.Run("key "+strconv.Quote(c.key), func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "`+c.key+`", "base_url": "https://shop.example.com"}`) if rec.Code != c.status { t.Errorf("status = %d, expected %d: %s", rec.Code, c.status, rec.Body.String()) } }) } }) t.Run("A taken key is refused", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPost, "/v1/site/", shopBody) expectSiteStatusAndBody(t, rec, http.StatusConflict, "site_key_taken") if len(e.sites.sites) != 2 { t.Errorf("a site was added: %+v", e.sites.sites) } }) t.Run("Other fields are validated", func(t *testing.T) { cases := []struct { name string body string status int token string }{ {"name empty", `{"name": "", "key": "shop", "base_url": "https://shop.example.com"}`, 400, ""}, {"name 101 characters", `{"name": "` + strings.Repeat("n", 101) + `", "key": "shop", "base_url": "https://shop.example.com"}`, 400, ""}, {"default_robots follow", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "default_robots": "follow"}`, 400, ""}, {"locale english", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "locale": "english"}`, 400, "invalid_locale"}, {"locale fr_FR", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "locale": "fr_FR"}`, 400, "invalid_locale"}, {"default_social_image_id 999", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "default_social_image_id": 999}`, 400, "image_not_found"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", c.body) if rec.Code != c.status { t.Fatalf("status = %d: %s", rec.Code, rec.Body.String()) } if c.token != "" && bodyText(rec) != c.token { t.Errorf("body = %q, expected %q", bodyText(rec), c.token) } if c.token == "" && (bodyText(rec) == "" || strings.Contains(bodyText(rec), "_")) { t.Errorf("expected the validator error text, got %q", bodyText(rec)) } if len(e.sites.sites) != 1 { t.Errorf("a site was added: %+v", e.sites.sites) } }) } }) t.Run("A malformed body is refused", func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", "{not json") expectSiteStatusAndBody(t, rec, http.StatusBadRequest, apperr.BodyErrorMessage) if len(e.sites.sites) != 1 { t.Errorf("a site was added") } }) t.Run("A valid locale is accepted and an empty one clears it", func(t *testing.T) { cases := []struct { sent string stored *string }{ {`"en"`, stringPointer("en")}, {`"fr-CA"`, stringPointer("fr-CA")}, {`"zh-Hant-TW"`, stringPointer("zh-Hant-TW")}, {`""`, nil}, {`null`, nil}, } for _, c := range cases { t.Run(c.sent, func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.owner(t, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com", "locale": `+c.sent+`}`) if rec.Code != http.StatusCreated || !reflect.DeepEqual(decodeSiteBody(t, rec).Locale, c.stored) { t.Errorf("status %d, body %s", rec.Code, rec.Body.String()) } }) } }) } func TestSitesEditing(t *testing.T) { t.Run("The owner edits a site", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"name": "Boutique", "base_url": "https://boutique.example.com", "default_robots": "noindex", "locale": "fr"}`) updated := decodeSiteBody(t, rec) want := site.Site{ID: 2, Name: "Boutique", Key: "shop", BaseURL: "https://boutique.example.com", DefaultRobots: "noindex", Locale: stringPointer("fr")} if rec.Code != http.StatusOK || !reflect.DeepEqual(updated, want) { t.Errorf("status %d, updated %+v", rec.Code, updated) } }) t.Run("Absent fields are untouched", func(t *testing.T) { e := newSiteEnvWithShop(t) shop := e.sites.sites[siteShopID] shop.Locale, shop.DefaultRobots = stringPointer("fr"), "noindex" e.sites.sites[siteShopID] = shop rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"name": "Boutique"}`) want := shop want.Name = "Boutique" if rec.Code != http.StatusOK || !reflect.DeepEqual(e.sites.sites[siteShopID], want) { t.Errorf("status %d, site %+v", rec.Code, e.sites.sites[siteShopID]) } }) t.Run("The default social image is set and cleared", func(t *testing.T) { e := newSiteEnvWithShop(t) e.seedBanner(5, "banner.png", "hash") set := e.owner(t, http.MethodPatch, "/v1/site/2", `{"default_social_image_id": 5}`) afterSet := e.sites.sites[siteShopID].DefaultSocialImageID cleared := e.owner(t, http.MethodPatch, "/v1/site/2", `{"default_social_image_id": null}`) if set.Code != http.StatusOK || afterSet == nil || *afterSet != 5 { t.Errorf("set: status %d, image %v", set.Code, afterSet) } if cleared.Code != http.StatusOK || e.sites.sites[siteShopID].DefaultSocialImageID != nil { t.Errorf("clear: status %d, image %v", cleared.Code, e.sites.sites[siteShopID].DefaultSocialImageID) } }) t.Run("The key never changes", func(t *testing.T) { cases := []struct { key string status int body string }{{"shop", 200, ""}, {"store", 400, "key_immutable"}} for _, c := range cases { t.Run(c.key, func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"key": "`+c.key+`"}`) if rec.Code != c.status || (c.body != "" && bodyText(rec) != c.body) { t.Errorf("status %d body %q", rec.Code, bodyText(rec)) } if e.sites.sites[siteShopID].Key != "shop" { t.Errorf("key changed to %q", e.sites.sites[siteShopID].Key) } }) } }) t.Run("A rejected key change leaves every other field unchanged", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"name": "Boutique", "key": "store"}`) expectSiteStatusAndBody(t, rec, http.StatusBadRequest, "key_immutable") if e.sites.sites[siteShopID].Name != "Shop" { t.Errorf("name changed to %q", e.sites.sites[siteShopID].Name) } }) t.Run("The default flag is not editable here", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"is_default": true}`) expectSiteStatusAndBody(t, rec, http.StatusBadRequest, "use_set_default") if !reflect.DeepEqual(e.sites.defaults(), []uint32{siteMainID}) { t.Errorf("defaults = %v", e.sites.defaults()) } }) t.Run("Editing is validated like creation", func(t *testing.T) { cases := []struct { name string body string token string }{ {"base_url http", `{"base_url": "http://shop.example.com"}`, "invalid_site_url"}, {"base_url path", `{"base_url": "https://shop.example.com/x"}`, "invalid_site_url"}, {"locale fr_FR", `{"locale": "fr_FR"}`, "invalid_locale"}, {"default_social_image_id 999", `{"default_social_image_id": 999}`, "image_not_found"}, {"name empty", `{"name": ""}`, ""}, {"default_robots follow", `{"default_robots": "follow"}`, ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { e := newSiteEnvWithShop(t) before := e.sites.snapshot() rec := e.owner(t, http.MethodPatch, "/v1/site/2", c.body) if rec.Code != http.StatusBadRequest { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } if c.token != "" && bodyText(rec) != c.token { t.Errorf("body = %q, expected %q", bodyText(rec), c.token) } if c.token == "" && (bodyText(rec) == "" || strings.Contains(bodyText(rec), "_")) { t.Errorf("expected the validator error text, got %q", bodyText(rec)) } if !reflect.DeepEqual(before, e.sites.snapshot()) { t.Errorf("site changed") } }) } }) t.Run("A missing site is not found", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/999", `{"name": "X"}`) expectSiteStatusAndBody(t, rec, http.StatusNotFound, "site_not_found") }) t.Run("A no-op edit records nothing", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"name": "Shop", "base_url": "https://shop.example.com/", "default_robots": "index", "locale": null}`) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } e.noEvent(t) }) t.Run("A malformed id is refused", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/abc", `{"name": "X"}`) expectSiteStatusAndBody(t, rec, http.StatusBadRequest, apperr.IdUint32ErrorMessage) }) } func TestSitesDefault(t *testing.T) { t.Run("The owner makes another site the default", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/2/default", "") got := decodeSiteBody(t, rec) if rec.Code != http.StatusOK || got.Name != "Shop" || !got.IsDefault || e.sites.sites[siteMainID].IsDefault { t.Errorf("status %d, body %s", rec.Code, rec.Body.String()) } }) t.Run("Setting the current default is a no-op", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/1/default", "") if rec.Code != http.StatusOK || e.sites.setDefaultCalls != 0 { t.Errorf("status %d, setDefault calls %d", rec.Code, e.sites.setDefaultCalls) } e.noEvent(t) }) t.Run("A missing site cannot become the default", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodPatch, "/v1/site/999/default", "") expectSiteStatusAndBody(t, rec, http.StatusNotFound, "site_not_found") }) t.Run("The swap is atomic", func(t *testing.T) { e := newSiteEnvWithShop(t) e.sites.fail.failNext("SetDefault") rec := e.owner(t, http.MethodPatch, "/v1/site/2/default", "") if rec.Code != http.StatusInternalServerError || !reflect.DeepEqual(e.sites.defaults(), []uint32{siteMainID}) { t.Errorf("status %d, defaults %v", rec.Code, e.sites.defaults()) } }) t.Run("New articles start on the new default", func(t *testing.T) { e := newSiteEnvWithShop(t) e.owner(t, http.MethodPatch, "/v1/site/2/default", "") rec := e.owner(t, http.MethodPost, "/v1/article", `{"title": "Spring release", "body": "Hello"}`) if rec.Code != http.StatusCreated || !strings.Contains(rec.Body.String(), `"site_id":2`) { t.Errorf("status %d, body %s", rec.Code, rec.Body.String()) } }) } func TestSitesDeleting(t *testing.T) { t.Run("A site without articles is deleted", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodDelete, "/v1/site/2", "") if rec.Code != http.StatusNoContent { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } if _, exists := e.sites.sites[siteShopID]; exists { t.Errorf("site 2 still exists") } }) t.Run("A site with articles needs a destination", func(t *testing.T) { e := newSiteEnvWithShop(t) for id := uint32(1); id <= 3; id++ { e.seedArticle(id, siteShopID, true) } before := e.sites.snapshot() rec := e.owner(t, http.MethodDelete, "/v1/site/2", "") expectSiteStatusAndBody(t, rec, http.StatusConflict, "site_has_articles") if !reflect.DeepEqual(before, e.sites.snapshot()) || e.articlesOn(siteShopID) != 3 { t.Errorf("state changed") } }) t.Run("The articles move in one transaction", func(t *testing.T) { e := newSiteEnvWithShop(t) e.sites.seed(siteBlogID, "Blog", "blog", false) for id := uint32(1); id <= 3; id++ { e.seedArticle(id, siteShopID, true) } rec := e.owner(t, http.MethodDelete, "/v1/site/2?reassign_to=3", "") if rec.Code != http.StatusNoContent || e.articlesOn(siteBlogID) != 3 { t.Errorf("status %d, articles on 3: %d", rec.Code, e.articlesOn(siteBlogID)) } if _, exists := e.sites.sites[siteShopID]; exists { t.Errorf("site 2 still exists") } }) t.Run("Reassign and delete are all-or-nothing", func(t *testing.T) { e := newSiteEnvWithShop(t) e.sites.seed(siteBlogID, "Blog", "blog", false) e.seedArticle(1, siteShopID, true) e.sites.fail.failNext("DeleteMovingArticles") rec := e.owner(t, http.MethodDelete, "/v1/site/2?reassign_to=3", "") if rec.Code != http.StatusInternalServerError || e.articlesOn(siteShopID) != 1 { t.Errorf("status %d, articles on 2: %d", rec.Code, e.articlesOn(siteShopID)) } if _, exists := e.sites.sites[siteShopID]; !exists { t.Errorf("site 2 vanished") } }) t.Run("The articles are moved by one repository operation", func(t *testing.T) { e := newSiteEnvWithShop(t) e.sites.seed(siteBlogID, "Blog", "blog", false) e.seedArticle(1, siteShopID, true) e.articles.fail.failNext("Update") rec := e.owner(t, http.MethodDelete, "/v1/site/2?reassign_to=3", "") if rec.Code != http.StatusNoContent || e.sites.deleteCalls != 1 || e.sites.deleteReassignments[0] == nil || *e.sites.deleteReassignments[0] != siteBlogID { t.Errorf("status %d, delete calls %d", rec.Code, e.sites.deleteCalls) } }) t.Run("An unusable destination", func(t *testing.T) { cases := []struct { reassign string status int body string }{ {"999", 404, "site_not_found"}, {"2", 400, "invalid_reassign"}, {"abc", 400, "invalid_reassign"}, {"-1", 400, "invalid_reassign"}, } for _, c := range cases { t.Run(c.reassign, func(t *testing.T) { e := newSiteEnvWithShop(t) for id := uint32(1); id <= 3; id++ { e.seedArticle(id, siteShopID, true) } before := e.sites.snapshot() rec := e.owner(t, http.MethodDelete, "/v1/site/2?reassign_to="+c.reassign, "") expectSiteStatusAndBody(t, rec, c.status, c.body) if !reflect.DeepEqual(before, e.sites.snapshot()) || e.articlesOn(siteShopID) != 3 { t.Errorf("state changed") } }) } }) t.Run("The default site cannot be deleted", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodDelete, "/v1/site/1", "") expectSiteStatusAndBody(t, rec, http.StatusForbidden, "default_site") }) t.Run("The default check comes before the articles check", func(t *testing.T) { e := newSiteEnvWithShop(t) for id := uint32(1); id <= 5; id++ { e.seedArticle(id, siteMainID, true) } rec := e.owner(t, http.MethodDelete, "/v1/site/1", "") expectSiteStatusAndBody(t, rec, http.StatusForbidden, "default_site") }) t.Run("A missing site is not found", func(t *testing.T) { e := newSiteEnvWithShop(t) rec := e.owner(t, http.MethodDelete, "/v1/site/999", "") expectSiteStatusAndBody(t, rec, http.StatusNotFound, "site_not_found") }) t.Run("An unused reassign_to is ignored", func(t *testing.T) { e := newSiteEnvWithShop(t) e.sites.seed(siteBlogID, "Blog", "blog", false) blogBefore := e.sites.sites[siteBlogID] rec := e.owner(t, http.MethodDelete, "/v1/site/2?reassign_to=3", "") if rec.Code != http.StatusNoContent || !reflect.DeepEqual(e.sites.sites[siteBlogID], blogBefore) { t.Errorf("status %d", rec.Code) } }) } func TestSitesOnlyTheOwnerWrites(t *testing.T) { t.Run("Other roles and keys are refused", func(t *testing.T) { requests := []struct{ method, path, body string }{ {http.MethodPost, "/v1/site/", `{"name": "Blog", "key": "blog", "base_url": "https://blog.example.com"}`}, {http.MethodPatch, "/v1/site/2", `{"name": "Boutique"}`}, {http.MethodPatch, "/v1/site/2/default", ""}, {http.MethodDelete, "/v1/site/2", ""}, } for _, request := range requests { t.Run(request.method+" "+request.path+" with a verified admin's jwt cookie", func(t *testing.T) { e := newSiteEnvWithShop(t) before := e.sites.snapshot() rec := e.as(t, apiKeyDevId, request.method, request.path, request.body) expectSiteStatusAndBody(t, rec, http.StatusForbidden, siteOwnerOnlyBody) if !reflect.DeepEqual(before, e.sites.snapshot()) { t.Errorf("a site changed") } }) t.Run(request.method+" "+request.path+" with a valid API key", func(t *testing.T) { e := newSiteEnvWithShop(t) before := e.sites.snapshot() raw, _ := e.issueKey(apiKeyOwnerId, "Deploy bot") rec := e.asKey(t, raw, request.method, request.path, request.body) expectSiteStatusAndBody(t, rec, http.StatusForbidden, siteSessionOnlyBody) if !reflect.DeepEqual(before, e.sites.snapshot()) { t.Errorf("a site changed") } }) } }) t.Run("The role is checked before the body", func(t *testing.T) { e := newSiteEnvWithMain(t) rec := e.as(t, apiKeyDevId, http.MethodPost, "/v1/site/", "{not json") expectSiteStatusAndBody(t, rec, http.StatusForbidden, siteOwnerOnlyBody) }) }