v0.27.1 c78ce83

normalize_test.go RawHistoryFolder

1package url
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7)
8
9func TestNormalize(t *testing.T) {
10	tests := []struct {
11		name  string
12		input string
13		want  string
14	}{
15		{
16			name:  "empty string",
17			input: "",
18			want:  "",
19		},
20		{
21			name:  "http scheme unchanged",
22			input: "http://example.com",
23			want:  "http://example.com",
24		},
25		{
26			name:  "https scheme unchanged",
27			input: "https://example.com",
28			want:  "https://example.com",
29		},
30		{
31			name:  "file scheme unchanged",
32			input: "file:///path/to/file.html",
33			want:  "file:///path/to/file.html",
34		},
35		{
36			name:  "dumb scheme unchanged",
37			input: "dumb://home",
38			want:  "dumb://home",
39		},
40		{
41			name:  "about scheme unchanged",
42			input: "about:blank",
43			want:  "about:blank",
44		},
45		{
46			name:  "domain gets https",
47			input: "example.com",
48			want:  "https://example.com",
49		},
50		{
51			name:  "domain with path gets https",
52			input: "example.com/path",
53			want:  "https://example.com/path",
54		},
55		{
56			name:  "search query unchanged",
57			input: "hello world",
58			want:  "hello world",
59		},
60		{
61			name:  "single word unchanged",
62			input: "hello",
63			want:  "hello",
64		},
65		{
66			name:  "localhost",
67			input: "localhost",
68			want:  "http://localhost",
69		},
70		{
71			name:  "localhost with port",
72			input: "localhost:5173",
73			want:  "http://localhost:5173",
74		},
75		{
76			name:  "localhost with alt port",
77			input: "localhost:8080",
78			want:  "http://localhost:8080",
79		},
80		{
81			name:  "localhost with path",
82			input: "localhost/api/v1",
83			want:  "http://localhost/api/v1",
84		},
85		{
86			name:  "localhost.com is a domain not localhost",
87			input: "localhost.com",
88			want:  "https://localhost.com",
89		},
90		{
91			name:  "localhostevil.com is a domain not localhost",
92			input: "localhostevil.com",
93			want:  "https://localhostevil.com",
94		},
95	}
96
97	for _, tt := range tests {
98		t.Run(tt.name, func(t *testing.T) {
99			got := Normalize(tt.input)
100			if got != tt.want {
101				t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
102			}
103		})
104	}
105}
106
107func TestNormalize_LocalFiles(t *testing.T) {
108	// Create a temporary file for testing
109	tmpDir := t.TempDir()
110	tmpFile := filepath.Join(tmpDir, "test.html")
111	if err := os.WriteFile(tmpFile, []byte("<html></html>"), 0644); err != nil {
112		t.Fatalf("failed to create temp file: %v", err)
113	}
114
115	// Create a subdirectory with a file
116	subDir := filepath.Join(tmpDir, "subdir")
117	if err := os.Mkdir(subDir, 0755); err != nil {
118		t.Fatalf("failed to create subdir: %v", err)
119	}
120	subFile := filepath.Join(subDir, "page.html")
121	if err := os.WriteFile(subFile, []byte("<html></html>"), 0644); err != nil {
122		t.Fatalf("failed to create subdir file: %v", err)
123	}
124
125	tests := []struct {
126		name  string
127		input string
128		want  string
129	}{
130		{
131			name:  "absolute path to existing file",
132			input: tmpFile,
133			want:  "file://" + tmpFile,
134		},
135		{
136			name:  "absolute path to existing directory",
137			input: tmpDir,
138			want:  "file://" + tmpDir,
139		},
140		{
141			name:  "non-existent absolute path returned unchanged",
142			input: "/nonexistent/file.html",
143			want:  "/nonexistent/file.html",
144		},
145		{
146			name:  "non-existent relative path with dot prefix returned unchanged",
147			input: "./nonexistent/file.html",
148			want:  "./nonexistent/file.html",
149		},
150		{
151			name:  "non-existent home path returned unchanged",
152			input: "~/nonexistent/file.html",
153			want:  "~/nonexistent/file.html",
154		},
155	}
156
157	for _, tt := range tests {
158		t.Run(tt.name, func(t *testing.T) {
159			got := Normalize(tt.input)
160			if got != tt.want {
161				t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
162			}
163		})
164	}
165}
166
167func TestNormalize_RelativePaths(t *testing.T) {
168	// Create a temp file in current directory
169	tmpFile, err := os.CreateTemp(".", "test-*.html")
170	if err != nil {
171		t.Fatalf("failed to create temp file: %v", err)
172	}
173	tmpFile.Close()
174	defer os.Remove(tmpFile.Name())
175
176	// Get absolute path for comparison
177	absPath, err := filepath.Abs(tmpFile.Name())
178	if err != nil {
179		t.Fatalf("failed to get abs path: %v", err)
180	}
181
182	t.Run("relative path to existing file", func(t *testing.T) {
183		got := Normalize(tmpFile.Name())
184		want := "file://" + absPath
185		if got != want {
186			t.Errorf("Normalize(%q) = %q, want %q", tmpFile.Name(), got, want)
187		}
188	})
189}
190
191func TestExpandHome(t *testing.T) {
192	home, err := os.UserHomeDir()
193	if err != nil {
194		t.Skip("cannot get home directory")
195	}
196
197	tests := []struct {
198		name  string
199		input string
200		want  string
201	}{
202		{
203			name:  "tilde expansion",
204			input: "~/Documents",
205			want:  filepath.Join(home, "Documents"),
206		},
207		{
208			name:  "no tilde unchanged",
209			input: "/absolute/path",
210			want:  "/absolute/path",
211		},
212		{
213			name:  "relative path unchanged",
214			input: "./relative/path",
215			want:  "./relative/path",
216		},
217		{
218			name:  "tilde in middle unchanged",
219			input: "/path/~/file",
220			want:  "/path/~/file",
221		},
222	}
223
224	for _, tt := range tests {
225		t.Run(tt.name, func(t *testing.T) {
226			got := expandHome(tt.input)
227			if got != tt.want {
228				t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.want)
229			}
230		})
231	}
232}
233
234func TestTrimLeadingSpacesIfURL(t *testing.T) {
235	tests := []struct {
236		name  string
237		input string
238		want  string
239	}{
240		{
241			name:  "single space before https URL",
242			input: " https://example.com",
243			want:  "https://example.com",
244		},
245		{
246			name:  "multiple spaces before https URL",
247			input: "   https://example.com",
248			want:  "https://example.com",
249		},
250		{
251			name:  "tab before URL",
252			input: "\thttps://example.com",
253			want:  "https://example.com",
254		},
255		{
256			name:  "mixed whitespace before URL",
257			input: " \t https://example.com",
258			want:  "https://example.com",
259		},
260		{
261			name:  "space before domain",
262			input: " example.com",
263			want:  "example.com",
264		},
265		{
266			name:  "spaces before localhost",
267			input: "  localhost:8080",
268			want:  "localhost:8080",
269		},
270		{
271			name:  "no leading space URL unchanged",
272			input: "https://example.com",
273			want:  "https://example.com",
274		},
275		{
276			name:  "search query with leading space unchanged",
277			input: " hello world",
278			want:  " hello world",
279		},
280		{
281			name:  "single word with leading space unchanged",
282			input: " hello",
283			want:  " hello",
284		},
285		{
286			name:  "empty string unchanged",
287			input: "",
288			want:  "",
289		},
290		{
291			name:  "only spaces unchanged",
292			input: "   ",
293			want:  "   ",
294		},
295	}
296
297	for _, tt := range tests {
298		t.Run(tt.name, func(t *testing.T) {
299			got := TrimLeadingSpacesIfURL(tt.input)
300			if got != tt.want {
301				t.Errorf("TrimLeadingSpacesIfURL(%q) = %q, want %q", tt.input, got, tt.want)
302			}
303		})
304	}
305}
306
307func TestLooksLikeURL(t *testing.T) {
308	tests := []struct {
309		name  string
310		input string
311		want  bool
312	}{
313		{
314			name:  "empty string",
315			input: "",
316			want:  false,
317		},
318		{
319			name:  "http URL",
320			input: "http://example.com",
321			want:  true,
322		},
323		{
324			name:  "https URL",
325			input: "https://example.com",
326			want:  true,
327		},
328		{
329			name:  "file URL",
330			input: "file:///path/to/file",
331			want:  true,
332		},
333		{
334			name:  "dumb URL",
335			input: "dumb://home",
336			want:  true,
337		},
338		{
339			name:  "about URL",
340			input: "about:blank",
341			want:  true,
342		},
343		{
344			name:  "domain",
345			input: "example.com",
346			want:  true,
347		},
348		{
349			name:  "domain with path",
350			input: "example.com/path",
351			want:  true,
352		},
353		{
354			name:  "search query",
355			input: "hello world",
356			want:  false,
357		},
358		{
359			name:  "single word",
360			input: "hello",
361			want:  false,
362		},
363		{
364			name:  "localhost",
365			input: "localhost",
366			want:  true,
367		},
368		{
369			name:  "localhost with port",
370			input: "localhost:5173",
371			want:  true,
372		},
373		{
374			name:  "localhost with alt port",
375			input: "localhost:8080",
376			want:  true,
377		},
378		{
379			name:  "localhost with path",
380			input: "localhost/api/v1",
381			want:  true,
382		},
383		{
384			name:  "localhost.com is a domain not localhost",
385			input: "localhost.com",
386			want:  true,
387		},
388		{
389			name:  "localhostevil.com is a domain not localhost",
390			input: "localhostevil.com",
391			want:  true,
392		},
393	}
394
395	for _, tt := range tests {
396		t.Run(tt.name, func(t *testing.T) {
397			got := LooksLikeURL(tt.input)
398			if got != tt.want {
399				t.Errorf("LooksLikeURL(%q) = %v, want %v", tt.input, got, tt.want)
400			}
401		})
402	}
403}
404
405func TestIsExternalScheme(t *testing.T) {
406	tests := []struct {
407		name  string
408		input string
409		want  bool
410	}{
411		{
412			name:  "empty string",
413			input: "",
414			want:  false,
415		},
416		{
417			name:  "http scheme",
418			input: "http://example.com",
419			want:  false,
420		},
421		{
422			name:  "https scheme",
423			input: "https://example.com",
424			want:  false,
425		},
426		{
427			name:  "file scheme",
428			input: "file:///path/to/file.html",
429			want:  false,
430		},
431		{
432			name:  "dumb scheme",
433			input: "dumb://home",
434			want:  false,
435		},
436		{
437			name:  "about scheme",
438			input: "about:blank",
439			want:  false,
440		},
441		{
442			name:  "data scheme",
443			input: "data:text/plain;base64,SGVsbG8gV29ybGQ==",
444			want:  false,
445		},
446		{
447			name:  "blob scheme",
448			input: "blob:https://example.com/blob-id",
449			want:  false,
450		},
451		{
452			name:  "javascript scheme",
453			input: "javascript:alert('hello')",
454			want:  false,
455		},
456		{
457			name:  "vscode scheme",
458			input: "vscode://open",
459			want:  true,
460		},
461		{
462			name:  "spotify scheme",
463			input: "spotify:track:123",
464			want:  true,
465		},
466		{
467			name:  "steam scheme",
468			input: "steam://run/123",
469			want:  true,
470		},
471		{
472			name:  "uppercase HTTP scheme",
473			input: "HTTP://example.com",
474			want:  false,
475		},
476		{
477			name:  "uppercase ABOUT scheme",
478			input: "ABOUT:blank",
479			want:  false,
480		},
481		{
482			name:  "uppercase VSCODE scheme",
483			input: "VSCODE://open",
484			want:  true,
485		},
486		{
487			name:  "malformed URL",
488			input: "://invalid",
489			want:  false,
490		},
491		{
492			name:  "no scheme",
493			input: "example.com",
494			want:  false,
495		},
496		{
497			name:  "scheme only",
498			input: "custom://",
499			want:  true,
500		},
501	}
502
503	for _, tt := range tests {
504		t.Run(tt.name, func(t *testing.T) {
505			got := IsExternalScheme(tt.input)
506			if got != tt.want {
507				t.Errorf("IsExternalScheme(%q) = %v, want %v", tt.input, got, tt.want)
508			}
509		})
510	}
511}
512
513func TestExtractOrigin_ValidURI(t *testing.T) {
514	tests := []struct {
515		name     string
516		uri      string
517		expected string
518	}{
519		{
520			name:     "https with port",
521			uri:      "https://example.com:8443/path?query=1",
522			expected: "https://example.com:8443",
523		},
524		{
525			name:     "https without port",
526			uri:      "https://example.com/path",
527			expected: "https://example.com",
528		},
529		{
530			name:     "http",
531			uri:      "http://localhost:8080/app",
532			expected: "http://localhost:8080",
533		},
534		{
535			name:     "uppercase host",
536			uri:      "https://EXAMPLE.COM/path",
537			expected: "https://example.com",
538		},
539		{
540			name:     "uppercase scheme",
541			uri:      "HTTPS://example.com/path",
542			expected: "https://example.com",
543		},
544		{
545			name:     "https default port omitted",
546			uri:      "https://example.com:443/path",
547			expected: "https://example.com",
548		},
549		{
550			name:     "http default port omitted",
551			uri:      "http://example.com:80/path",
552			expected: "http://example.com",
553		},
554		{
555			name:     "https explicit non-default port kept",
556			uri:      "https://example.com:8080/path",
557			expected: "https://example.com:8080",
558		},
559		{
560			name:     "mixed case with default port",
561			uri:      "HTTPS://EXAMPLE.COM:443/path",
562			expected: "https://example.com",
563		},
564		{
565			name:     "ipv6 with explicit port",
566			uri:      "https://[::1]:8443/path",
567			expected: "https://[::1]:8443",
568		},
569	}
570
571	for _, tt := range tests {
572		t.Run(tt.name, func(t *testing.T) {
573			origin, err := ExtractOrigin(tt.uri)
574			if err != nil {
575				t.Errorf("ExtractOrigin(%q) returned error: %v", tt.uri, err)
576				return
577			}
578			if origin != tt.expected {
579				t.Errorf("ExtractOrigin(%q) = %q, want %q", tt.uri, origin, tt.expected)
580			}
581		})
582	}
583}
584
585func TestExtractOrigin_InvalidURI(t *testing.T) {
586	tests := []struct {
587		name string
588		uri  string
589	}{
590		{name: "empty", uri: ""},
591		{name: "no scheme", uri: "example.com"},
592		{name: "no host", uri: "https://"},
593	}
594
595	for _, tt := range tests {
596		t.Run(tt.name, func(t *testing.T) {
597			origin, err := ExtractOrigin(tt.uri)
598			if err == nil {
599				t.Errorf("ExtractOrigin(%q) should return error, got origin: %q", tt.uri, origin)
600			}
601		})
602	}
603}

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