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= pattern= nesting=false priority=0.5 changefreq=daily`}, {`{"url":"/b"}`, `type=url url=/b ref= pattern= nesting=false priority= changefreq=`}, {`{"type":null}`, `type= url= ref= pattern= nesting=false priority= changefreq=`}, {`{"type":"journal-post","reference":7,"priority":0.5,"changefreq":true,"nesting":1}`, `type=journal-post url= ref=7 pattern= nesting=true priority=0.5 changefreq=1`}, {`{"type":"x","priority":1.0,"reference":12.50,"changefreq":false,"nesting":"1"}`, `type=x url= ref=12.5 pattern= nesting=true priority=1 changefreq=`}, {`{"type":"x","nesting":"false"}`, `type=x url= ref= pattern= nesting=true priority= changefreq=`}, {`{"type":"x","nesting":""}`, `type=x url= ref= pattern= nesting=false priority= changefreq=`}, {`{"type":"x","nesting":null,"urlPattern":"/p/:slug"}`, `type=x url= ref= pattern=/p/:slug nesting=false priority= changefreq=`}, {`{"type":"x","reference":12345678901234}`, `type=x url= ref=12345678901234 pattern= nesting=false priority= changefreq=`}, {`null`, `type=url url= ref= pattern= nesting=false priority= changefreq=`}, } { 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 "" } 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))) }