test(11.2): cover site_url and site_label and gate the framework, app and site stages
- TestCheckSiteURL (21 accepted and rejected values), TestSiteLabel and TestSiteURLPrecedence (option over site.yaml, label-without-URL and invalid option errors); new TestParseSite rows for blank and two-line labels - TestSiteLink: escaped label, section page and 404 page, exact unset header bytes - TestDocsSiteFlagsInHelp and the --site-label-without-URL error - check-phase11.2.sh --framework, --app and --site
This commit is contained in:
@@ -104,6 +104,38 @@ func TestDocsBuildSiteFlags(t *testing.T) {
|
||||
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-url") {
|
||||
t.Fatalf("docs:build --site-url javascript:alert(1): err = %v, want a --site-url error", err)
|
||||
}
|
||||
|
||||
// The framework's own site.yaml sets no site_url, so a label alone fails.
|
||||
buf.Reset()
|
||||
root, err = bonfire.NewRoot("summer", toolCommands(), &buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", filepath.Join(t.TempDir(), "nolabel"), "--site-label", "x"})
|
||||
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-label needs --site-url") {
|
||||
t.Fatalf("docs:build --site-label x: err = %v, want a --site-label needs --site-url error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocsSiteFlagsInHelp checks that docs:build and docs:serve list the
|
||||
// --site-url and --site-label flags in their help.
|
||||
func TestDocsSiteFlagsInHelp(t *testing.T) {
|
||||
for _, cmd := range []string{"docs:build", "docs:serve"} {
|
||||
var buf bytes.Buffer
|
||||
root, err := bonfire.NewRoot("summer", toolCommands(), &buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root.SetArgs([]string{cmd, "--help"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("%s --help: %v", cmd, err)
|
||||
}
|
||||
for _, want := range []string{"--site-url", "--site-label", "overrides site.yaml site_url", "overrides site.yaml site_label"} {
|
||||
if !strings.Contains(buf.String(), want) {
|
||||
t.Errorf("%s help lacks %q:\n%s", cmd, want, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// requiredPages lists the guide pages the docs must keep. Each content plan
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package docsite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -30,6 +31,9 @@ func TestParseSite(t *testing.T) {
|
||||
{"site_url javascript", valid + "site_url: javascript:alert(1)\n", "site_url must be an http(s) URL with a host or a path starting with a single /"},
|
||||
{"site_url protocol-relative", valid + "site_url: //acme.example\n", "site_url must be an http(s) URL with a host or a path starting with a single /"},
|
||||
{"site_label without site_url", valid + "site_label: Acme\n", "site_label needs site_url"},
|
||||
{"blank site_label", valid + "site_url: /\nsite_label: \" \"\n", "site_label must be one non-empty line"},
|
||||
{"two-line site_label", valid + "site_url: /\nsite_label: \"a\\nb\"\n", "site_label must be one non-empty line"},
|
||||
{"site_url with a newline", valid + "site_url: \"/a\\nb\"\n", "site_url must be an http(s) URL"},
|
||||
} {
|
||||
if _, err := ParseSite([]byte(tc.raw)); err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Errorf("%s: err = %v, want %q", tc.name, err, tc.want)
|
||||
@@ -41,6 +45,9 @@ func TestParseSite(t *testing.T) {
|
||||
t.Errorf("site_url %q: ParseSite = %+v, %v", u, got, err)
|
||||
}
|
||||
}
|
||||
if got, err := ParseSite([]byte(valid + "site_url: /\nsite_label: Acme\n")); err != nil || got.SiteURL != "/" || got.SiteLabel != "Acme" {
|
||||
t.Errorf("site_url with site_label: ParseSite = %+v, %v", got, err)
|
||||
}
|
||||
if strings.Contains(func() string { _, err := ParseSite([]byte(valid + "colour: red\n")); return err.Error() }(), "\n") {
|
||||
t.Error("a decode error must be reported on one line")
|
||||
}
|
||||
@@ -319,3 +326,108 @@ func TestNormalizeDefaults(t *testing.T) {
|
||||
t.Fatalf("normalize(relative) = %+v", o)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckSiteURL pins the site_url policy (T-11.2-09): an http(s) URL
|
||||
// with a host and no user info, or a path starting with exactly one /.
|
||||
func TestCheckSiteURL(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, raw string
|
||||
ok bool
|
||||
}{
|
||||
{"https host", "https://acme.example", true},
|
||||
{"https host slash", "https://acme.example/", true},
|
||||
{"http port path", "http://acme.example:8080/x", true},
|
||||
{"root path", "/", true},
|
||||
{"path", "/home", true},
|
||||
{"empty", "", false},
|
||||
{"javascript", "javascript:alert(1)", false},
|
||||
{"javascript mixed case", "JavaScript:alert(1)", false},
|
||||
{"data", "data:text/html,x", false},
|
||||
{"protocol-relative", "//acme.example", false},
|
||||
{"bare host", "acme.example", false},
|
||||
{"relative path", "docs/x", false},
|
||||
{"scheme without host", "https://", false},
|
||||
{"user info", "https://user:pw@acme.example", false},
|
||||
{"ftp", "ftp://acme.example", false},
|
||||
{"space in path", "/ x", false},
|
||||
{"newline", "https://acme.example/\nx", false},
|
||||
{"leading tab", "\t/home", false},
|
||||
{"backslash host", "/\\acme.example", false},
|
||||
{"mailto opaque", "mailto:docs@acme.example", false},
|
||||
{"control character", "/home\x00", false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := checkSiteURL(tc.raw)
|
||||
if tc.ok && err != nil {
|
||||
t.Fatalf("checkSiteURL(%q) = %v, want accepted", tc.raw, err)
|
||||
}
|
||||
if !tc.ok && !errors.Is(err, errSiteURL) {
|
||||
t.Fatalf("checkSiteURL(%q) = %v, want errSiteURL", tc.raw, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSiteLabel(t *testing.T) {
|
||||
for _, tc := range []struct{ url, label, want string }{
|
||||
{"/", " acme.example ", "acme.example"},
|
||||
{"https://acme.example/docs", "Acme", "Acme"},
|
||||
{"https://acme.example/docs", "", "acme.example"},
|
||||
{"https://acme.example/docs", " ", "acme.example"},
|
||||
{"http://acme.example:8080/", "", "acme.example:8080"},
|
||||
{"/", "", "Home"},
|
||||
{"/home", "", "Home"},
|
||||
} {
|
||||
if got := siteLabel(tc.url, tc.label); got != tc.want {
|
||||
t.Errorf("siteLabel(%q, %q) = %q, want %q", tc.url, tc.label, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSiteURLPrecedence checks that the --site-url and --site-label options
|
||||
// override site.yaml the way --base-url overrides base_url.
|
||||
func TestSiteURLPrecedence(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
yaml string
|
||||
opts Options
|
||||
wantURL string
|
||||
wantLabel string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "nothing set"},
|
||||
{name: "yaml url, derived label", yaml: "site_url: https://acme.example:8443/x\n", wantURL: "https://acme.example:8443/x", wantLabel: "acme.example:8443"},
|
||||
{name: "yaml url and label", yaml: "site_url: /\nsite_label: Yaml\n", wantURL: "/", wantLabel: "Yaml"},
|
||||
{name: "option url wins", yaml: "site_url: https://yaml.example\n", opts: Options{SiteURL: "/"}, wantURL: "/", wantLabel: "Home"},
|
||||
{name: "option label wins", yaml: "site_url: /\nsite_label: Yaml\n", opts: Options{SiteLabel: "Opt"}, wantURL: "/", wantLabel: "Opt"},
|
||||
{name: "option url keeps yaml label", yaml: "site_url: /\nsite_label: Yaml\n", opts: Options{SiteURL: "https://opt.example"}, wantURL: "https://opt.example", wantLabel: "Yaml"},
|
||||
{name: "options only", opts: Options{SiteURL: "https://opt.example/", SiteLabel: "Opt"}, wantURL: "https://opt.example/", wantLabel: "Opt"},
|
||||
{name: "option label without url", opts: Options{SiteLabel: "x"}, wantErr: "docsite: --site-label needs --site-url or site.yaml site_url"},
|
||||
{name: "invalid option url", yaml: "site_url: /\n", opts: Options{SiteURL: "javascript:alert(1)"}, wantErr: "docsite: --site-url: must be an http(s) URL"},
|
||||
{name: "protocol-relative option url", opts: Options{SiteURL: "//acme.example"}, wantErr: "docsite: --site-url: "},
|
||||
{name: "two-line option label", opts: Options{SiteURL: "/", SiteLabel: "a\nb"}, wantErr: "docsite: --site-label: must be one non-empty line"},
|
||||
{name: "blank option label", opts: Options{SiteURL: "/", SiteLabel: " "}, wantErr: "docsite: --site-label: must be one non-empty line"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := themeTree(t)
|
||||
if err := os.WriteFile(filepath.Join(root, "docs", "site.yaml"), []byte(fixtureSite+tc.yaml), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
opts := tc.opts
|
||||
opts.Root, opts.Commands, opts.Out = root, fixtureCommands, filepath.Join(t.TempDir(), "site")
|
||||
s, problems, err := load(opts)
|
||||
if tc.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("load error = %v, want %q", err, tc.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || len(problems) > 0 {
|
||||
t.Fatalf("load: %v %q", err, problemLines(problems))
|
||||
}
|
||||
if s.siteURL != tc.wantURL || s.siteLabel != tc.wantLabel {
|
||||
t.Fatalf("site link = %q %q, want %q %q", s.siteURL, s.siteLabel, tc.wantURL, tc.wantLabel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,35 +77,44 @@ func assertCSPSafe(t *testing.T, name, html string) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSiteLink checks the optional link back to the main site on a page
|
||||
// and on the 404 page: absent without a site URL, labelled explicitly, by
|
||||
// the URL host, or Home.
|
||||
// TestSiteLink checks the optional link back to the main site on the home
|
||||
// page, a section page and the 404 page: absent without a site URL (the
|
||||
// header bytes unchanged), labelled explicitly, by the URL host, or Home,
|
||||
// and always HTML-escaped.
|
||||
func TestSiteLink(t *testing.T) {
|
||||
// unsetHeader is the wordmark's closing tag followed directly by the
|
||||
// spacer: the header bytes from before site_url existed.
|
||||
const unsetHeader = `<span class="wordmark-cms">CMS</span></span></a>` + "\n" + `<div class="header-spacer"></div>`
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
opts Options
|
||||
want []string
|
||||
deny []string
|
||||
}{
|
||||
{"unset", Options{}, nil},
|
||||
{"explicit label", Options{SiteURL: "/", SiteLabel: "acme.example"}, []string{`<a class="site-link" href="/"`, `<span class="site-link-label">acme.example</span>`}},
|
||||
{"host label", Options{SiteURL: "https://acme.example/docs"}, []string{`<a class="site-link" href="https://acme.example/docs"`, `<span class="site-link-label">acme.example</span>`}},
|
||||
{"home label", Options{SiteURL: "/"}, []string{`<a class="site-link" href="/"`, `<span class="site-link-label">Home</span>`}},
|
||||
{"unset", Options{}, []string{unsetHeader}, []string{"site-link"}},
|
||||
{"explicit label", Options{SiteURL: "/", SiteLabel: "acme.example"}, []string{`<a class="site-link" href="/" aria-label="acme.example">`, `<span class="site-link-label">acme.example</span>`}, []string{unsetHeader}},
|
||||
{"host label", Options{SiteURL: "https://acme.example/docs"}, []string{`<a class="site-link" href="https://acme.example/docs"`, `<span class="site-link-label">acme.example</span>`}, nil},
|
||||
{"host and port label", Options{SiteURL: "http://acme.example:8080/"}, []string{`<a class="site-link" href="http://acme.example:8080/"`, `<span class="site-link-label">acme.example:8080</span>`}, nil},
|
||||
{"home label", Options{SiteURL: "/"}, []string{`<a class="site-link" href="/"`, `<span class="site-link-label">Home</span>`}, nil},
|
||||
{"escaped label", Options{SiteURL: "/", SiteLabel: "<b>&"}, []string{`aria-label="<b>&"`, `<span class="site-link-label"><b>&</span>`}, []string{"<b>&"}},
|
||||
} {
|
||||
opts := tc.opts
|
||||
opts.Root, opts.Commands, opts.Out = themeTree(t), fixtureCommands, filepath.Join(t.TempDir(), "site")
|
||||
if _, problems, err := Build(opts); err != nil || len(problems) > 0 {
|
||||
t.Fatalf("%s: Build: %v %q", tc.name, err, problemLines(problems))
|
||||
}
|
||||
for _, name := range []string{"index.html", "404.html"} {
|
||||
for _, name := range []string{"index.html", "setup/start.html", "404.html"} {
|
||||
html := readOut(t, opts.Out, name)
|
||||
if tc.want == nil && strings.Contains(html, "site-link") {
|
||||
t.Errorf("%s: %s has a site link", tc.name, name)
|
||||
}
|
||||
for _, w := range tc.want {
|
||||
if !strings.Contains(html, w) {
|
||||
t.Errorf("%s: %s lacks %s", tc.name, name, w)
|
||||
}
|
||||
}
|
||||
for _, d := range tc.deny {
|
||||
if strings.Contains(html, d) {
|
||||
t.Errorf("%s: %s contains %s", tc.name, name, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,18 @@ ROOT="${PHASE11_2_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
APP="${PHASE11_2_APP:-$ROOT/../sm-summercmsio-app}"
|
||||
PLUG="$APP/plugins/golem15/summercms"
|
||||
SITE="$APP/vue-summercmsio-app"
|
||||
APP_PKG="git.golem15.com/golem15/sm-summercmsio-app"
|
||||
# Statement coverage floors (SC5).
|
||||
PLUGIN_COVERAGE_MIN=90.0
|
||||
DOCSITE_COVERAGE_MIN=85.0
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage:
|
||||
check-phase11.2.sh --framework
|
||||
check-phase11.2.sh --plugin
|
||||
check-phase11.2.sh --app
|
||||
check-phase11.2.sh --site
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
@@ -125,6 +130,20 @@ coverage_at_least() {
|
||||
awk -v t="$total" -v m="$min" 'BEGIN { exit !(t + 0 >= m + 0) }' || refuse "coverage $total% is below $min% in $dir"
|
||||
}
|
||||
|
||||
# Framework tests of the D-41/D-46 site link, plus the docs checker.
|
||||
DOCSITE_TESTS=(TestParseSite TestCheckSiteURL TestSiteLabel TestSiteURLPrecedence TestSiteLink)
|
||||
SUMMER_TESTS=(TestDocsTree TestDocsBuildRealTree TestDocsBuildSiteFlags TestDocsSiteFlagsInHelp)
|
||||
|
||||
run_framework() {
|
||||
(cd "$ROOT" && go vet ./...)
|
||||
named_tests "$ROOT" ./internal/docsite "${DOCSITE_TESTS[@]}" || refuse "framework: internal/docsite named tests"
|
||||
named_tests "$ROOT" ./cmd/summer "${SUMMER_TESTS[@]}" || refuse "framework: cmd/summer named tests"
|
||||
coverage_at_least "$ROOT" "$DOCSITE_COVERAGE_MIN" ./internal/docsite
|
||||
"$ROOT/scripts/check-phase11.1.sh" --docs
|
||||
"$ROOT/scripts/check-phase11.1.sh" --forbidden
|
||||
echo "phase11.2 framework passed"
|
||||
}
|
||||
|
||||
PLUGIN_TESTS=(TestStaticSmoke TestStaticSite TestStaticDocs TestStaticConditionalAndRange TestStaticNoBlockingHeaders
|
||||
TestStaticRedirectLocations TestStaticMissing404Page TestNewHandlersMissingIndex TestContentType TestSiteImmutable
|
||||
TestRoutesAssemble TestRoutesFailClosed TestRoutesCoexistWithAdminPatterns TestPluginIdentity TestPluginEmbeddedTree
|
||||
@@ -138,7 +157,46 @@ run_plugin() {
|
||||
echo "phase11.2 plugin passed"
|
||||
}
|
||||
|
||||
# The D-40 terminal-check helpers (the gated TestTerminalCommands runs in
|
||||
# the terminal stage).
|
||||
APP_TESTS=(TestLoadTerminal TestTerminalScript TestTerminalEnv)
|
||||
|
||||
run_app() {
|
||||
local pkgs
|
||||
need_dir "$APP"
|
||||
go -C "$APP" vet ./...
|
||||
pkgs="$(go -C "$APP" list ./...)"
|
||||
[ "$pkgs" = "$APP_PKG" ] || refuse "app: go list ./... printed '$pkgs', want only $APP_PKG"
|
||||
(unset SUMMERCMS_TERMINAL_CHECK && named_tests "$APP" . "${APP_TESTS[@]}") || refuse "app: named tests"
|
||||
echo "phase11.2 app passed"
|
||||
}
|
||||
|
||||
# run_site installs from the lockfile, generates the static site and runs
|
||||
# the node:test suites (output, terminal and scroll-spy tests).
|
||||
run_site() {
|
||||
local log
|
||||
need_dir "$SITE"
|
||||
command -v pnpm >/dev/null || refuse "site: pnpm not found"
|
||||
pnpm -C "$SITE" install --frozen-lockfile
|
||||
pnpm -C "$SITE" run generate
|
||||
log="$(mktemp)"
|
||||
SCRATCH+=("$log")
|
||||
pnpm -C "$SITE" test >"$log" 2>&1 || {
|
||||
cat "$log" >&2
|
||||
refuse "site: pnpm test failed"
|
||||
}
|
||||
grep -E '^# (tests|pass|fail|skipped|todo|cancelled) ' "$log"
|
||||
grep -qx '# fail 0' "$log" || refuse "site: no '# fail 0' line"
|
||||
grep -qE '^# pass [1-9][0-9]*$' "$log" || refuse "site: no passing tests"
|
||||
grep -qx '# skipped 0' "$log" || refuse "site: skipped tests"
|
||||
grep -qx '# todo 0' "$log" || refuse "site: todo tests"
|
||||
echo "phase11.2 site passed"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--framework) run_framework ;;
|
||||
--plugin) run_plugin ;;
|
||||
--app) run_app ;;
|
||||
--site) run_site ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user