Files
sm-seo-plugin/redirects_test.go
Jakub Zych e31ac7c1c6 feat: address-change 301 (tracer)
- golem15_seo_redirects table and Redirect model
- AddressChanged: old -> new 301 in the save transaction, earlier
  redirects pointed at the new address, self-loops dropped
- the SEO tab save compares descriptor URLs with the entry URLs and
  answers the redirects.auto_created notice
- resolve answers redirect with the absolute target for an exact source
2026-10-27 11:20:59 +01:00

122 lines
4.0 KiB
Go

package seo_test
import (
"encoding/json"
"net/http"
"strings"
"testing"
)
// redirectRow is a stored redirect as the tests read it.
type redirectRow struct {
ID uint
Source string
Match string
Target string
Status int
Query string
IgnoreCase bool
TrailingSlash bool
Origin string
}
// redirects returns every stored redirect in id order.
func (e *testEnv) redirects(t *testing.T) []redirectRow {
t.Helper()
var rows []redirectRow
if err := e.db.Raw(`SELECT id, source, match, target, status, query, ignore_case, trailing_slash, origin FROM golem15_seo_redirects ORDER BY id`).Scan(&rows).Error; err != nil {
t.Fatal(err)
}
return rows
}
// redirectTo returns the target of the exact redirect of source, "" when
// there is none.
func redirectTo(rows []redirectRow, source string) string {
for _, r := range rows {
if r.Source == source && r.Match == "exact" {
return r.Target
}
}
return ""
}
// seedRedirect stores a redirect directly.
func (e *testEnv) seedRedirect(t *testing.T, source, match, target string, status int) {
t.Helper()
if err := e.db.Exec(`INSERT INTO golem15_seo_redirects (source, match, target, status, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())`,
source, match, target, status).Error; err != nil {
t.Fatal(err)
}
}
// saveNotices returns the save notices of an admin save response.
func saveNotices(t *testing.T, body []byte) []struct{ Field, Message string } {
t.Helper()
var out struct {
Meta struct {
Notices []struct {
Field string `json:"field"`
Message string `json:"message"`
} `json:"notices"`
} `json:"meta"`
}
if err := json.Unmarshal(body, &out); err != nil {
t.Fatal(err)
}
var notices []struct{ Field, Message string }
for _, n := range out.Meta.Notices {
notices = append(notices, struct{ Field, Message string }{n.Field, n.Message})
}
return notices
}
// TestSlugChange301 is the plan's tracer (SC-3, D-18): an editor changes a
// published page's slug in the admin; inside the save a 301 from the old
// address to the new one is created, an earlier redirect to the old
// address is pointed straight at the new one, the editor gets the
// redirects.auto_created notice, and the resolve answer redirects the old
// address in one hop.
func TestSlugChange301(t *testing.T) {
env := bootEnv(t)
token := env.admin(t, true)
id := env.createPage(t, token, map[string]any{"title": "Alpha", "slug": "a", "published": true})
env.seedRedirect(t, "/pages/x", "exact", "/pages/a", 301)
rec := env.expect(t, http.StatusOK, http.MethodPut, adminAPI("/acme/pages/pages/"+id), token, map[string]any{
"title": "Alpha", "slug": "b", "published": true,
})
var found bool
for _, n := range saveNotices(t, rec.Body.Bytes()) {
if n.Field == "slug" && strings.Contains(n.Message, "“/pages/a”") && strings.Contains(n.Message, "“/pages/b”") && strings.Contains(n.Message, "1 earlier") {
found = true
}
}
if !found {
t.Fatalf("update notices lack redirects.auto_created on slug: %s", rec.Body.String())
}
got := env.resolve(t, "/pages/a")
if got["action"] != "redirect" || got["status"] != float64(301) || got["location"] != siteURL+"/pages/b" {
t.Fatalf("resolve /pages/a = %v, want redirect 301 to %s/pages/b", got, siteURL)
}
if got := env.resolve(t, "/pages/b"); got["action"] != "ok" {
t.Fatalf("resolve /pages/b = %v, want ok", got)
}
rows := env.redirects(t)
if target := redirectTo(rows, "/pages/x"); target != "/pages/b" {
t.Fatalf("earlier redirect /pages/x -> %q, want flattened to /pages/b (%+v)", target, rows)
}
if target := redirectTo(rows, "/pl/pages/a"); target != "/pl/pages/b" {
t.Fatalf("pl redirect /pl/pages/a -> %q, want /pl/pages/b (%+v)", target, rows)
}
for _, r := range rows {
if r.Source == "/pages/a" && r.Origin != "slug" {
t.Fatalf("address-change redirect origin = %q, want slug", r.Origin)
}
}
if got := env.resolve(t, "/pages/x"); got["location"] != siteURL+"/pages/b" {
t.Fatalf("resolve /pages/x = %v, want one hop to /pages/b", got)
}
}