Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 36m ago 32290114
history blame raw 5.3 KiB
1package web
2
3import (
4 "strings"
5 "testing"
6)
7
8// A relative link in a file has to be resolved the way a reader expects: against
9// the directory the file lives in, not against the URL the page happens to be
10// served from. The overview page and the code browser serve the same README from
11// different addresses, so getting this wrong breaks both.
12func TestDocHrefResolvesAgainstTheDocument(t *testing.T) {
13 t.Parallel()
14 root := treeDoc{Repo: "demo", Ref: "main", Path: "README.md"}
15 guide := treeDoc{Repo: "demo", Ref: "main", Path: "docs/guide.md"}
16 feature := treeDoc{Repo: "demo", Ref: "feat/tree", Path: "README.md"}
17 loose := treeDoc{Repo: "demo"}
18
19 cases := []struct {
20 name string
21 doc treeDoc
22 dest string
23 image bool
24 want string
25 ok bool
26 }{
27 {
28 name: "markdown opens rendered", doc: root, dest: "docs/guide.md",
29 want: "/demo/blob/main/docs/guide.md?preview=1", ok: true,
30 },
31 {
32 name: "fragment follows the preview flag", doc: root, dest: "docs/guide.md#using-it",
33 want: "/demo/blob/main/docs/guide.md?preview=1#using-it", ok: true,
34 },
35 {
36 name: "other files open as source", doc: root, dest: "LICENSE",
37 want: "/demo/blob/main/LICENSE", ok: true,
38 },
39 {
40 name: "nested non-markdown", doc: root, dest: ".vole/workflows/verify.yaml",
41 want: "/demo/blob/main/.vole/workflows/verify.yaml", ok: true,
42 },
43 {
44 name: "a directory opens the tree", doc: root, dest: "docs/",
45 want: "/demo/tree/main/docs", ok: true,
46 },
47 {
48 name: "the document's own directory", doc: guide, dest: "./",
49 want: "/demo/tree/main/docs", ok: true,
50 },
51 {
52 name: "the repository root", doc: root, dest: "./",
53 want: "/demo/code?ref=main", ok: true,
54 },
55 {
56 name: "parent climbs one level", doc: guide, dest: "../LICENSE",
57 want: "/demo/blob/main/LICENSE", ok: true,
58 },
59 {
60 name: "climbing stops at the root", doc: guide, dest: "../../../LICENSE",
61 want: "/demo/blob/main/LICENSE", ok: true,
62 },
63 {
64 name: "a sibling from a subdirectory", doc: guide, dest: "sibling.md",
65 want: "/demo/blob/main/docs/sibling.md?preview=1", ok: true,
66 },
67 {
68 name: "images point at raw", doc: root, dest: "img/shot.png", image: true,
69 want: "/demo/raw/main/img/shot.png", ok: true,
70 },
71 {
72 name: "images resolve against their own file", doc: guide, dest: "shot.png", image: true,
73 want: "/demo/raw/main/docs/shot.png", ok: true,
74 },
75 {
76 name: "a ref with a slash stays one segment", doc: feature, dest: "LICENSE",
77 want: "/demo/blob/feat%2Ftree/LICENSE", ok: true,
78 },
79
80 // Everything below already names somewhere, so the document does not get
81 // to reinterpret it.
82 {name: "scheme", doc: root, dest: "https://example.com/x"},
83 {name: "mailto", doc: root, dest: "mailto:someone@example.com"},
84 {name: "protocol relative", doc: root, dest: "//example.com/x"},
85 {name: "site absolute", doc: root, dest: "/demo/issues/01M5JR2JK29KKMAA1T9MDNTPE1"},
86 {name: "same document anchor", doc: root, dest: "#why-vole"},
87 {name: "same document query", doc: root, dest: "?preview=1"},
88 {name: "empty", doc: root, dest: ""},
89 {name: "not in a tree", doc: loose, dest: "docs/guide.md"},
90 }
91
92 for _, tc := range cases {
93 t.Run(tc.name, func(t *testing.T) {
94 got, ok := docHref(tc.doc, tc.dest, tc.image)
95 if ok != tc.ok {
96 t.Fatalf("docHref(%q) ok = %v, want %v", tc.dest, ok, tc.ok)
97 }
98 if got != tc.want {
99 t.Fatalf("docHref(%q) = %q, want %q", tc.dest, got, tc.want)
100 }
101 })
102 }
103}
104
105// A body may name an entity by its durable ULID or by its `#42` alias. Both link,
106// and the alias goes to the kind-agnostic route because only the server can say
107// whether a number names an issue or a change request.
108func TestCrossReferencesLinkBothForms(t *testing.T) {
109 t.Parallel()
110 const id = "01M5JR2JK29KKMAA1T9MDNTPBP"
111 cases := []struct {
112 name string
113 in string
114 want string
115 }{
116 {
117 name: "ulid",
118 in: "fixed in #" + id + "\n",
119 want: "fixed in [#" + id + "](/demo/issues/" + id + ")\n",
120 },
121 {
122 name: "number",
123 in: "see #42 for the reason\n",
124 want: "see [#42](/demo/n/42) for the reason\n",
125 },
126 {
127 name: "number at end of line",
128 in: "closes #7",
129 want: "closes [#7](/demo/n/7)",
130 },
131 {
132 name: "inside a fence is literal",
133 in: "```\n#42\n```\n",
134 want: "```\n#42\n```\n",
135 },
136 {
137 name: "a heading is not a reference",
138 in: "# 42 things\n",
139 want: "# 42 things\n",
140 },
141 }
142 for _, tc := range cases {
143 t.Run(tc.name, func(t *testing.T) {
144 if got := linkCrossReferences("demo", tc.in); got != tc.want {
145 t.Fatalf("linkCrossReferences(%q) = %q, want %q", tc.in, got, tc.want)
146 }
147 })
148 }
149}
150
151// The instance introduction has no repository, so a `#42` in it must stay the
152// text it was written as. Linking it would produce //n/42, which is a page
153// that cannot exist, and it is the kind of broken link only a reader notices.
154func TestInstanceIntroIsNotLinkedIntoARepository(t *testing.T) {
155 t.Parallel()
156 w := newTestWeb(t)
157 got := string(w.renderInstanceMarkdown("# Welcome\n\nSee #42 and @keenan.\n\n<script>alert(1)</script>\n"))
158 for _, want := range []string{"<h1>Welcome</h1>", "#42", "@keenan"} {
159 if !strings.Contains(got, want) {
160 t.Fatalf("intro is missing %q:\n%s", want, got)
161 }
162 }
163 for _, unwanted := range []string{"<script", "//"} {
164 if strings.Contains(got, unwanted) {
165 t.Fatalf("intro contains %q:\n%s", unwanted, got)
166 }
167 }
168}
169