Files
go/internal/docs/links_test.go
T
endandClaude Sonnet 5.5 750b7449b0 feat: add docs checker and record templates
docs new creates ADRs and plans from embedded templates; docs check reports
path:line: message problems per docs/docs-format.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 14:46:56 -07:00

118 lines
3.7 KiB
Go

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", "<ftp://example.com/x>", []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\n<a href=\"foo.md\">x</a>", 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)
}
}