v0.32.0 0617278

scheme_handler_assets_p1_test.go RawHistoryFolder

1package cef
2
3// P1 characterization for Plan 05 (internal systemview startup).
4//
5// These tests lock the CURRENT asset behavior before any caching work:
6// every WASM request fully re-reads and re-decompresses (P1.1), and the
7// response-class map (P1.2) records exactly where no-store applies.
8// Header expectations for served 200s follow the P3.3 policy (unversioned
9// successes explicitly noncacheable, immutable only for verified
10// versions); error pages keep their headerless shape throughout.
11
12import (
13	"io/fs"
14	"net/http"
15	"sync"
16	"testing"
17	"testing/fstest"
18
19	purecef "github.com/bnema/purego-cef/cef"
20	"github.com/stretchr/testify/require"
21
22	"github.com/bnema/dumber/assets"
23)
24
25// decodeCountingFS counts full asset reads: with no cache, one served WASM
26// request equals one decompression. The counter lives outside production
27// code; P2 replaces call-counting with a real decode-once assertion.
28type decodeCountingFS struct {
29	fstest.MapFS
30	mu    sync.Mutex
31	reads map[string]int
32}
33
34func (d *decodeCountingFS) record(name string) {
35	d.mu.Lock()
36	defer d.mu.Unlock()
37	if d.reads == nil {
38		d.reads = map[string]int{}
39	}
40	d.reads[name]++
41}
42
43func (d *decodeCountingFS) count(name string) int {
44	d.mu.Lock()
45	defer d.mu.Unlock()
46	return d.reads[name]
47}
48
49func countingWASMFS(t *testing.T, wasm []byte) *decodeCountingFS {
50	t.Helper()
51	return &decodeCountingFS{
52		MapFS: fstest.MapFS{
53			"systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, wasm)},
54		},
55	}
56}
57
58func TestAssetP1_RepeatedReadsRedundantlyDecode(t *testing.T) {
59	t.Parallel()
60
61	wasm := []byte("\x00asm-redundant-fixture-p1")
62	fsys := countingWASMFS(t, wasm)
63
64	const requests = 3
65	got := make([][]byte, requests)
66	for i := range got {
67		data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
68		require.NoError(t, err)
69		fsys.record("systemviews/systemviews.wasm.br")
70		got[i] = data
71	}
72	for _, data := range got[1:] {
73		require.Equal(t, got[0], data, "repeated requests must serve identical bytes")
74	}
75	require.Equal(t, requests, fsys.count("systemviews/systemviews.wasm.br"),
76		"P1 baseline: every request re-reads the compressed asset; P2 must decode once")
77}
78
79func TestAssetP1_ConcurrentReadsServeIdenticalBytes(t *testing.T) {
80	t.Parallel()
81
82	wasm := []byte("\x00asm-concurrent-fixture-p1")
83	fsys := countingWASMFS(t, wasm)
84
85	const readers = 8
86	got := make([][]byte, readers)
87	errs := make([]error, readers)
88	var wg sync.WaitGroup
89	for i := range got {
90		wg.Add(1)
91		go func(i int) {
92			defer wg.Done()
93			got[i], errs[i] = readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
94		}(i)
95	}
96	wg.Wait()
97	for i := range got {
98		require.NoError(t, errs[i])
99		require.Equal(t, wasm, got[i])
100	}
101}
102
103func TestAssetP1_CompressedPreferredOverRaw(t *testing.T) {
104	t.Parallel()
105
106	fromCompressed := []byte("\x00asm-from-br")
107	fromRaw := []byte("\x00asm-from-raw")
108	fsys := fstest.MapFS{
109		"systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, fromCompressed)},
110		"systemviews/systemviews.wasm":    {Data: fromRaw},
111	}
112
113	data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
114	require.NoError(t, err)
115	require.Equal(t, fromCompressed, data, "compressed artifact wins when present")
116}
117
118func TestAssetP1_RawOnlyFallback(t *testing.T) {
119	t.Parallel()
120
121	raw := []byte("\x00asm-raw-only")
122	fsys := fstest.MapFS{
123		"systemviews/systemviews.wasm": {Data: raw},
124	}
125
126	data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
127	require.NoError(t, err)
128	require.Equal(t, raw, data)
129}
130
131func TestAssetP1_CorruptCompressedHasNoRawFallback(t *testing.T) {
132	t.Parallel()
133
134	// Current semantics: any successfully read .br is authoritative. A
135	// corrupt compressed artifact is an explicit error even when valid raw
136	// data exists. P2 keeps this tightened behavior.
137	fsys := fstest.MapFS{
138		"systemviews/systemviews.wasm.br": {Data: []byte("not-brotli-data")},
139		"systemviews/systemviews.wasm":    {Data: []byte("\x00asm-valid-raw")},
140	}
141
142	_, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
143	require.Error(t, err, "corrupt compressed WASM must fail explicitly, not fall back to raw")
144}
145
146func TestAssetP1_OversizedDecompressedRejected(t *testing.T) {
147	t.Parallel()
148
149	// Zeros compress to almost nothing, so this stays fast while proving
150	// the decompressed-size bound.
151	big := make([]byte, maxSystemviewsWASMBytes+1)
152	fsys := fstest.MapFS{
153		"systemviews/systemviews.wasm.br": {Data: brotliCompressForTest(t, big)},
154	}
155
156	_, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
157	require.Error(t, err, "decompressed output past the 64 MiB bound must fail")
158}
159
160func TestAssetP1_RawReadCurrentlyUnbounded(t *testing.T) {
161	t.Parallel()
162
163	// P1 baseline: the raw path has no size bound at all. P2.1 deliberately
164	// tightens this alongside the compressed path.
165	big := make([]byte, maxSystemviewsWASMBytes+1)
166	fsys := fstest.MapFS{
167		"systemviews/systemviews.wasm": {Data: big},
168	}
169
170	data, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
171	require.NoError(t, err)
172	require.Len(t, data, maxSystemviewsWASMBytes+1, "raw reads are unbounded at baseline")
173}
174
175func TestAssetP1_MissingAssetReportsNotExist(t *testing.T) {
176	t.Parallel()
177
178	fsys := fstest.MapFS{}
179
180	_, err := readAssetWithEncoding(fsys, "systemviews/systemviews.wasm", "systemviews.wasm")
181	require.Error(t, err)
182	require.ErrorIs(t, err, fs.ErrNotExist, "missing asset must surface fs.ErrNotExist")
183}
184
185// staticHandlerOf unwraps the identity-stubbed resource handler. Callers
186// must stub cefNewResourceHandler to the identity function first.
187func staticHandlerOf(t *testing.T, rh any) *staticResourceHandler {
188	t.Helper()
189	h, ok := rh.(*staticResourceHandler)
190	require.True(t, ok, "expected *staticResourceHandler, got %T", rh)
191	return h
192}
193
194func stubIdentityResourceHandler(t *testing.T) {
195	t.Helper()
196	old := cefNewResourceHandler
197	cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
198	t.Cleanup(func() { cefNewResourceHandler = old })
199}
200
201// P1.2 response classes: exactly where no-store is applied today. Asset
202// success, error pages, and the crash shell carry NO Cache-Control header
203// at baseline; P3 grants immutable headers only to version-validated
204// static bytes.
205
206func TestAssetP1_ShellServes200CarriesNoStore(t *testing.T) {
207	stubIdentityResourceHandler(t)
208	h := newTestDumbSchemeHandler(t)
209	h.setAssets(assets.WebUIAssets)
210
211	rh := staticHandlerOf(t, h.handleAsset(mustParseURL(t, "https://dumber.invalid/history")))
212	require.Equal(t, http.StatusOK, rh.statusCode)
213	require.Equal(t, "text/html; charset=utf-8", rh.contentType)
214	require.NotEmpty(t, rh.data)
215	require.Equal(t, noStoreCacheControl, rh.headers["Cache-Control"],
216		"P3.3 policy: the unversioned shell is explicitly noncacheable; only version-verified bytes earn immutable")
217}
218
219func TestAssetP1_CSSServes200CarriesNoStore(t *testing.T) {
220	stubIdentityResourceHandler(t)
221	h := newTestDumbSchemeHandler(t)
222	h.setAssets(assets.WebUIAssets)
223
224	rh := staticHandlerOf(t, h.handleAsset(mustParseURL(t, "https://dumber.invalid/systemviews.css")))
225	require.Equal(t, http.StatusOK, rh.statusCode)
226	require.Equal(t, "text/css; charset=utf-8", rh.contentType, "Go mime table carries the charset suffix")
227	require.Equal(t, noStoreCacheControl, rh.headers["Cache-Control"],
228		"P3.3 policy: unversioned static assets are explicitly noncacheable")
229}
230
231func TestAssetP1_MissingAssetServes404WithoutCacheHeaders(t *testing.T) {
232	stubIdentityResourceHandler(t)
233	h := newTestDumbSchemeHandler(t)
234	h.setAssets(assets.WebUIAssets)
235
236	rh := staticHandlerOf(t, h.handleAsset(mustParseURL(t, "https://dumber.invalid/does-not-exist.js")))
237	require.Equal(t, http.StatusNotFound, rh.statusCode)
238	require.Empty(t, rh.headers, "error pages carry no Cache-Control at baseline")
239}
240
241func TestAssetP1_RedirectCarriesNoStore(t *testing.T) {
242	stubIdentityResourceHandler(t)
243	h := newTestDumbSchemeHandler(t)
244
245	rh := staticHandlerOf(t, h.newRedirectResourceHandler(http.StatusTemporaryRedirect, "https://dumber.invalid/history"))
246	require.Equal(t, http.StatusTemporaryRedirect, rh.statusCode)
247	require.Equal(t, "https://dumber.invalid/history", rh.headers["Location"])
248	require.Equal(t, "no-store", rh.headers["Cache-Control"])
249}
250
251func TestAssetP1_RawAndErrorHandlersCarryNoCacheHeaders(t *testing.T) {
252	stubIdentityResourceHandler(t)
253	h := newTestDumbSchemeHandler(t)
254
255	require.Empty(t, staticHandlerOf(t, h.newRawResourceHandler(http.StatusOK, "text/html; charset=utf-8", []byte("<p>x</p>"))).headers)
256	require.Empty(t, staticHandlerOf(t, h.newErrorResourceHandler(http.StatusInternalServerError, "boom")).headers,
257		"error pages carry no Cache-Control at baseline")
258}
259
260func TestAssetP1_APIRawAndJSONCarryNoStoreCORS(t *testing.T) {
261	stubIdentityResourceHandler(t)
262	h := newTestDumbSchemeHandler(t)
263
264	raw := staticHandlerOf(t, h.newAPIRawResourceHandler(http.StatusOK, "application/json", []byte(`{}`)))
265	require.Equal(t, "no-store", raw.headers["Cache-Control"])
266	require.Equal(t, "*", raw.headers["Access-Control-Allow-Origin"])
267
268	js := staticHandlerOf(t, h.newAPIJSONResourceHandler(http.StatusNotFound, map[string]string{"error": "not found"}))
269	require.Equal(t, http.StatusNotFound, js.statusCode)
270	require.Equal(t, "application/json", js.contentType)
271	require.Equal(t, "no-store", js.headers["Cache-Control"])
272	require.Equal(t, "*", js.headers["Access-Control-Allow-Origin"])
273	require.JSONEq(t, `{"error":"not found"}`, string(js.data))
274}
275
276func TestAssetP1_PrivateAPICarriesNoStoreWithoutCORS(t *testing.T) {
277	stubIdentityResourceHandler(t)
278	h := newTestDumbSchemeHandler(t)
279
280	raw := staticHandlerOf(t, h.newPrivateAPIRawResourceHandler(http.StatusNoContent, "text/plain; charset=utf-8", nil))
281	require.Equal(t, "no-store", raw.headers["Cache-Control"])
282	require.NotContains(t, raw.headers, "Access-Control-Allow-Origin")
283
284	js := staticHandlerOf(t, h.newPrivateAPIJSONResourceHandler(http.StatusForbidden, map[string]string{"error": "forbidden"}))
285	require.Equal(t, "no-store", js.headers["Cache-Control"])
286	require.NotContains(t, js.headers, "Access-Control-Allow-Origin")
287}
288
289func TestAssetP1_FaviconHandlerCarriesNoStore(t *testing.T) {
290	stubIdentityResourceHandler(t)
291
292	rh := newFaviconResourceHandler(t.Context(), nil, "example.com", 32)
293	fav, ok := rh.(*faviconResourceHandler)
294	require.True(t, ok, "expected *faviconResourceHandler, got %T", rh)
295	require.Equal(t, "no-store", fav.headers["Cache-Control"])
296}
297
298func TestAssetP1_TopLevelAPIPathNeverResolvesAsAsset(t *testing.T) {
299	for _, raw := range []string{
300		"https://dumber.invalid/api/config",
301		"https://dumber.invalid/api/message",
302	} {
303		_, _, ok := resolveAssetPath(mustParseURL(t, raw))
304		require.False(t, ok, "%s must not resolve as an asset", raw)
305	}
306}
307
308func TestAssetP1_NestedAPIPathFallsThroughTo404(t *testing.T) {
309	stubIdentityResourceHandler(t)
310	h := newTestDumbSchemeHandler(t)
311	h.setAssets(assets.WebUIAssets)
312
313	// handleAPI routes real API traffic before handleAsset; a nested api/
314	// path reaching the asset layer must 404, never serve asset bytes.
315	rh := staticHandlerOf(t, h.handleAsset(mustParseURL(t, "https://dumber.invalid/history/api/message")))
316	require.Equal(t, http.StatusNotFound, rh.statusCode)
317}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close