package docs import ( "os" "path/filepath" "strings" "testing" ) // TestLinkDestinations covers the section 7 rules through one body link. func TestLinkDestinations(t *testing.T) { tests := []struct{ dest, want string }{ // want "" means valid {"/docs/spec.txt", ""}, {"/docs/spec.txt#frag", ""}, {"/docs/sp%20ace.md", ""}, {"#anchor", ""}, {"mailto:a@example.com", ""}, {"https://example.com/a?b=1#c", ""}, {"foo.md", "relative"}, {"./foo.md", "relative"}, {"../docs/spec.txt", "relative"}, {"/docs/../docs/spec.txt", `".."`}, {"/docs/%2e%2e/docs/spec.txt", `".."`}, {`/docs\spec.txt`, "backslash"}, {`C:\spec.txt`, "backslash"}, {"//example.com/x", "protocol-relative"}, {"/docs/spec.txt?x=1", "queries"}, {"/docs/%zz", "percent-encoding"}, {"/docs/missing.txt", "does not exist"}, {"/Users/me/spec.txt", "does not exist"}, {"/docs", "not a regular file"}, {"/docs/out/f.md", "outside the repository"}, {"file:///etc/passwd", "scheme"}, {"ftp://example.com/x", "scheme"}, {"javascript:alert(1)", "scheme"}, {"https://u:p@example.com/x", "credentials"}, {"https:///nohost", "host"}, {"<>", "empty"}, } for _, tt := range tests { t.Run(tt.dest, func(t *testing.T) { dest := tt.dest if strings.ContainsAny(dest, " ") { dest = "<" + dest + ">" } doc := adrBase + "\nSee [x](" + dest + ").\n" res, err := Check(tree(t, withSupport(map[string]string{aPath: doc}), map[string]string{"docs/out": "@outside"})) must(t, err) var want []string if tt.want != "" { want = []string{aPath + ":35: " + tt.want} } expect(t, res.Problems, want) }) } } // TestLinkForms covers which Markdown constructs are link destinations. func TestLinkForms(t *testing.T) { tests := []struct { name, body string want []string }{ {"image", "![x](foo.md)", []string{":35: relative"}}, {"autolink", "", []string{":35: scheme"}}, {"reference link and definition", "[x][r]\n\n[r]: foo.md", []string{":35: relative", ":37: relative"}}, {"unused definition", "[r]: foo.md", []string{":35: relative"}}, {"link in a list", "* [x](foo.md)", []string{":35: relative"}}, {"bare URL and raw HTML are not checked", "http://u:p@example.com and @user #123 abc1234\n\nx", nil}, {"link in code is not a link", "`[x](foo.md)`\n\n```\n[x](foo.md)\n```", nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { res, err := Check(tree(t, withSupport(map[string]string{aPath: adrBase + "\n" + tt.body + "\n"}), nil)) must(t, err) var want []string for _, w := range tt.want { want = append(want, aPath+w) } expect(t, res.Problems, want) }) } } // TestSpecExamples keeps the minimal examples in docs/docs-format.md valid. func TestSpecExamples(t *testing.T) { spec, err := os.ReadFile("../../docs/docs-format.md") must(t, err) root := t.TempDir() parts := strings.Split(string(spec[strings.Index(string(spec), "## 9."):]), "```markdown\n") if len(parts) != 3 { t.Fatalf("expected two examples, got %d", len(parts)-1) } // The path of each example is the backticked name on the line before its fence. heads := strings.Split(string(spec[strings.Index(string(spec), "## 9."):]), "\n") var paths []string for _, l := range heads { if strings.HasPrefix(l, "`docs/") { paths = append(paths, strings.Trim(l, "`:")) } } if len(paths) != 2 { t.Fatalf("expected two example paths, got %v", paths) } for i, part := range parts[1:] { body, _, _ := strings.Cut(part, "```") full := filepath.Join(root, paths[i]) must(t, os.MkdirAll(filepath.Dir(full), 0o755)) must(t, os.WriteFile(full, []byte(body), 0o644)) } res, err := Check(root) must(t, err) expect(t, res.Problems, nil) if res.Records != 2 { t.Fatalf("records = %d", res.Records) } }