v0.28.0 3cb3770

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			name:  "ipv4 with port",
395			input: "100.64.0.10:3000",
396			want:  true,
397		},
398		{
399			name:  "ipv4",
400			input: "127.0.0.1",
401			want:  true,
402		},
403		{
404			name:  "ipv6 bracketed",
405			input: "[::1]",
406			want:  true,
407		},
408	}
409
410	for _, tt := range tests {
411		t.Run(tt.name, func(t *testing.T) {
412			got := LooksLikeURL(tt.input)
413			if got != tt.want {
414				t.Errorf("LooksLikeURL(%q) = %v, want %v", tt.input, got, tt.want)
415			}
416		})
417	}
418}
419
420func TestNormalize_IPAddressGetsHTTPByDefault(t *testing.T) {
421	tests := []struct {
422		name  string
423		input string
424		want  string
425	}{
426		{name: "tailnet ipv4", input: "100.64.0.10", want: "http://100.64.0.10"},
427		{name: "tailnet ipv4 with port", input: "100.64.0.10:8080", want: "http://100.64.0.10:8080"},
428		{name: "ipv4 with path", input: "100.64.0.10/api", want: "http://100.64.0.10/api"},
429	}
430
431	for _, tt := range tests {
432		t.Run(tt.name, func(t *testing.T) {
433			got := Normalize(tt.input)
434			if got != tt.want {
435				t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
436			}
437		})
438	}
439}
440
441func TestIsExternalScheme(t *testing.T) {
442	tests := []struct {
443		name  string
444		input string
445		want  bool
446	}{
447		{
448			name:  "empty string",
449			input: "",
450			want:  false,
451		},
452		{
453			name:  "http scheme",
454			input: "http://example.com",
455			want:  false,
456		},
457		{
458			name:  "https scheme",
459			input: "https://example.com",
460			want:  false,
461		},
462		{
463			name:  "file scheme",
464			input: "file:///path/to/file.html",
465			want:  false,
466		},
467		{
468			name:  "dumb scheme",
469			input: "dumb://home",
470			want:  false,
471		},
472		{
473			name:  "about scheme",
474			input: "about:blank",
475			want:  false,
476		},
477		{
478			name:  "data scheme",
479			input: "data:text/plain;base64,SGVsbG8gV29ybGQ==",
480			want:  false,
481		},
482		{
483			name:  "blob scheme",
484			input: "blob:https://example.com/blob-id",
485			want:  false,
486		},
487		{
488			name:  "javascript scheme",
489			input: "javascript:alert('hello')",
490			want:  false,
491		},
492		{
493			name:  "vscode scheme",
494			input: "vscode://open",
495			want:  true,
496		},
497		{
498			name:  "spotify scheme",
499			input: "spotify:track:123",
500			want:  true,
501		},
502		{
503			name:  "steam scheme",
504			input: "steam://run/123",
505			want:  true,
506		},
507		{
508			name:  "uppercase HTTP scheme",
509			input: "HTTP://example.com",
510			want:  false,
511		},
512		{
513			name:  "uppercase ABOUT scheme",
514			input: "ABOUT:blank",
515			want:  false,
516		},
517		{
518			name:  "uppercase VSCODE scheme",
519			input: "VSCODE://open",
520			want:  true,
521		},
522		{
523			name:  "malformed URL",
524			input: "://invalid",
525			want:  false,
526		},
527		{
528			name:  "no scheme",
529			input: "example.com",
530			want:  false,
531		},
532		{
533			name:  "scheme only",
534			input: "custom://",
535			want:  true,
536		},
537	}
538
539	for _, tt := range tests {
540		t.Run(tt.name, func(t *testing.T) {
541			got := IsExternalScheme(tt.input)
542			if got != tt.want {
543				t.Errorf("IsExternalScheme(%q) = %v, want %v", tt.input, got, tt.want)
544			}
545		})
546	}
547}
548
549func TestExtractOrigin_ValidURI(t *testing.T) {
550	tests := []struct {
551		name     string
552		uri      string
553		expected string
554	}{
555		{
556			name:     "https with port",
557			uri:      "https://example.com:8443/path?query=1",
558			expected: "https://example.com:8443",
559		},
560		{
561			name:     "https without port",
562			uri:      "https://example.com/path",
563			expected: "https://example.com",
564		},
565		{
566			name:     "http",
567			uri:      "http://localhost:8080/app",
568			expected: "http://localhost:8080",
569		},
570		{
571			name:     "uppercase host",
572			uri:      "https://EXAMPLE.COM/path",
573			expected: "https://example.com",
574		},
575		{
576			name:     "uppercase scheme",
577			uri:      "HTTPS://example.com/path",
578			expected: "https://example.com",
579		},
580		{
581			name:     "https default port omitted",
582			uri:      "https://example.com:443/path",
583			expected: "https://example.com",
584		},
585		{
586			name:     "http default port omitted",
587			uri:      "http://example.com:80/path",
588			expected: "http://example.com",
589		},
590		{
591			name:     "https explicit non-default port kept",
592			uri:      "https://example.com:8080/path",
593			expected: "https://example.com:8080",
594		},
595		{
596			name:     "mixed case with default port",
597			uri:      "HTTPS://EXAMPLE.COM:443/path",
598			expected: "https://example.com",
599		},
600		{
601			name:     "ipv6 with explicit port",
602			uri:      "https://[::1]:8443/path",
603			expected: "https://[::1]:8443",
604		},
605	}
606
607	for _, tt := range tests {
608		t.Run(tt.name, func(t *testing.T) {
609			origin, err := ExtractOrigin(tt.uri)
610			if err != nil {
611				t.Errorf("ExtractOrigin(%q) returned error: %v", tt.uri, err)
612				return
613			}
614			if origin != tt.expected {
615				t.Errorf("ExtractOrigin(%q) = %q, want %q", tt.uri, origin, tt.expected)
616			}
617		})
618	}
619}
620
621func TestExtractOrigin_InvalidURI(t *testing.T) {
622	tests := []struct {
623		name string
624		uri  string
625	}{
626		{name: "empty", uri: ""},
627		{name: "no scheme", uri: "example.com"},
628		{name: "no host", uri: "https://"},
629	}
630
631	for _, tt := range tests {
632		t.Run(tt.name, func(t *testing.T) {
633			origin, err := ExtractOrigin(tt.uri)
634			if err == nil {
635				t.Errorf("ExtractOrigin(%q) should return error, got origin: %q", tt.uri, origin)
636			}
637		})
638	}
639}

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