~bigbes/sr-ht-dolt

ref: 11c622f38dc9e15a1c23880fca39e2f6b1d8b1b1 sr-ht-dolt/web/handlers_ready_test.go -rw-r--r-- 12.7 KiB
11c622f3 — Eugene Blikh web: stop printing browse errors to the reader 5 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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package web

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

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

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

// The cross-database ready page. What matters here is not the projection — that
// is beads' and is tested there — but the two things only this layer can get
// wrong: which databases a caller is shown, and how many stores one request is
// allowed to open.

// readyStore builds a fake session for one tracker: a beads schema, a head
// commit four minutes old, and the given issue rows.
func readyStore(head string, issues [][]string) *fakeSession {
	return &fakeSession{
		branches: []browse.Branch{{Name: "main", Head: head}},
		tables:   beadsTables(),
		commits: []browse.CommitInfo{{
			Hash: head, Author: "Eugene Blikh", Date: testNow.Add(-4 * time.Minute),
			Message: "bd: create (auto-commit)",
		}},
		rowsByTable: map[string]*browse.RowPage{
			"issues": {
				Columns: []string{"id", "title", "status", "priority", "issue_type", "assignee", "is_blocked"},
				Rows:    issues,
				Total:   len(issues),
			},
			"dependencies": {
				Columns: []string{"id", "issue_id", "depends_on_issue_id", "type"},
				Total:   0,
			},
		},
	}
}

// addTracker registers a database with the metadata store and wires its store
// path to a fake session, the way the production pair (repo.Path →
// BrowseOpener) is wired.
func addTracker(h *harness, owner string, ownerID int, name string, vis core.Visibility, sess *fakeSession) *core.Repo {
	repo := h.store.add(&core.Repo{
		Name: name, OwnerID: ownerID, OwnerName: owner,
		Path: "/var/lib/dolt/~" + owner + "/" + name, Visibility: vis,
	})
	if h.browse.byPath == nil {
		h.browse.byPath = map[string]*fakeSession{}
	}
	h.browse.byPath[repo.Path] = sess
	return repo
}

// readyHarness is two public trackers: alpha with two ready issues among four,
// beta with one.
func readyHarness(t *testing.T) (*harness, *fakeSession, *fakeSession) {
	t.Helper()
	h := newHarness(t)
	alpha := readyStore("h-alpha", [][]string{
		{"a-1", "Ready, top priority", "open", "0", "task", "alice", "0"},
		{"a-2", "Ready, lower", "open", "2", "bug", "bob", "0"},
		{"a-3", "Under way", "in_progress", "0", "task", "alice", "0"},
		{"a-4", "Waiting on something", "open", "0", "task", "bob", "1"},
	})
	beta := readyStore("h-beta", [][]string{
		{"b-1", "The one ready thing", "open", "1", "task", "carol", "0"},
		{"b-2", "Finished", "closed", "0", "task", "carol", "0"},
	})
	addTracker(h, "alice", 1, "alpha", core.VisibilityPublic, alpha)
	addTracker(h, "bob", 2, "beta", core.VisibilityPublic, beta)
	return h, alpha, beta
}

func TestReadyPageRendersGroupsInOrder(t *testing.T) {
	pinClock(t, testNow)
	h, _, _ := readyHarness(t)

	rec := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "body=%s", rec.Body.String())
	body := rec.Body.String()

	// Both databases, the busier one first, each with its own count.
	assert.Contains(t, body, "~alice/alpha")
	assert.Contains(t, body, "~bob/beta")
	assert.Less(t, strings.Index(body, "~alice/alpha"), strings.Index(body, "~bob/beta"),
		"groups are ordered by ready count desc")
	assert.Contains(t, body, "2 ready")
	assert.Contains(t, body, "1 ready")
	assert.Contains(t, body, "3 ready issues")

	// The ready ones, linked into their own database's board.
	assert.Contains(t, body, `href="/~alice/alpha/view/beads?issue=a-1"`)
	assert.Contains(t, body, `href="/~alice/alpha/view/beads?issue=a-2"`)
	assert.Contains(t, body, `href="/~bob/beta/view/beads?issue=b-1"`)
	// …and only those: in-progress, blocked and closed issues are not ready.
	assert.NotContains(t, body, "issue=a-3")
	assert.NotContains(t, body, "issue=a-4")
	assert.NotContains(t, body, "issue=b-2")

	// Each group carries its own database's freshness, so "ready" from a store
	// that stopped receiving pushes is not claimed silently.
	assert.Contains(t, body, `class="beads-freshness"`)
	assert.Contains(t, body, "main · last commit")
	assert.Contains(t, body, "4 minutes ago")
	assert.Contains(t, body, `href="/~alice/alpha/commit/h-alpha"`)
	assert.Contains(t, body, `href="/~bob/beta/commit/h-beta"`)
}

