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}