- posts: FormAfterCreate/Update purge after the content_html translations, new FormAfterDelete; categories: FormAfterCreate and FormAfterDelete - NewSiteCachePurger refuses half configuration, non-http(s) URLs, user info, fragments, http to a non-loopback host and tokens under 32 chars; Boot logs the endpoint host and path once - TestPostsAdminPurgesSiteCache smoke test - README: Configuration section (keys, contract, timing, scope), API rows
203 lines
6.3 KiB
Go
203 lines
6.3 KiB
Go
package journal_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-journal-plugin/controllers"
|
|
"git.golem15.com/golem15/sm-journal-plugin/models"
|
|
)
|
|
|
|
// purgeTestToken is a 40-character purge token for the receiver.
|
|
const purgeTestToken = "purge-test-token-0123456789abcdefghijklm"
|
|
|
|
const purgeTestPath = "/api/__journal__/cache/purge"
|
|
|
|
// purgeRequest is one request the purge receiver got.
|
|
type purgeRequest struct {
|
|
Method string
|
|
Path string
|
|
Authorization string
|
|
ContentType string
|
|
Body map[string]string
|
|
// Seen is what the receiver's probe read from the database while the
|
|
// request was in flight.
|
|
Seen string
|
|
}
|
|
|
|
// purgeReceiver stands in for the host site's purge route on 127.0.0.1.
|
|
type purgeReceiver struct {
|
|
srv *httptest.Server
|
|
mu sync.Mutex
|
|
got []purgeRequest
|
|
status int
|
|
probe func() string
|
|
}
|
|
|
|
func newPurgeReceiver(t *testing.T) *purgeReceiver {
|
|
t.Helper()
|
|
r := &purgeReceiver{status: http.StatusOK}
|
|
r.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
raw, _ := io.ReadAll(req.Body)
|
|
var body map[string]string
|
|
_ = json.Unmarshal(raw, &body)
|
|
r.mu.Lock()
|
|
probe, status := r.probe, r.status
|
|
r.mu.Unlock()
|
|
seen := ""
|
|
if probe != nil {
|
|
seen = probe()
|
|
}
|
|
r.mu.Lock()
|
|
r.got = append(r.got, purgeRequest{
|
|
Method: req.Method,
|
|
Path: req.URL.Path,
|
|
Authorization: req.Header.Get("Authorization"),
|
|
ContentType: req.Header.Get("Content-Type"),
|
|
Body: body,
|
|
Seen: seen,
|
|
})
|
|
r.mu.Unlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if status == http.StatusOK {
|
|
_, _ = w.Write([]byte(`{"purged":3}`))
|
|
}
|
|
}))
|
|
t.Cleanup(r.srv.Close)
|
|
return r
|
|
}
|
|
|
|
// env returns the extra environment that points the Journal plugin at r.
|
|
func (r *purgeReceiver) env() []string {
|
|
return []string{
|
|
"SUMMER_GOLEM15__JOURNAL__SITE_CACHE__PURGE_URL=" + r.srv.URL + purgeTestPath,
|
|
"SUMMER_GOLEM15__JOURNAL__SITE_CACHE__PURGE_TOKEN=" + purgeTestToken,
|
|
}
|
|
}
|
|
|
|
// take returns the requests received so far and forgets them.
|
|
func (r *purgeReceiver) take() []purgeRequest {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
got := r.got
|
|
r.got = nil
|
|
return got
|
|
}
|
|
|
|
func (r *purgeReceiver) setProbe(probe func() string) {
|
|
r.mu.Lock()
|
|
r.probe = probe
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// expectOnePurge asserts exactly one contract request for kind and slug was
|
|
// received since the last take, and returns it.
|
|
func (r *purgeReceiver) expectOnePurge(t *testing.T, stage, kind, slug string) purgeRequest {
|
|
t.Helper()
|
|
got := r.take()
|
|
if len(got) != 1 {
|
|
t.Fatalf("%s: %d purge requests, want 1: %+v", stage, len(got), got)
|
|
}
|
|
req := got[0]
|
|
if req.Method != http.MethodPost || req.Path != purgeTestPath {
|
|
t.Fatalf("%s: purge request %s %s, want POST %s", stage, req.Method, req.Path, purgeTestPath)
|
|
}
|
|
if req.Authorization != "Bearer "+purgeTestToken {
|
|
t.Fatalf("%s: Authorization = %q, want the bearer token", stage, req.Authorization)
|
|
}
|
|
if !strings.HasPrefix(req.ContentType, "application/json") {
|
|
t.Fatalf("%s: Content-Type = %q, want application/json", stage, req.ContentType)
|
|
}
|
|
if req.Body["kind"] != kind || req.Body["slug"] != slug || len(req.Body) != 2 {
|
|
t.Fatalf("%s: purge body = %#v, want kind %q and slug %q", stage, req.Body, kind, slug)
|
|
}
|
|
return req
|
|
}
|
|
|
|
// TestCategoriesAdminPurgesSiteCache proves an admin update of a category
|
|
// sends one authorized purge request to the host site after the save
|
|
// transaction commits (G-16.2-2, D-19).
|
|
func TestCategoriesAdminPurgesSiteCache(t *testing.T) {
|
|
recv := newPurgeReceiver(t)
|
|
env := newPostsEnv(t, recv.env()...)
|
|
token := env.admin(t, controllers.PermissionAccessCategories)
|
|
|
|
rec := env.expect(t, http.StatusCreated, http.MethodPost, adminAPI(adminCategoriesPath), token, map[string]any{
|
|
"name": map[string]any{"en": "Purge news", "pl": "Wiadomości"},
|
|
"slug": map[string]any{"en": "purge-news", "pl": "wiadomosci"},
|
|
"content": "<p>Before</p>\n",
|
|
})
|
|
id := adminCreatedID(t, rec)
|
|
itemPath := adminAPI(fmt.Sprintf("%s/%d", adminCategoriesPath, id))
|
|
recv.take()
|
|
|
|
content := func() string {
|
|
var cat models.Category
|
|
if err := env.gdb.First(&cat, id).Error; err != nil {
|
|
return "error: " + err.Error()
|
|
}
|
|
if cat.Content == nil {
|
|
return ""
|
|
}
|
|
return *cat.Content
|
|
}
|
|
recv.setProbe(content)
|
|
|
|
rec = env.expect(t, http.StatusOK, http.MethodGet, itemPath, token, nil)
|
|
var served map[string]json.RawMessage
|
|
if err := json.Unmarshal(adminDecode(t, rec).Data, &served); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const edited = "<p>After the edit</p>\n"
|
|
body := servedBody(served, "name", "slug")
|
|
body["content"] = edited
|
|
env.expect(t, http.StatusOK, http.MethodPut, itemPath, token, body)
|
|
|
|
req := recv.expectOnePurge(t, "update", "category", "purge-news")
|
|
if req.Seen != edited {
|
|
t.Fatalf("the purge saw content %q, want the committed %q (purge before commit)", req.Seen, edited)
|
|
}
|
|
}
|
|
|
|
// TestPostsAdminPurgesSiteCache proves an admin update of a post sends one
|
|
// authorized purge request with kind post after the save commits.
|
|
func TestPostsAdminPurgesSiteCache(t *testing.T) {
|
|
recv := newPurgeReceiver(t)
|
|
env := newPostsEnv(t, recv.env()...)
|
|
token := env.admin(t, controllers.PermissionAccessPosts)
|
|
|
|
rec := env.expect(t, http.StatusCreated, http.MethodPost, adminAPI(adminPostsPath), token, map[string]any{
|
|
"title": map[string]any{"en": "Purge post", "pl": "Wpis"},
|
|
"slug": map[string]any{"en": "purge-post", "pl": "wpis"},
|
|
"content": map[string]any{"en": "<p>Before</p>"},
|
|
"excerpt": map[string]any{"en": "Excerpt", "pl": "Wyjątek"},
|
|
})
|
|
id := adminCreatedID(t, rec)
|
|
recv.take()
|
|
|
|
recv.setProbe(func() string {
|
|
var post models.Post
|
|
if err := env.gdb.First(&post, id).Error; err != nil {
|
|
return "error: " + err.Error()
|
|
}
|
|
return post.ContentHTML
|
|
})
|
|
served := servedForm(t, env, token, id)
|
|
const edited = "<p>After the edit</p>"
|
|
body := servedBody(served, "title", "slug", "excerpt")
|
|
body["content"] = map[string]any{"en": edited}
|
|
env.expect(t, http.StatusOK, http.MethodPut, adminAPI(fmt.Sprintf("%s/%d", adminPostsPath, id)), token, body)
|
|
|
|
req := recv.expectOnePurge(t, "update", "post", "purge-post")
|
|
if req.Seen != edited {
|
|
t.Fatalf("the purge saw content_html %q, want the committed %q (purge before commit)", req.Seen, edited)
|
|
}
|
|
}
|