// A PRIVATE tracker the caller may not browse is absent. Not a 403, not a
// counted-but-unnamed group: nothing on the page may hint that it exists.
func TestReadyPageHidesADatabaseTheCallerMayNotBrowse(t *testing.T) {
	pinClock(t, testNow)
	h, _, _ := readyHarness(t)
	secret := readyStore("h-secret", [][]string{
		{"s-1", "The secret thing", "open", "0", "task", "dave", "0"},
	})
	addTracker(h, "dave", 9, "secrets", core.VisibilityPrivate, secret)

	rec := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "a hidden database is not a refusal")
	body := rec.Body.String()

	assert.NotContains(t, body, "secrets")
	assert.NotContains(t, body, "s-1")
	assert.NotContains(t, body, "The secret thing")
	assert.NotContains(t, body, "dave")
	assert.NotContains(t, body, "h-secret")
	assert.Zero(t, secret.opens, "a database the caller may not browse is never opened")
	// The visible ones are unaffected.
	assert.Contains(t, body, "~alice/alpha")
	assert.Contains(t, body, "3 ready issues")

	// Its owner sees it, which is what makes the absence above a visibility rule
	// and not a broken page.
	owner := h.do("GET", "/ready", testCaller(9, "dave"), nil)
	require.Equal(t, http.StatusOK, owner.Code)
	assert.Contains(t, owner.Body.String(), "~dave/secrets")
	assert.Contains(t, owner.Body.String(), "s-1")
}

// The head-hash gate: a second request whose heads have not moved reads no rows
// at all. Counted on the fake, never timed.
func TestReadyPageHeadHashGateReadsNothingTwice(t *testing.T) {
	pinClock(t, testNow)
	h, alpha, beta := readyHarness(t)

	first := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, first.Code)
	reads := alpha.rowReads
	require.Greater(t, reads, 0, "the first request must read rows")

	second := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, second.Code)

	assert.Equal(t, reads, alpha.rowReads, "an unmoved head must cost no row reads")
	assert.Equal(t, 1, alpha.tableReads, "nor a second table listing")
	assert.Equal(t, 1, alpha.logReads, "nor a second log read")
	assert.Equal(t, 1, beta.tableReads, "the sibling database is gated too")
	// Both stores are still opened and their branches listed — that is what the
	// gate is gated on, and it is the cheap half.
	assert.Equal(t, 2, alpha.opens)
	// The page is the same page, cache or not.
	assert.Equal(t, first.Body.String(), second.Body.String())
}

// The TTL: past it the projection is read again even though nothing moved, so a
// cache can never be the reason a reader sees yesterday's answer.
func TestReadyPageTTLExpiryForcesAReread(t *testing.T) {
	pinClock(t, testNow)
	h, alpha, _ := readyHarness(t)

	require.Equal(t, http.StatusOK, h.do("GET", "/ready", nil, nil).Code)
	reads := alpha.rowReads

	// One tick short of the TTL: still cached.
	pinClock(t, testNow.Add(beads.ReadyCacheTTL-time.Second))
	require.Equal(t, http.StatusOK, h.do("GET", "/ready", nil, nil).Code)
	assert.Equal(t, reads, alpha.rowReads, "inside the TTL the projection stands")

	// Past it: read again.
	pinClock(t, testNow.Add(beads.ReadyCacheTTL+time.Second))
	require.Equal(t, http.StatusOK, h.do("GET", "/ready", nil, nil).Code)
	assert.Greater(t, alpha.rowReads, reads, "past the TTL the projection is re-read")
}

// The ceiling: more candidate databases than beads.ReadyMaxDatabases and the
// page says so. A silent cap reads as "that is everything".
func TestReadyPageCeilingSaysItWasCapped(t *testing.T) {
	pinClock(t, testNow)
	h := newHarness(t)
	var overflow []*fakeSession
	for i := 1; i <= beads.ReadyMaxDatabases+2; i++ {
		sess := readyStore(fmt.Sprintf("h%02d", i), [][]string{
			{fmt.Sprintf("t%02d-1", i), "Ready", "open", "1", "task", "alice", "0"},
		})
		addTracker(h, "alice", 1, fmt.Sprintf("db%02d", i), core.VisibilityPublic, sess)
		if i > beads.ReadyMaxDatabases {
			overflow = append(overflow, sess)
		}
	}

	rec := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code)
	body := rec.Body.String()

	assert.Contains(t, body, fmt.Sprintf("only the first %d were read", beads.ReadyMaxDatabases))
	assert.Contains(t, body, fmt.Sprintf("%d ready issues", beads.ReadyMaxDatabases))
	for _, sess := range overflow {
		assert.Zero(t, sess.opens, "a database past the ceiling is never opened")
	}
}

