manifest_test.go RawHistoryFolder

1package assets
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"io/fs"
8	"os"
9	"strings"
10	"testing"
11
12	"github.com/andybalholm/brotli"
13)
14
15func TestParseManifestRejectsBadInputs(t *testing.T) {
16	t.Parallel()
17
18	good := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
19	pin := func(digest string, size int) string {
20		return fmt.Sprintf(`"sha256":%q,"size":%d`, digest, size)
21	}
22	entry := func(css, wasm, js string) string {
23		return `{"version":1,"files":{"systemviews.css":{` + css + `},"systemviews.wasm":{` + wasm + `},"wasm_exec.js":{` + js + `}}}`
24	}
25	valid := entry(pin(good, 1), pin(good, 2), pin(good, 3))
26	if _, err := ParseManifest([]byte(valid)); err != nil {
27		t.Fatalf("ParseManifest(valid) error = %v", err)
28	}
29
30	cases := map[string]string{
31		"bad version":  strings.Replace(valid, `"version":1`, `"version":2`, 1),
32		"missing pin":  `{"version":1,"files":{"systemviews.css":{` + pin(good, 1) + `},"systemviews.wasm":{` + pin(good, 2) + `}}}`,
33		"unknown file": strings.TrimSuffix(valid, `}`) + `,"evil.js":{` + pin(good, 4) + `}}`,
34		"empty digest": entry(pin("", 1), pin(good, 2), pin(good, 3)),
35		"short digest": entry(pin("abc", 1), pin(good, 2), pin(good, 3)),
36		"long digest":  entry(pin(good+"00", 1), pin(good, 2), pin(good, 3)),
37		"nonhex digest": entry(pin(strings.Repeat("g", 64), 1), pin(good, 2),
38			pin(good, 3)),
39		"upper digest": entry(pin(strings.Repeat("A", 64), 1), pin(good, 2),
40			pin(good, 3)),
41		// Pins breaking out of the shell's quoted attributes must fail
42		// closed instead of reaching HTML/JS interpolation.
43		"quote breakout": entry(pin(good, 1),
44			pin("');alert(1);//"+strings.Repeat("0", 52), 2), pin(good, 3)),
45		"markup pin": entry(pin(good, 1),
46			pin("<script>alert(1)</"+"script>"+strings.Repeat("0", 39), 2), pin(good, 3)),
47		"not JSON":      `not json`,
48		"trailing data": valid + `{}`,
49	}
50	for name, input := range cases {
51		t.Run(name, func(t *testing.T) {
52			t.Parallel()
53			if _, err := ParseManifest([]byte(input)); err == nil {
54				t.Fatalf("ParseManifest(%s) succeeded, want error", name)
55			}
56		})
57	}
58}
59
60func TestManifestBytesAreCanonical(t *testing.T) {
61	t.Parallel()
62
63	good := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
64	m := Manifest{Version: ManifestVersion, Files: map[string]FileEntry{
65		"wasm_exec.js":     {SHA256: good, Size: 3},
66		"systemviews.wasm": {SHA256: good, Size: 2},
67		"systemviews.css":  {SHA256: good, Size: 1},
68	}}
69	first, err := m.Bytes()
70	if err != nil {
71		t.Fatalf("Bytes() error = %v", err)
72	}
73	second, err := m.Bytes()
74	if err != nil {
75		t.Fatalf("Bytes() error = %v", err)
76	}
77	if !bytes.Equal(first, second) {
78		t.Fatal("manifest encoding is not reproducible")
79	}
80	if !bytes.HasSuffix(first, []byte("\n")) || bytes.Contains(first, []byte("1970")) {
81		t.Fatalf("manifest must end with newline and carry no timestamps:\n%s", first)
82	}
83	// Sorted keys: css < wasm < wasm_exec.
84	css := bytes.Index(first, []byte("systemviews.css"))
85	wasm := bytes.Index(first, []byte("systemviews.wasm"))
86	js := bytes.Index(first, []byte("wasm_exec.js"))
87	if css >= wasm || wasm >= js {
88		t.Fatalf("manifest keys not sorted:\n%s", first)
89	}
90	if _, err := ParseManifest(first); err != nil {
91		t.Fatalf("canonical bytes do not round-trip: %v", err)
92	}
93}
94
95// TestAssetManifestMatchesEmbeddedArtifacts validates every manifest entry
96// against the actually embedded artifacts, including raw/compressed WASM
97// agreement. It skips when the generated bundle is absent, mirroring the
98// existing WASM presence test.
99func TestAssetManifestMatchesEmbeddedArtifacts(t *testing.T) {
100	data, err := WebUIAssets.ReadFile("systemviews/" + ManifestName)
101	if err != nil {
102		if os.Getenv("DUMBER_REQUIRE_SYSTEMVIEWS_WASM") == "" {
103			t.Skipf("generated asset manifest not present; run make build-systemviews: %v", err)
104		}
105		t.Fatalf("read embedded asset manifest: %v", err)
106	}
107	m, err := ParseManifest(data)
108	if err != nil {
109		t.Fatalf("ParseManifest(embedded) error = %v", err)
110	}
111	sub, err := fs.Sub(WebUIAssets, "systemviews")
112	if err != nil {
113		t.Fatalf("fs.Sub(embedded systemviews) error = %v", err)
114	}
115	if err := m.Verify(sub); err != nil {
116		t.Fatalf("embedded artifacts do not match manifest: %v", err)
117	}
118	// The manifest pins raw bytes, but serving decodes the compressed
119	// artifact as authoritative: the embedded .br must agree too, or a
120	// stale compressed bundle would pass verification yet fail serving.
121	if err := verifyEmbeddedCompressedAgreement(sub, m); err != nil {
122		t.Fatalf("embedded compressed WASM disagrees: %v", err)
123	}
124}
125
126// verifyEmbeddedCompressedAgreement checks the embedded .br decodes to
127// exactly the pinned raw bytes under a bounded read. Test-only: production
128// agreement checks live in cmd/systemviews-assets; this keeps the assets
129// package dependency-free.
130func verifyEmbeddedCompressedAgreement(fsys fs.FS, m Manifest) error {
131	want := m.Files["systemviews.wasm"]
132	compressed, err := fs.ReadFile(fsys, "systemviews.wasm.br")
133	if err != nil {
134		return fmt.Errorf("read embedded systemviews.wasm.br: %w", err)
135	}
136	raw, err := fs.ReadFile(fsys, "systemviews.wasm")
137	if err != nil {
138		return fmt.Errorf("read embedded systemviews.wasm: %w", err)
139	}
140	data, err := io.ReadAll(io.LimitReader(brotli.NewReader(bytes.NewReader(compressed)), 64*1024*1024+1))
141	if err != nil {
142		return fmt.Errorf("decompress embedded systemviews.wasm.br: %w", err)
143	}
144	if !bytes.Equal(data, raw) || int64(len(raw)) != want.Size {
145		return fmt.Errorf("embedded systemviews.wasm.br does not match pinned raw bytes")
146	}
147	return nil
148}

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