- 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
122 lines
4.0 KiB
Go
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)
|
|
}
|
|
}
|