~bigbes/sr-ht-dolt

ref: 12c7ff77f8281cd0ca61177bcf07b9c031a04c97 sr-ht-dolt/web/markdown_test.go -rw-r--r-- 12.1 KiB
12c7ff77 — Eugene Blikh ci: publish this build's own coverage and benchmarks 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package web

import (
	"errors"
	"net/http"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
	"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)

// --- the rendering itself ------------------------------------------------------
//
// These render with no index at all — the nil receiver, which is what a page
// gets when the database listing failed. Everything asserted here is therefore
// about the markdown and about what is allowed to reach the browser, not about
// the links.

// renderMemory is one memory body, rendered by a memoryLinks that knows no
// database.
func renderMemory(src string) string {
	var noLinks *memoryLinks
	return string(noLinks.Body(src))
}

// The shapes the memories are actually written in: a bold leader, code spans, a
// bullet list, a fenced recipe, and hard-wrapped prose that reflows into one
// paragraph rather than keeping the width its author's terminal had.
func TestMemoryBodyRendersMarkdown(t *testing.T) {
	got := renderMemory("**Why:** the networks are declared `external: true` everywhere,\n" +
		"and compose cannot create one.\n" +
		"\n" +
		"- first, `./prepare.sh`\n" +
		"- then `labng push backup`\n" +
		"\n" +
		"```\n" +
		"dolt clone https://dolt.srht.bigb.es/~bigbes/x\n" +
		"```\n")

	assert.Contains(t, got, "<strong>Why:</strong>")
	assert.Contains(t, got, "<code>external: true</code>")
	assert.Contains(t, got, "<ul>")
	assert.Contains(t, got, "<li>first, <code>./prepare.sh</code></li>")
	assert.Contains(t, got, "<pre><code>dolt clone")
	assert.Contains(t, got, "everywhere,\nand compose cannot create one.",
		"a hard-wrapped paragraph reflows: the source's line breaks are not the document's")
	assert.NotContains(t, got, "**", "the asterisks are markup and must not survive as text")
}

// A memory is stored text and is escaped on its way out, exactly as it was when
// the page printed paragraphs. Markup written in one renders as the characters
// somebody typed and never as an element.
func TestMemoryBodyEscapesStoredMarkup(t *testing.T) {
	got := renderMemory("Supersedes the memory that asserted <b>otherwise</b>.\n" +
		"\n" +
		"<script>alert('x')</script>\n")

	assert.NotContains(t, got, "<b>otherwise</b>")
	assert.Contains(t, got, "&lt;b&gt;otherwise&lt;/b&gt;")
	assert.NotContains(t, got, "<script>")
	assert.Contains(t, got, "&lt;script&gt;")
}

// Raw markup is shown rather than dropped, which is what makes a placeholder
// survive: goldmark's safe default omits `<NAME>` as a tag, and the memory then
// says "SRHT__VER" — a different fact, silently.
func TestMemoryBodyKeepsPlaceholdersThatLookLikeTags(t *testing.T) {
	got := renderMemory("Bump SRHT_<NAME>_VER in phoebe-lab/srht/versions.env.\n")

	assert.Contains(t, got, "SRHT_&lt;NAME&gt;_VER")
	assert.NotContains(t, got, "raw HTML omitted")
	assert.NotContains(t, got, "SRHT__VER", "dropping the tag would rewrite the sentence")
}

// A link whose scheme is a script is written with no destination at all, and an
// image is a link rather than a fetch: opening a memory may not make a request
// to a host the reader never chose to contact.
func TestMemoryBodyRefusesDangerousLinksAndRemoteImages(t *testing.T) {
	got := renderMemory("[click](javascript:alert(1)) and ![a pixel](https://tracker.example/p.png)\n")

	assert.NotContains(t, got, "javascript:")
	assert.NotContains(t, got, "<img", "an image reference is not fetched by opening the page")
	assert.Contains(t, got, `<a class="mem-img" href="https://tracker.example/p.png">a pixel</a>`)
}

// A bare URL in the prose is a link the reader can follow.
func TestMemoryBodyLinkifiesBareURLs(t *testing.T) {
	got := renderMemory("Clone from https://dolt.srht.bigb.es/~bigbes/x to check.\n")
	assert.Contains(t, got, `<a href="https://dolt.srht.bigb.es/~bigbes/x">`)
}

// A [[slug]] no database holds is muted and inert — and so is one held by a
// database this reader may not browse, which is the point: the two must be one
// rendering.
func TestMemoryBodyLeavesAnUnresolvedWikilinkInert(t *testing.T) {
	got := renderMemory("Related: [[go-vcs-stamp-dirty]] — the stamp.\n")

	assert.Contains(t, got, `<span class="mem-link-out"`)
	assert.Contains(t, got, ">go-vcs-stamp-dirty</span>")
	assert.NotContains(t, got, "<a", "an unresolved reference is not a link")
	assert.NotContains(t, got, "[[", "the brackets are notation and are consumed")
}

// Bracket notation that is not a reference is left as the text it is: a
// wikilink is a slug, and prose in brackets is prose.
func TestMemoryBodyIgnoresBracketsThatAreNotSlugs(t *testing.T) {
	for _, src := range []string{
		"An aside [[with words in it]] mid-sentence.\n",
		"An unclosed [[reference that never ends.\n",
		"An empty [[]] pair.\n",
	} {
		got := renderMemory(src)
		assert.NotContains(t, got, "mem-link", "%q must not become a reference", src)
		assert.Contains(t, got, "[[", "%q keeps its brackets as text", src)
	}
}

// A reference inside a code span is being shown, not made — the notation is
// what the memory is talking about. Inline parsers do not run inside a code
// span, which is why this is a property of the design rather than a special
// case.
func TestMemoryBodyDoesNotResolveInsideCodeSpans(t *testing.T) {
	got := renderMemory("Link memories with `[[their-name]]` in the body.\n")

	assert.Contains(t, got, "<code>[[their-name]]</code>")
	assert.NotContains(t, got, "mem-link")
}

// --- the links, across databases ----------------------------------------------

// memoryLinkTracker is a tracker as the index reads one: a config naming its
// issue prefix and holding memories. The memories' own bodies matter only for
// the database whose page is rendered; the rest are read for their slugs.
func memoryLinkTracker(head, prefix string, memories ...[2]string) *fakeSession {
	rows := [][]string{{"compact_tier2_days", "30"}, {"issue_prefix", prefix}}
	for _, m := range memories {
		rows = append(rows, []string{"kv.memory." + m[0], m[1]})
	}
	return &fakeSession{
		branches: []browse.Branch{{Name: "main", Head: head}},
		tables:   append(beadsTables(), memoryConfigTable()),
		rowsByTable: map[string]*browse.RowPage{
			"issues":       {Columns: []string{"id", "title", "status"}, Total: 0},
			"dependencies": {Columns: []string{"id", "issue_id", "depends_on_issue_id", "type"}, Total: 0},
			"config":       {Columns: []string{"key", "value"}, Rows: rows, Total: len(rows)},
		},
	}
}

// memoryLinkHarness is the instance these tests read: alice/alpha, whose memory
// page is rendered, a second public tracker (bob/beta) holding a memory alpha's
// prose references, and a PRIVATE one (dave/secrets) holding another. Both alice
// and bob carry a memory under the same slug, which is how the tie is checked.
func memoryLinkHarness(t *testing.T) *harness {
	t.Helper()
	h := newHarness(t)

	body := "See [[gitignore-home]] for the ignore rules, and [[shared-note]] " +
		"for the tie.\n" +
		"\n" +
		"Unknown: [[nowhere-at-all]]. Private: [[secret-note]].\n" +
		"\n" +
		"- a step\n" +
		"  - a nested one, see [[gitignore-home]]\n" +
		"\n" +
		"Filed as beta-46c.2, unlike `beta-nex` which is only quoted.\n" +
		"Superseded by beta-46c.3\nand then reopened.\n"

	addTracker(h, "alice", 1, "alpha", core.VisibilityPublic,
		memoryLinkTracker("h-alpha", "alpha",
			[2]string{"the-page", body},
			[2]string{"shared-note", "alice's copy"}))
	addTracker(h, "bob", 2, "beta", core.VisibilityPublic,
		memoryLinkTracker("h-beta", "beta",
			[2]string{"gitignore-home", "bob's memory"},
			[2]string{"shared-note", "bob's copy"}))
	addTracker(h, "dave", 9, "secrets", core.VisibilityPrivate,
		memoryLinkTracker("h-secret", "secret",
			[2]string{"secret-note", "not for you"}))

	setViews(t, h, &memoryView{})
	return h
}

// The whole point of the notation: a [[slug]] resolves to whichever database
// holds that memory, and a memory in another tracker is as ordinary a target as
// one in this tracker.
func TestMemoryWikilinksResolveAcrossDatabases(t *testing.T) {
	pinClock(t, testNow)
	h := memoryLinkHarness(t)

	rec := h.do("GET", "/~alice/alpha/view/memory?key=the-page", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
	body := rec.Body.String()

	// The sibling database's memory.
	assert.Contains(t, body,
		`<a class="mem-link" href="/~bob/beta/view/memory?key=gitignore-home">gitignore-home</a>`)

	// A slug both trackers hold resolves here: the page's own database is asked
	// first, and a memory mirrored into two trackers is one memory.
	assert.Contains(t, body,
		`<a class="mem-link" href="/~alice/alpha/view/memory?key=shared-note">shared-note</a>`)
	assert.NotContains(t, body, "/~bob/beta/view/memory?key=shared-note")

	// A slug nobody holds, and a slug held by a database this caller may not
	// browse, are the same rendering. The second is the load-bearing one: a link,
	// a distinct class or a different tooltip would each publish the existence of
	// a private database.
	assert.Contains(t, body, `<span class="mem-link-out" title="No memory with this slug in a tracker you can browse.">nowhere-at-all</span>`)
	assert.Contains(t, body, `<span class="mem-link-out" title="No memory with this slug in a tracker you can browse.">secret-note</span>`)
	assert.NotContains(t, body, "/~dave/secrets/")

	// And inside a nested list item, whose lines the parser hands back with a
	// synthesised indent: an offset into such a line is not an offset into the
	// source, and a reference built from one would carry the wrong slug.
	assert.Contains(t, body,
		`a nested one, see <a class="mem-link" href="/~bob/beta/view/memory?key=gitignore-home">gitignore-home</a>`)
}

// The prose in a memory names issues too, and it reaches the same index the
// detail pane's ids do — one read of one table per database answers both.
func TestMemoryProseLinksIssueIDs(t *testing.T) {
	pinClock(t, testNow)
	h := memoryLinkHarness(t)

	rec := h.do("GET", "/~alice/alpha/view/memory?key=the-page", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
	body := rec.Body.String()

	assert.Contains(t, body, `<a href="/~bob/beta/view/beads?issue=beta-46c.2">beta-46c.2</a>`)
	// An id inside a code span is being quoted, not cited.
	assert.Contains(t, body, "<code>beta-nex</code>")
	assert.NotContains(t, body, `issue=beta-nex`)

	// An id that ends a line keeps the line break that followed it. Without it
	// the next line's first word is glued to the id — the link would read
	// "beta-46c.3and then reopened".
	assert.Contains(t, body,
		`<a href="/~bob/beta/view/beads?issue=beta-46c.3">beta-46c.3</a>`+"\nand then reopened.")
}

// The index is built once per request and cached per database on the view, the
// same bound the board's link index has. A second request with unmoved heads
// reads no config row for the databases it only consults.
func TestMemoryLinkIndexIsCachedPerDatabase(t *testing.T) {
	pinClock(t, testNow)
	h := memoryLinkHarness(t)

	first := h.do("GET", "/~alice/alpha/view/memory", nil, nil)
	require.Equal(t, http.StatusOK, first.Code)
	beta := h.browse.byPath["/var/lib/dolt/~bob/beta"]
	require.NotNil(t, beta)
	require.Greater(t, beta.rowReads, 0, "the first render must read the sibling's config")
	reads := beta.rowReads

	second := h.do("GET", "/~alice/alpha/view/memory", nil, nil)
	require.Equal(t, http.StatusOK, second.Code)
	assert.Equal(t, reads, beta.rowReads, "an unmoved head reads no row the second time")
	assert.Contains(t, second.Body.String(), "gitignore-home",
		"and the cached index still resolves the reference")
}

// A memory page renders whether or not the index could be built: the links are
// decoration on top of an answer.
func TestMemoryRendersWithoutAnIndex(t *testing.T) {
	pinClock(t, testNow)
	h := memoryLinkHarness(t)
	h.store.listErr = errors.New("db: list repositories: connection refused")

	rec := h.do("GET", "/~alice/alpha/view/memory?key=the-page", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
	body := rec.Body.String()

	assert.Contains(t, body, "mem-link-out", "every reference is left unresolved")
	assert.NotContains(t, body, `class="mem-link"`)
	assert.True(t, strings.Contains(body, "the-page"), "and the memory itself is still shown")
}