package sitemap_test import ( "context" "encoding/json" "strings" "testing" "git.golem15.com/golem15/sm-sitemap-plugin/classes" "git.golem15.com/golem15/sm-sitemap-plugin/console" "git.golem15.com/golem15/sm-sitemap-plugin/models" ) // storedExtra reads the additional addresses of settings row 1. func (e *sitemapEnv) storedExtra(t *testing.T) []models.ExtraURL { t.Helper() s, err := classes.LoadSettings(e.gdb) if err != nil { t.Fatal(err) } return s.Extra() } // extraURLs lists the addresses of rows. func extraURLs(rows []models.ExtraURL) string { out := make([]string, 0, len(rows)) for _, r := range rows { out = append(out, r.URL) } return strings.Join(out, " ") } // TestImportWinter pins sitemap:import-winter on Postgres (D-32): the url // items of a WinterCMS definition become the additional addresses (static // and CMS page items through --page-url, types renamed by --map first), // every other item is dropped and listed, --theme picks a definition, // --dry-run writes nothing, stored addresses are replaced only with // --force, and frontend_url and frontend_default_locale become site_url and // default_locale while the PHP-parity keys are reported as ignored; missing, // empty and malformed exports and an unsafe frontend_url are refused. func TestImportWinter(t *testing.T) { e := bootIndex(t) ctx := context.Background() dir := t.TempDir() row := func(id int, theme string, items string) string { quoted, _ := json.Marshal(items) return `{"id": ` + string(rune('0'+id)) + `, "theme": "` + theme + `", "data": ` + string(quoted) + "}\n" } items := `[{"type":"url","url":"/","changefreq":"always","priority":"1.0"},` + `{"type":"blog-posts","reference":null,"nesting":"1","cmsPage":"blog/post"},` + `{"type":"acme-post","reference":"7"},` + `{"type":"static-page","reference":"about"},` + `{"type":"cms-page","reference":"news"},` + `{"type":"remove-me"},` + `{"type":"url","url":"javascript://%0aalert(1)%0a//x"},` + `{"type":"url","url":"/"}]` defs := writeFile(t, dir, "definitions.jsonl", row(1, "main", items)+"\n"+row(2, "other", `[{"type":"url","url":"/other"}]`)) run := func(opts console.WinterOptions) (console.WinterReport, string, error) { out, buf := testOutput() report, err := console.ImportWinterFiles(ctx, e.gdb, opts, out) return report, buf.String(), err } t.Run("refusals", func(t *testing.T) { for name, tc := range map[string]struct { opts console.WinterOptions want string }{ "no file": {console.WinterOptions{}, "give --definitions, --settings or both"}, "missing file": {console.WinterOptions{DefinitionsFile: dir + "/missing.jsonl"}, "File not found"}, "empty file": {console.WinterOptions{DefinitionsFile: writeFile(t, dir, "empty.jsonl", "\n\n")}, "holds no definition"}, "bad line": {console.WinterOptions{DefinitionsFile: writeFile(t, dir, "bad.jsonl", "{\n")}, "line 1"}, "several rows": {console.WinterOptions{DefinitionsFile: defs}, "holds 2 definitions (themes 'main', 'other'); pick one with --theme"}, "unknown theme": {console.WinterOptions{DefinitionsFile: defs, Theme: "nope"}, "no definition of theme 'nope'"}, "data not items": {console.WinterOptions{DefinitionsFile: writeFile(t, dir, "scalar.jsonl", `{"id":1,"theme":"t","data":"5"}`)}, "not a list of items"}, "bad settings": {console.WinterOptions{SettingsFile: writeFile(t, dir, "settings-bad.json", `[1]`)}, "must be a JSON object"}, } { _, out, err := run(tc.opts) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("%s: err = %v, want %q\n%s", name, err, tc.want, out) } } if got := e.storedExtra(t); len(got) != 0 { t.Fatalf("a refused import wrote %+v", got) } }) opts := console.WinterOptions{DefinitionsFile: defs, Theme: "main", Maps: map[string]string{"remove-me": ""}, PageURLs: map[string]string{"news": "/news"}} wantLines := []string{ "item 1 (type 'blog-posts'): cmsPage 'blog/post' dropped (CMS pages are not supported)", "item 1 (type 'blog-posts'): dropped (the content types list their records themselves)", "item 2 (type 'acme-post', reference '7'): dropped (the content types list their records themselves)", "item 3 (type 'static-page', reference 'about'): dropped (static and CMS page types are not supported)", "item 4 (type 'cms-page', reference 'news'): copied as an additional address '/news'", "item 5 (type 'remove-me'): type 'remove-me' mapped to ''", "item 5 (type 'remove-me'): dropped (mapped to nothing)", "item 6 (type 'url', url 'javascript://%0aalert(1)%0a//x'): dropped (the url is not a path or an http(s) URL)", "item 7 (type 'url', url '/'): dropped (address '/' is already listed)", } t.Run("dry run writes nothing", func(t *testing.T) { dry := opts dry.DryRun = true report, out, err := run(dry) if err != nil { t.Fatalf("dry run: %v\n%s", err, out) } if report.Kept != 2 || report.Dropped != 6 || report.Remapped != 2 { t.Fatalf("report = %+v\n%s", report, out) } for _, line := range wantLines { if !strings.Contains(out, line) { t.Errorf("output lacks %q:\n%s", line, out) } } if !strings.Contains(out, "Dry run: 2 items kept, 2 remapped, 6 dropped.") || !strings.Contains(out, "Dry run complete (no changes made).") { t.Fatalf("dry run output:\n%s", out) } if got := e.storedExtra(t); len(got) != 0 { t.Fatalf("the dry run wrote %+v", got) } }) t.Run("copies, refuses a second copy, replaces with --force", func(t *testing.T) { report, out, err := run(opts) if err != nil { t.Fatalf("import: %v\n%s", err, out) } if got := extraURLs(e.storedExtra(t)); report.Kept != 2 || got != "/ /news" { t.Fatalf("copied = %q (report %+v)", got, report) } if !strings.Contains(out, "Definition copied: 2 items kept, 2 remapped, 6 dropped.") || !strings.Contains(out, "theme 'main'") { t.Fatalf("output:\n%s", out) } other := console.WinterOptions{DefinitionsFile: defs, Theme: "other"} if _, out, err := run(other); err == nil || !strings.Contains(err.Error(), "already has 2 additional addresses; rerun with --force") { t.Fatalf("second copy: %v\n%s", err, out) } dry := other dry.DryRun = true if _, out, err := run(dry); err != nil || !strings.Contains(out, "already has 2 additional addresses") { t.Fatalf("dry run over addresses: %v\n%s", err, out) } if got := extraURLs(e.storedExtra(t)); got != "/ /news" { t.Fatalf("a refused copy changed the addresses: %q", got) } other.Force = true if _, out, err := run(other); err != nil { t.Fatalf("--force: %v\n%s", err, out) } if got := extraURLs(e.storedExtra(t)); got != "/other" { t.Fatalf("after --force = %q", got) } }) t.Run("data held as JSON or null", func(t *testing.T) { raw := writeFile(t, dir, "raw.jsonl", `{"id":5,"theme":"t","data":[{"type":"url","url":"/raw"}]}`) if _, out, err := run(console.WinterOptions{DefinitionsFile: raw, Force: true}); err != nil { t.Fatalf("raw JSON data: %v\n%s", err, out) } if got := extraURLs(e.storedExtra(t)); got != "/raw" { t.Fatalf("raw data = %q", got) } null := writeFile(t, dir, "null.jsonl", `{"id":6,"theme":"t","data":null}`) if report, out, err := run(console.WinterOptions{DefinitionsFile: null, Force: true}); err != nil || report.Kept != 0 { t.Fatalf("null data: %+v %v\n%s", report, err, out) } if got := e.storedExtra(t); len(got) != 0 { t.Fatalf("null data stored %+v", got) } }) t.Run("settings", func(t *testing.T) { first := writeFile(t, dir, "settings.json", `{"frontend_url":"https://www.example.test","frontend_default_locale":"pl","enable_priority_tiers":"1","priority_blog":0.7,"unknown":"x"}`) report, out, err := run(console.WinterOptions{SettingsFile: first}) if err != nil || !report.Settings || !strings.Contains(out, "setting 'unknown' ignored (not a sitemap setting of this port)") || !strings.Contains(out, "setting 'priority_blog' ignored (the per-type sitemaps do not use it)") || !strings.Contains(out, "Settings copied.") { t.Fatalf("settings: %+v %v\n%s", report, err, out) } s, err := classes.LoadSettings(e.gdb) if err != nil || models.Text(s.SiteURL) != "https://www.example.test" || models.Text(s.DefaultLocale) != "pl" { t.Fatalf("copied settings = %+v %v", s, err) } second := writeFile(t, dir, "settings2.json", `"{\"frontend_url\":\"https://other.example.test\"}"`) if _, out, err := run(console.WinterOptions{SettingsFile: second}); err != nil { t.Fatalf("second settings: %v\n%s", err, out) } if s, _ = classes.LoadSettings(e.gdb); models.Text(s.SiteURL) != "https://other.example.test" || s.DefaultLocale != nil { t.Fatalf("second settings = %+v", s) } dry := writeFile(t, dir, "settings3.json", `{"frontend_url":"https://dry.example.test"}`) if _, out, err := run(console.WinterOptions{SettingsFile: dry, DryRun: true}); err != nil || !strings.Contains(out, "Dry run: the settings would be copied.") { t.Fatalf("settings dry run: %v\n%s", err, out) } // T-17-15: an unsafe frontend_url stops the import, also as a dry // run, and the row keeps its value. quoted, _ := json.Marshal(map[string]string{"frontend_url": `https://x"/>