// A database that cannot be opened costs itself only: the rest of the page
// renders, and the browse error reaches the log and not the reader.
func TestReadyPageSurvivesADatabaseThatCannotBeOpened(t *testing.T) {
	pinClock(t, testNow)
	h, _, _ := readyHarness(t)
	broken := h.store.add(&core.Repo{
		Name: "broken", OwnerID: 4, OwnerName: "erin",
		Path: "/var/lib/dolt/~erin/broken", Visibility: core.VisibilityPublic,
	})
	h.browse.errByPath = map[string]error{
		broken.Path: errors.New("browse: open /var/lib/dolt/~erin/broken: manifest is corrupt"),
	}

	rec := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code)
	body := rec.Body.String()

	// The rest of the page is the page.
	assert.Contains(t, body, "~alice/alpha")
	assert.Contains(t, body, "~bob/beta")
	assert.Contains(t, body, "3 ready issues")
	// The gap is admitted; the error is not printed.
	assert.Contains(t, body, "could not be read")
	assert.NotContains(t, body, "manifest is corrupt")
	assert.NotContains(t, body, "/var/lib/dolt")
}

// A database that is not a beads tracker is skipped silently: no group, no
// note, no mention.
func TestReadyPageSkipsNonBeadsDatabases(t *testing.T) {
	pinClock(t, testNow)
	h, _, _ := readyHarness(t)
	plain := &fakeSession{
		branches: []browse.Branch{{Name: "main", Head: "h-plain"}},
		tables:   []browse.TableInfo{{Name: "measurements", Columns: []browse.ColumnInfo{{Name: "id"}}}},
	}
	addTracker(h, "frank", 5, "sensors", core.VisibilityPublic, plain)

	rec := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code)
	body := rec.Body.String()

	assert.NotContains(t, body, "sensors")
	assert.NotContains(t, body, "could not be read")
	assert.Zero(t, plain.rowReads, "a non-beads database is never read for rows")
	assert.Contains(t, body, "3 ready issues")
}

func TestReadyPageFilters(t *testing.T) {
	pinClock(t, testNow)

	get := func(t *testing.T, target string) string {
		t.Helper()
		h, _, _ := readyHarness(t)
		rec := h.do("GET", target, nil, nil)
		require.Equal(t, http.StatusOK, rec.Code, "body=%s", rec.Body.String())
		return rec.Body.String()
	}

	t.Run("q narrows over id and title", func(t *testing.T) {
		body := get(t, "/ready?q=one+ready+thing")
		assert.Contains(t, body, "issue=b-1")
		assert.NotContains(t, body, "issue=a-1")
		assert.Contains(t, body, "1 ready issue")
	})

	t.Run("assignee narrows", func(t *testing.T) {
		body := get(t, "/ready?assignee=bob")
		assert.Contains(t, body, "issue=a-2")
		assert.NotContains(t, body, "issue=a-1")
		assert.NotContains(t, body, "issue=b-1")
	})

	t.Run("priority narrows", func(t *testing.T) {
		body := get(t, "/ready?priority=0")
		assert.Contains(t, body, "issue=a-1")
		assert.NotContains(t, body, "issue=a-2")
	})

	t.Run("db narrows to one database", func(t *testing.T) {
		body := get(t, "/ready?db=bob%2Fbeta")
		assert.Contains(t, body, "~bob/beta")
		assert.NotContains(t, body, "~alice/alpha")
		assert.Contains(t, body, `href="/ready"`, "and offers a way back to all of them")
	})

	t.Run("a filter matching nothing says so", func(t *testing.T) {
		body := get(t, "/ready?assignee=nobody")
		assert.Contains(t, body, "Nothing ready matches these filters")
	})
}

// An instance with no beads databases at all still answers, and says what it
// found rather than nothing.
func TestReadyPageEmptyInstance(t *testing.T) {
	pinClock(t, testNow)
	h := newHarness(t)

	rec := h.do("GET", "/ready", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code)
	assert.Contains(t, rec.Body.String(), "Nothing is ready to work")
}

// The page is reachable from the dashboard — the only place a reader would look
// for a question that belongs to no single database.
func TestDashboardLinksToTheReadyPage(t *testing.T) {
	h := newHarness(t)

	signedIn := h.do("GET", "/", testCaller(3, "bob"), nil)
	require.Equal(t, http.StatusOK, signedIn.Code)
	assert.Contains(t, signedIn.Body.String(), `href="/ready"`)

	anon := h.do("GET", "/", nil, nil)
	require.Equal(t, http.StatusOK, anon.Code)
	assert.Contains(t, anon.Body.String(), `href="/ready"`)
}

// HEAD is registered for this route like every other read route, and answers
// the same status as the GET.
func TestReadyPageAnswersHead(t *testing.T) {
	pinClock(t, testNow)
	h, _, _ := readyHarness(t)

	rec := h.do("HEAD", "/ready", nil, nil)
	assert.Equal(t, http.StatusOK, rec.Code)
}