v0.26.2 3cd9d2e

github_test.go RawHistoryFolder

1package updater
2
3import (
4	"crypto/sha256"
5	"encoding/hex"
6	"os"
7	"path/filepath"
8	"strings"
9	"testing"
10)
11
12func TestCompareVersions(t *testing.T) {
13	tests := []struct {
14		name     string
15		v1       string
16		v2       string
17		expected int
18	}{
19		// Equal versions
20		{"equal simple", "1.0.0", "1.0.0", 0},
21		{"equal two parts", "1.0", "1.0", 0},
22		{"equal one part", "1", "1", 0},
23
24		// v1 < v2 (should update)
25		{"major less", "1.0.0", "2.0.0", -1},
26		{"minor less", "1.1.0", "1.2.0", -1},
27		{"patch less", "1.1.1", "1.1.2", -1},
28		{"complex less", "0.20.1", "0.21.0", -1},
29		{"real world less", "0.20.1", "0.20.2", -1},
30
31		// v1 > v2 (no update needed)
32		{"major greater", "2.0.0", "1.0.0", 1},
33		{"minor greater", "1.2.0", "1.1.0", 1},
34		{"patch greater", "1.1.2", "1.1.1", 1},
35
36		// Pre-release versions (suffix stripped)
37		{"prerelease equal", "1.0.0-alpha", "1.0.0", 0},
38		{"prerelease less", "1.0.0-alpha", "1.0.1", -1},
39		{"prerelease greater", "1.0.1-beta", "1.0.0", 1},
40
41		// Partial versions
42		{"partial v1", "1", "1.0.0", 0},
43		{"partial v2", "1.0.0", "1", 0},
44		{"partial less", "1", "2.0.0", -1},
45		{"partial greater", "2", "1.0.0", 1},
46
47		// Edge cases
48		{"zero versions", "0.0.0", "0.0.0", 0},
49		{"zero less", "0.0.0", "0.0.1", -1},
50	}
51
52	for _, tt := range tests {
53		t.Run(tt.name, func(t *testing.T) {
54			result := compareVersions(tt.v1, tt.v2)
55			if result != tt.expected {
56				t.Errorf("compareVersions(%q, %q) = %d, want %d", tt.v1, tt.v2, result, tt.expected)
57			}
58		})
59	}
60}
61
62func TestGetArchName(t *testing.T) {
63	// This test just ensures the function doesn't panic and returns something.
64	arch := getArchName()
65	if arch == "" {
66		t.Error("getArchName() returned empty string")
67	}
68}
69
70func TestValidateDownloadURL(t *testing.T) {
71	tests := []struct {
72		name    string
73		url     string
74		wantErr bool
75		errMsg  string
76	}{
77		// Valid URLs
78		{
79			name:    "valid latest release URL",
80			url:     "https://github.com/bnema/dumber/releases/latest/download/dumber_linux_x86_64.tar.gz",
81			wantErr: false,
82		},
83		{
84			name:    "valid versioned release URL",
85			url:     "https://github.com/bnema/dumber/releases/download/v0.15.0/dumber_linux_x86_64.tar.gz",
86			wantErr: false,
87		},
88
89		// Invalid scheme
90		{
91			name:    "http not allowed",
92			url:     "http://github.com/bnema/dumber/releases/latest/download/dumber_linux_x86_64.tar.gz",
93			wantErr: true,
94			errMsg:  "must use HTTPS",
95		},
96		{
97			name:    "ftp not allowed",
98			url:     "ftp://github.com/bnema/dumber/releases/latest/download/file.tar.gz",
99			wantErr: true,
100			errMsg:  "must use HTTPS",
101		},
102
103		// Invalid host
104		{
105			name:    "wrong host",
106			url:     "https://evil.com/bnema/dumber/releases/latest/download/file.tar.gz",
107			wantErr: true,
108			errMsg:  "must be from github.com",
109		},
110		{
111			name:    "subdomain not allowed",
112			url:     "https://raw.github.com/bnema/dumber/releases/latest/download/file.tar.gz",
113			wantErr: true,
114			errMsg:  "must be from github.com",
115		},
116		{
117			name:    "typosquat domain",
118			url:     "https://githuh.com/bnema/dumber/releases/latest/download/file.tar.gz",
119			wantErr: true,
120			errMsg:  "must be from github.com",
121		},
122
123		// Invalid path
124		{
125			name:    "not a releases URL",
126			url:     "https://github.com/bnema/dumber/raw/main/README.md",
127			wantErr: true,
128			errMsg:  "must be a GitHub releases URL",
129		},
130		{
131			name:    "blob URL not allowed",
132			url:     "https://github.com/bnema/dumber/blob/main/cmd/dumber/main.go",
133			wantErr: true,
134			errMsg:  "must be a GitHub releases URL",
135		},
136
137		// Malformed URLs
138		{
139			name:    "empty URL",
140			url:     "",
141			wantErr: true,
142		},
143		{
144			name:    "invalid URL format",
145			url:     "://not-a-url",
146			wantErr: true,
147		},
148	}
149
150	for _, tt := range tests {
151		t.Run(tt.name, func(t *testing.T) {
152			err := validateDownloadURL(tt.url)
153			if tt.wantErr {
154				if err == nil {
155					t.Errorf("validateDownloadURL(%q) expected error, got nil", tt.url)
156				} else if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) {
157					t.Errorf("validateDownloadURL(%q) error = %q, want to contain %q", tt.url, err.Error(), tt.errMsg)
158				}
159			} else {
160				if err != nil {
161					t.Errorf("validateDownloadURL(%q) unexpected error: %v", tt.url, err)
162				}
163			}
164		})
165	}
166}
167
168func TestSanitizeTarPath(t *testing.T) {
169	destDir := "/tmp/extract"
170
171	tests := []struct {
172		name     string
173		path     string
174		wantErr  bool
175		errMsg   string
176		wantPath string
177	}{
178		// Valid paths
179		{
180			name:     "simple filename",
181			path:     "dumber",
182			wantErr:  false,
183			wantPath: "/tmp/extract/dumber",
184		},
185		{
186			name:     "nested path",
187			path:     "dumber_v0.15.0/dumber",
188			wantErr:  false,
189			wantPath: "/tmp/extract/dumber_v0.15.0/dumber",
190		},
191		{
192			name:     "deep nested path",
193			path:     "release/bin/dumber",
194			wantErr:  false,
195			wantPath: "/tmp/extract/release/bin/dumber",
196		},
197
198		// Path traversal attempts
199		{
200			name:    "parent directory escape",
201			path:    "../../../etc/passwd",
202			wantErr: true,
203			errMsg:  "path traversal",
204		},
205		{
206			name:    "hidden parent escape",
207			path:    "dumber_v0.15.0/../../etc/passwd",
208			wantErr: true,
209			errMsg:  "path traversal",
210		},
211		{
212			name:     "double dot at start of filename",
213			path:     "..dumber",
214			wantErr:  false, // Valid filename, not a path traversal (.. is not a path component)
215			wantPath: "/tmp/extract/..dumber",
216		},
217		{
218			name:    "absolute path unix",
219			path:    "/etc/passwd",
220			wantErr: true,
221			errMsg:  "absolute path",
222		},
223		{
224			name:    "dot dot in middle",
225			path:    "foo/../../../bar",
226			wantErr: true,
227			errMsg:  "path traversal",
228		},
229
230		// Edge cases
231		{
232			name:     "current directory",
233			path:     "./dumber",
234			wantErr:  false,
235			wantPath: "/tmp/extract/dumber",
236		},
237		{
238			name:     "multiple slashes",
239			path:     "dumber_v0.15.0//dumber",
240			wantErr:  false,
241			wantPath: "/tmp/extract/dumber_v0.15.0/dumber",
242		},
243	}
244
245	for _, tt := range tests {
246		t.Run(tt.name, func(t *testing.T) {
247			result, err := sanitizeTarPath(tt.path, destDir)
248			if tt.wantErr {
249				if err == nil {
250					t.Errorf("sanitizeTarPath(%q, %q) expected error, got nil", tt.path, destDir)
251				} else if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) {
252					t.Errorf("sanitizeTarPath(%q, %q) error = %q, want to contain %q", tt.path, destDir, err.Error(), tt.errMsg)
253				}
254			} else {
255				if err != nil {
256					t.Errorf("sanitizeTarPath(%q, %q) unexpected error: %v", tt.path, destDir, err)
257				} else if tt.wantPath != "" && result != tt.wantPath {
258					t.Errorf("sanitizeTarPath(%q, %q) = %q, want %q", tt.path, destDir, result, tt.wantPath)
259				}
260			}
261		})
262	}
263}
264
265func TestVerifyChecksum(t *testing.T) {
266	// Create a temporary file with known content
267	tmpDir := t.TempDir()
268	testFile := filepath.Join(tmpDir, "testfile")
269	testContent := []byte("hello world\n")
270
271	if err := os.WriteFile(testFile, testContent, 0o644); err != nil {
272		t.Fatalf("Failed to create test file: %v", err)
273	}
274
275	// Calculate expected SHA256 hash
276	hash := sha256.Sum256(testContent)
277	expectedHash := hex.EncodeToString(hash[:])
278
279	tests := []struct {
280		name     string
281		filePath string
282		hash     string
283		wantErr  bool
284		errMsg   string
285	}{
286		{
287			name:     "valid checksum lowercase",
288			filePath: testFile,
289			hash:     expectedHash,
290			wantErr:  false,
291		},
292		{
293			name:     "valid checksum uppercase",
294			filePath: testFile,
295			hash:     hex.EncodeToString(hash[:]),
296			wantErr:  false,
297		},
298		{
299			name:     "invalid checksum",
300			filePath: testFile,
301			hash:     "0000000000000000000000000000000000000000000000000000000000000000",
302			wantErr:  true,
303			errMsg:   "checksum mismatch",
304		},
305		{
306			name:     "wrong length hash",
307			filePath: testFile,
308			hash:     "abc123",
309			wantErr:  true,
310			errMsg:   "checksum mismatch",
311		},
312		{
313			name:     "file not found",
314			filePath: filepath.Join(tmpDir, "nonexistent"),
315			hash:     expectedHash,
316			wantErr:  true,
317			errMsg:   "failed to open",
318		},
319	}
320
321	for _, tt := range tests {
322		t.Run(tt.name, func(t *testing.T) {
323			err := verifyChecksum(tt.filePath, tt.hash)
324			if tt.wantErr {
325				if err == nil {
326					t.Errorf("verifyChecksum() expected error, got nil")
327				} else if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) {
328					t.Errorf("verifyChecksum() error = %q, want to contain %q", err.Error(), tt.errMsg)
329				}
330			} else {
331				if err != nil {
332					t.Errorf("verifyChecksum() unexpected error: %v", err)
333				}
334			}
335		})
336	}
337}
338
339func TestSizeLimits(t *testing.T) {
340	// Verify our size constants are sensible
341	if maxArchiveSize <= 0 {
342		t.Error("maxArchiveSize should be positive")
343	}
344	if maxBinarySize <= 0 {
345		t.Error("maxBinarySize should be positive")
346	}
347	if minBinarySize <= 0 {
348		t.Error("minBinarySize should be positive")
349	}
350	if minBinarySize >= maxBinarySize {
351		t.Errorf("minBinarySize (%d) should be less than maxBinarySize (%d)", minBinarySize, maxBinarySize)
352	}
353	if maxBinarySize >= maxArchiveSize {
354		t.Errorf("maxBinarySize (%d) should be less than maxArchiveSize (%d)", maxBinarySize, maxArchiveSize)
355	}
356}
357
358// contains checks if substr is in s (helper for error message checks).
359func contains(s, substr string) bool {
360	return strings.Contains(s, substr)
361}

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