Files
sm-sitemap-plugin/models/definition_test.go
Jakub Zych 4e55939a1f test(17-06): sitemap parity unit tests
- TestURLTo (every Url::to golden case, BackendURL, URLPath),
  TestXMLEscaping (PHP XmlEscapingTest as behaviour), TestTiers (each
  default rule, first match, off, bad pattern fails Boot), TestRewrite,
  TestHooks, TestURLCap, TestManifest (fail-safe reads, key
  normalization, dates), TestFillable (PHP MassAssignmentTest),
  TestDefinitionItemJSON, TestBackendURL (trusted forwarded proto)
- TestImportWinter on Postgres: maps and drops with report, --theme,
  --dry-run, --force, settings upsert, refusals
2026-10-12 19:38:04 +02:00

146 lines
6.2 KiB
Go

package models_test
import (
"encoding/json"
"fmt"
"slices"
"strings"
"testing"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/lagoon"
)
// TestFillable ports the PHP MassAssignmentTest (SITE-001) as behaviour:
// Definition.Fillable is exactly theme and data, and a fill with extra keys
// (id, timestamps, unknown keys) writes only those two.
func TestFillable(t *testing.T) {
if got := (models.Definition{}).Fillable(); !slices.Equal(got, []string{"theme", "data"}) {
t.Fatalf("Fillable = %v, want [theme data]", got)
}
def := models.Definition{ID: 4}
requested := map[string]any{
"id": 99,
"theme": "site",
"data": `[{"type":"url","url":"/a"}]`,
"created_at": "2020-01-01 00:00:00",
"updated_at": "2020-01-01 00:00:00",
"is_admin": true,
}
for _, production := range []bool{true, false} {
d := def
if err := lagoon.Fill(&d, d.Fillable(), requested, production); err != nil {
t.Fatalf("Fill: %v", err)
}
if d.ID != 4 || d.CreatedAt != nil || d.UpdatedAt != nil || models.Text(d.Theme) != "site" {
t.Fatalf("filled definition = %+v", d)
}
if items := d.Items(); len(items) != 1 || models.Text(items[0].URL) != "/a" {
t.Fatalf("filled data = %+v", items)
}
}
if (models.Definition{}).TableName() != "golem15_sitemap_definitions" || (models.Definition{}).MorphName() != `Golem15\Sitemap\Models\Definition` {
t.Fatal("table or morph name changed")
}
if rules := (models.Definition{}).Rules(); len(rules) != 0 {
t.Fatalf("Rules = %v", rules)
}
if got := (models.Settings{}).Fillable(); slices.Contains(got, "id") || len(got) != 9 {
t.Fatalf("Settings.Fillable = %v", got)
}
}
// TestDefinitionItemJSON pins the tolerant decoding of PHP-stored items
// (strings, numbers, booleans and null for text fields, PHP truthiness for
// nesting, unknown keys ignored, a missing type is url, a null item a
// default url item, an object-keyed list in key order) and the PHP toArray
// encoding (key set and order, cmsPage always null, null text kept).
func TestDefinitionItemJSON(t *testing.T) {
for _, tc := range []struct {
raw string
want string
}{
{`{"type":"url","url":"/a","priority":"0.5","changefreq":"daily","nesting":"0","reference":null,"urlPattern":null,"cmsPage":"x","code":"c","exists":false,"fillable":["a"]}`,
`type=url url=/a ref=<nil> pattern=<nil> nesting=false priority=0.5 changefreq=daily`},
{`{"url":"/b"}`, `type=url url=/b ref=<nil> pattern=<nil> nesting=false priority=<nil> changefreq=<nil>`},
{`{"type":null}`, `type= url=<nil> ref=<nil> pattern=<nil> nesting=false priority=<nil> changefreq=<nil>`},
{`{"type":"journal-post","reference":7,"priority":0.5,"changefreq":true,"nesting":1}`,
`type=journal-post url=<nil> ref=7 pattern=<nil> nesting=true priority=0.5 changefreq=1`},
{`{"type":"x","priority":1.0,"reference":12.50,"changefreq":false,"nesting":"1"}`,
`type=x url=<nil> ref=12.5 pattern=<nil> nesting=true priority=1 changefreq=`},
{`{"type":"x","nesting":"false"}`, `type=x url=<nil> ref=<nil> pattern=<nil> nesting=true priority=<nil> changefreq=<nil>`},
{`{"type":"x","nesting":""}`, `type=x url=<nil> ref=<nil> pattern=<nil> nesting=false priority=<nil> changefreq=<nil>`},
{`{"type":"x","nesting":null,"urlPattern":"/p/:slug"}`, `type=x url=<nil> ref=<nil> pattern=/p/:slug nesting=false priority=<nil> changefreq=<nil>`},
{`{"type":"x","reference":12345678901234}`, `type=x url=<nil> ref=12345678901234 pattern=<nil> nesting=false priority=<nil> changefreq=<nil>`},
{`null`, `type=url url=<nil> ref=<nil> pattern=<nil> nesting=false priority=<nil> changefreq=<nil>`},
} {
var item models.DefinitionItem
if err := json.Unmarshal([]byte(tc.raw), &item); err != nil {
t.Fatalf("%s: %v", tc.raw, err)
}
if got := describe(item); got != tc.want {
t.Errorf("%s\n got %s\nwant %s", tc.raw, got, tc.want)
}
}
for _, raw := range []string{`{"type":["x"]}`, `{"url":{"a":1}}`, `{"nesting":[1]}`, `[1]`, `"x"`} {
var item models.DefinitionItem
if err := json.Unmarshal([]byte(raw), &item); err == nil {
t.Errorf("%s decoded without an error", raw)
}
}
// A list, an object keyed like a PHP array with gaps, null and empty.
var list models.DefinitionItems
if err := json.Unmarshal([]byte(`{"3":{"url":"/c"},"1":{"url":"/a"},"x":{"url":"/x"}}`), &list); err != nil || len(list) != 3 ||
models.Text(list[0].URL) != "/c" || models.Text(list[1].URL) != "/a" || models.Text(list[2].URL) != "/x" {
t.Fatalf("object list = %+v %v", list, err)
}
for raw, n := range map[string]int{`[]`: 0, `null`: 0, `[{"url":"/a"},null]`: 2} {
var l models.DefinitionItems
if err := json.Unmarshal([]byte(raw), &l); err != nil || len(l) != n {
t.Fatalf("%s = %+v %v", raw, l, err)
}
}
var bad models.DefinitionItems
if err := json.Unmarshal([]byte(`"items"`), &bad); err == nil {
t.Fatal("a string decoded as an item list")
}
// Encoding: the PHP toArray key set and order.
pattern := "/p/:slug"
item := models.DefinitionItem{Type: "journal-post", Reference: str("7"), URLPattern: &pattern, Nesting: true, Priority: str("0.5")}
raw, err := json.Marshal(item)
if err != nil {
t.Fatal(err)
}
if want := `{"changefreq":null,"priority":"0.5","nesting":true,"type":"journal-post","url":null,"reference":"7","cmsPage":null,"urlPattern":"/p/:slug"}`; string(raw) != want {
t.Fatalf("Marshal = %s\nwant %s", raw, want)
}
if raw, _ := json.Marshal(models.DefinitionItems(nil)); string(raw) != "[]" {
t.Fatalf("nil list = %s", raw)
}
var back models.DefinitionItem
if err := json.Unmarshal(raw, &back); err != nil || describe(back) != describe(item) {
t.Fatalf("round trip = %s, %v", describe(back), err)
}
if models.Text(nil) != "" || models.Text(str("x")) != "x" {
t.Fatal("Text")
}
if s, err := models.DecodeText(json.RawMessage(`1e3`)); err != nil || *s != "1000" {
t.Fatalf("DecodeText(1e3) = %v %v", s, err)
}
}
func str(s string) *string { return &s }
func describe(d models.DefinitionItem) string {
p := func(s *string) string {
if s == nil {
return "<nil>"
}
return *s
}
return strings.TrimSpace(fmt.Sprintf("type=%s url=%s ref=%s pattern=%s nesting=%v priority=%s changefreq=%s",
d.Type, p(d.URL), p(d.Reference), p(d.URLPattern), d.Nesting, p(d.Priority), p(d.Changefreq)))
}