212 lines
8.9 KiB
Go
212 lines
8.9 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/audit"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const genericFailureSentence = "Something went wrong on our side. Please try again."
|
|
|
|
func TestSitesActivityLog(t *testing.T) {
|
|
t.Run("A successful write is recorded once after the change", func(t *testing.T) {
|
|
cases := []struct {
|
|
request string
|
|
action string
|
|
prepare func(e *siteEnv)
|
|
method string
|
|
path string
|
|
body string
|
|
status int
|
|
}{
|
|
{"POST /v1/site/ for \"Shop\"", "site.create", func(e *siteEnv) {}, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}`, http.StatusCreated},
|
|
{"PATCH /v1/site/2 renaming it", "site.edit", func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Boutique", "shop", false)
|
|
}, http.MethodPatch, "/v1/site/2", `{"name": "Shop"}`, http.StatusOK},
|
|
{"PATCH /v1/site/2/default", "site.set_default", func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
}, http.MethodPatch, "/v1/site/2/default", "", http.StatusOK},
|
|
{"DELETE /v1/site/2", "site.delete", func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
}, http.MethodDelete, "/v1/site/2", "", http.StatusNoContent},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.request, func(t *testing.T) {
|
|
e := newSiteEnvWithMain(t)
|
|
c.prepare(e)
|
|
|
|
rec := e.owner(t, c.method, c.path, c.body)
|
|
|
|
if rec.Code != c.status {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
event := e.single(t)
|
|
assertProducerEvent(t, event, c.action, audit.CategorySites, audit.OutcomeSuccess)
|
|
assertProducerTarget(t, event, "site", "2", "Shop")
|
|
assertProducerOwnerOrigin(t, event)
|
|
if event.Category != "sites" {
|
|
t.Errorf("category = %s", event.Category)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("A refused write is recorded as failed with a plain reason", func(t *testing.T) {
|
|
cases := []struct {
|
|
request string
|
|
action string
|
|
reason string
|
|
status int
|
|
prepare func(e *siteEnv)
|
|
method string
|
|
path string
|
|
body string
|
|
}{
|
|
{"POST /v1/site/ with a taken key", "site.create", "A site with that key already exists.", http.StatusConflict, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
}, http.MethodPost, "/v1/site/", `{"name": "Other", "key": "shop", "base_url": "https://other.example.com"}`},
|
|
{"PATCH /v1/site/2 changing the key", "site.edit", "The key of a site cannot be changed.", http.StatusBadRequest, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
}, http.MethodPatch, "/v1/site/2", `{"key": "store"}`},
|
|
{"DELETE /v1/site/1 on the default site", "site.delete", "The default site cannot be deleted.", http.StatusForbidden, func(e *siteEnv) {}, http.MethodDelete, "/v1/site/1", ""},
|
|
{"DELETE /v1/site/2 with articles", "site.delete", "The site still has articles.", http.StatusConflict, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
e.seedArticle(1, siteShopID, true)
|
|
}, http.MethodDelete, "/v1/site/2", ""},
|
|
{"POST /v1/site/ with an invalid base URL", "site.create", "The site address is not valid.", http.StatusBadRequest, func(e *siteEnv) {}, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "http://shop.example.com"}`},
|
|
{"PATCH /v1/site/2 with an invalid locale", "site.edit", "The locale is not valid.", http.StatusBadRequest, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
}, http.MethodPatch, "/v1/site/2", `{"locale": "not a locale"}`},
|
|
{"PATCH /v1/site/2 with is_default", "site.edit", "Use the default site action to change the default.", http.StatusBadRequest, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
}, http.MethodPatch, "/v1/site/2", `{"is_default": true}`},
|
|
{"DELETE /v1/site/2 reassigned to itself", "site.delete", "The destination site is not valid.", http.StatusBadRequest, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
e.seedArticle(1, siteShopID, true)
|
|
}, http.MethodDelete, "/v1/site/2?reassign_to=2", ""},
|
|
{"POST /v1/site/ when the repository fails with \"db is locked\"", "site.create", genericFailureSentence, http.StatusInternalServerError, func(e *siteEnv) {
|
|
e.sites.fail.failNextWith("Create", errors.New("db is locked"))
|
|
}, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}`},
|
|
{"PATCH /v1/site/2/default when the repository fails", "site.set_default", genericFailureSentence, http.StatusInternalServerError, func(e *siteEnv) {
|
|
e.sites.seed(siteShopID, "Shop", "shop", false)
|
|
e.sites.fail.failNext("SetDefault")
|
|
}, http.MethodPatch, "/v1/site/2/default", ""},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.request, func(t *testing.T) {
|
|
e := newSiteEnvWithMain(t)
|
|
c.prepare(e)
|
|
|
|
rec := e.owner(t, c.method, c.path, c.body)
|
|
|
|
if rec.Code != c.status {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
event := e.single(t)
|
|
assertProducerEvent(t, event, c.action, audit.CategorySites, audit.OutcomeFailed)
|
|
assertProducerFailure(t, event, c.reason)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Nothing is recorded when there is nothing to attribute", func(t *testing.T) {
|
|
cases := []struct {
|
|
request string
|
|
as uint32
|
|
method string
|
|
path string
|
|
body string
|
|
}{
|
|
{"PATCH /v1/site/999 as the owner", apiKeyOwnerId, http.MethodPatch, "/v1/site/999", `{"name": "X"}`},
|
|
{"DELETE /v1/site/999 as the owner", apiKeyOwnerId, http.MethodDelete, "/v1/site/999", ""},
|
|
{"POST /v1/site/ with an unknown image", apiKeyOwnerId, http.MethodPost, "/v1/site/", `{"name": "Other", "key": "other", "base_url": "https://other.example.com", "default_social_image_id": 999}`},
|
|
{"POST /v1/site/ as an admin (403)", apiKeyDevId, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}`},
|
|
{"POST /v1/site/ with no credentials (401)", 0, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}`},
|
|
{"PATCH /v1/site/abc as the owner", apiKeyOwnerId, http.MethodPatch, "/v1/site/abc", `{"name": "X"}`},
|
|
{"POST /v1/site/ with a malformed body", apiKeyOwnerId, http.MethodPost, "/v1/site/", "{not json"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.request, func(t *testing.T) {
|
|
e := newSiteEnvWithShop(t)
|
|
e.as(t, c.as, c.method, c.path, c.body)
|
|
e.noEvent(t)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("A failing audit repository never changes the response", func(t *testing.T) {
|
|
body := `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}`
|
|
healthy := newSiteEnvWithMain(t)
|
|
healthyRec := healthy.owner(t, http.MethodPost, "/v1/site/", body)
|
|
broken := newSiteEnvWithMain(t)
|
|
broken.audit.probe.appendErr = errDiskFull
|
|
|
|
rec := broken.owner(t, http.MethodPost, "/v1/site/", body)
|
|
|
|
if rec.Code != healthyRec.Code || rec.Body.String() != healthyRec.Body.String() {
|
|
t.Errorf("broken %d %s, healthy %d %s", rec.Code, rec.Body.String(), healthyRec.Code, healthyRec.Body.String())
|
|
}
|
|
if len(broken.sites.sites) != 2 {
|
|
t.Errorf("the site was not created")
|
|
}
|
|
})
|
|
|
|
t.Run("Reads are never recorded", func(t *testing.T) {
|
|
e := newSiteEnvWithShop(t)
|
|
|
|
e.owner(t, http.MethodGet, "/v1/site/", "")
|
|
|
|
e.noEvent(t)
|
|
})
|
|
|
|
t.Run("The key is never a change field", func(t *testing.T) {
|
|
e := newSiteEnvWithShop(t)
|
|
e.seedBanner(5, "banner.png", "hash")
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/site/2", `{"name": "Boutique", "base_url": "https://boutique.example.com", "default_robots": "noindex", "locale": "fr", "default_social_image_id": 5}`)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
fields := []string{}
|
|
for _, change := range e.single(t).Changes {
|
|
fields = append(fields, change.Field)
|
|
}
|
|
if !reflect.DeepEqual(fields, []string{"name", "base_url", "default_robots", "locale", "default_social_image"}) {
|
|
t.Errorf("fields = %v", fields)
|
|
}
|
|
})
|
|
|
|
t.Run("The activity API lists site events", func(t *testing.T) {
|
|
e := newSiteEnvWithMain(t)
|
|
e.owner(t, http.MethodPost, "/v1/site/", `{"name": "Shop", "key": "shop", "base_url": "https://shop.example.com"}`)
|
|
e.owner(t, http.MethodPatch, "/v1/site/2/default", "")
|
|
|
|
rec := e.owner(t, http.MethodGet, "/v1/activity/?category=sites", "")
|
|
|
|
var page struct {
|
|
Items []struct {
|
|
Action string `json:"action"`
|
|
Category string `json:"category"`
|
|
} `json:"items"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &page); err != nil || rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
actions := []string{}
|
|
for _, item := range page.Items {
|
|
if item.Category != "sites" {
|
|
t.Errorf("item category = %s", item.Category)
|
|
}
|
|
actions = append(actions, item.Action)
|
|
}
|
|
if len(actions) != 2 || !strings.Contains(strings.Join(actions, ","), "site.create") || !strings.Contains(strings.Join(actions, ","), "site.set_default") {
|
|
t.Errorf("actions = %v", actions)
|
|
}
|
|
})
|
|
}
|