v0.2.0 759cc92

validation_test.go RawHistoryFolder

1package parser
2
3import (
4	"testing"
5)
6
7func TestURLValidator_IsValidURL(t *testing.T) {
8	validator := NewURLValidator()
9
10	tests := []struct {
11		name     string
12		input    string
13		expected bool
14	}{
15		// Valid URLs with scheme
16		{"HTTPS URL", "https://example.com", true},
17		{"HTTP URL", "http://example.com", true},
18		{"URL with path", "https://example.com/path", true},
19		{"URL with query", "https://example.com?q=test", true},
20
21		// Valid domains
22		{"Simple domain", "example.com", true},
23		{"Subdomain", "www.example.com", true},
24		{"Multiple subdomains", "api.v1.example.com", true},
25		{"Domain with path", "example.com/path", true},
26
27		// Valid IP addresses
28		{"IPv4 address", "192.168.1.1", true},
29		{"Localhost IP", "127.0.0.1", true},
30
31		// Invalid inputs
32		{"Empty string", "", false},
33		{"Just text", "hello world", false},
34		{"Single word", "test", false},
35		{"Space in domain", "exam ple.com", false},
36		{"Invalid TLD", "example.toolongtobevalid", false},
37	}
38
39	for _, tt := range tests {
40		t.Run(tt.name, func(t *testing.T) {
41			result := validator.IsValidURL(tt.input)
42			if result != tt.expected {
43				t.Errorf("IsValidURL(%q) = %v, want %v", tt.input, result, tt.expected)
44			}
45		})
46	}
47}
48
49func TestURLValidator_IsDirectURL(t *testing.T) {
50	validator := NewURLValidator()
51
52	tests := []struct {
53		name     string
54		input    string
55		expected bool
56	}{
57		// Should be treated as direct URLs
58		{"Complete HTTPS URL", "https://github.com", true},
59		{"Complete HTTP URL", "http://example.com", true},
60		{"Domain only", "github.com", true},
61		{"Subdomain", "api.github.com", true},
62		{"Domain with path", "github.com/user/repo", true},
63		{"IP address", "192.168.1.1", true},
64		{"Localhost", "127.0.0.1", true},
65
66		// Should NOT be treated as direct URLs
67		{"Single word", "github", false},
68		{"Search query", "golang tutorial", false},
69		{"Shortcut format", "g: search query", false},
70		{"Empty string", "", false},
71		{"Just spaces", "   ", false},
72	}
73
74	for _, tt := range tests {
75		t.Run(tt.name, func(t *testing.T) {
76			result := validator.IsDirectURL(tt.input)
77			if result != tt.expected {
78				t.Errorf("IsDirectURL(%q) = %v, want %v", tt.input, result, tt.expected)
79			}
80		})
81	}
82}
83
84func TestURLValidator_IsSearchShortcut(t *testing.T) {
85	validator := NewURLValidator()
86
87	tests := []struct {
88		name           string
89		input          string
90		expectShortcut bool
91		expectKey      string
92		expectQuery    string
93	}{
94		{
95			name:           "Google shortcut",
96			input:          "g: golang tutorial",
97			expectShortcut: true,
98			expectKey:      "g",
99			expectQuery:    "golang tutorial",
100		},
101		{
102			name:           "GitHub shortcut",
103			input:          "gh: cobra cli",
104			expectShortcut: true,
105			expectKey:      "gh",
106			expectQuery:    "cobra cli",
107		},
108		{
109			name:           "Stack Overflow shortcut",
110			input:          "so: fuzzy search algorithm",
111			expectShortcut: true,
112			expectKey:      "so",
113			expectQuery:    "fuzzy search algorithm",
114		},
115		{
116			name:           "Empty query",
117			input:          "g: ",
118			expectShortcut: true,
119			expectKey:      "g",
120			expectQuery:    "",
121		},
122		{
123			name:           "Numeric shortcut",
124			input:          "123: test",
125			expectShortcut: true,
126			expectKey:      "123",
127			expectQuery:    "test",
128		},
129		// Not shortcuts
130		{
131			name:           "Just colon",
132			input:          ":",
133			expectShortcut: false,
134		},
135		{
136			name:           "No colon",
137			input:          "github search",
138			expectShortcut: false,
139		},
140		{
141			name:           "URL with colon",
142			input:          "https://example.com",
143			expectShortcut: false,
144		},
145		{
146			name:           "Empty string",
147			input:          "",
148			expectShortcut: false,
149		},
150	}
151
152	for _, tt := range tests {
153		t.Run(tt.name, func(t *testing.T) {
154			isShortcut, key, query := validator.IsSearchShortcut(tt.input)
155
156			if isShortcut != tt.expectShortcut {
157				t.Errorf("IsSearchShortcut(%q) isShortcut = %v, want %v", tt.input, isShortcut, tt.expectShortcut)
158			}
159
160			if tt.expectShortcut {
161				if key != tt.expectKey {
162					t.Errorf("IsSearchShortcut(%q) key = %q, want %q", tt.input, key, tt.expectKey)
163				}
164				if query != tt.expectQuery {
165					t.Errorf("IsSearchShortcut(%q) query = %q, want %q", tt.input, query, tt.expectQuery)
166				}
167			}
168		})
169	}
170}
171
172func TestURLValidator_NormalizeURL(t *testing.T) {
173	validator := NewURLValidator()
174
175	tests := []struct {
176		name     string
177		input    string
178		expected string
179	}{
180		// Already have scheme
181		{"HTTPS URL", "https://example.com", "https://example.com"},
182		{"HTTP URL", "http://example.com", "http://example.com"},
183
184		// Need scheme added
185		{"Domain only", "example.com", "https://example.com"},
186		{"Subdomain", "www.example.com", "https://www.example.com"},
187		{"Domain with path", "example.com/path", "https://example.com/path"},
188		{"IP address", "192.168.1.1", "https://192.168.1.1"},
189
190		// Special cases
191		{"Empty string", "", ""},
192		{"Just spaces", "   ", ""},
193		{"Non-URL text", "hello world", "hello world"}, // Should remain unchanged
194	}
195
196	for _, tt := range tests {
197		t.Run(tt.name, func(t *testing.T) {
198			result := validator.NormalizeURL(tt.input)
199			if result != tt.expected {
200				t.Errorf("NormalizeURL(%q) = %q, want %q", tt.input, result, tt.expected)
201			}
202		})
203	}
204}
205
206func TestURLValidator_ExtractDomain(t *testing.T) {
207	validator := NewURLValidator()
208
209	tests := []struct {
210		name     string
211		input    string
212		expected string
213	}{
214		// URLs with scheme
215		{"HTTPS URL", "https://github.com", "github.com"},
216		{"HTTP URL", "http://example.com", "example.com"},
217		{"URL with path", "https://github.com/user/repo", "github.com"},
218		{"URL with query", "https://google.com/search?q=test", "google.com"},
219		{"URL with port", "https://localhost:8080", "localhost"},
220
221		// Domains without scheme
222		{"Domain only", "github.com", "github.com"},
223		{"Subdomain", "api.github.com", "api.github.com"},
224		{"Domain with path", "github.com/user/repo", "github.com"},
225
226		// IP addresses
227		{"IPv4", "192.168.1.1", "192.168.1.1"},
228		{"IPv4 with port", "192.168.1.1:8080", "192.168.1.1"},
229
230		// Edge cases
231		{"Empty string", "", ""},
232		{"Just path", "/path/to/resource", ""}, // Invalid input returns empty
233	}
234
235	for _, tt := range tests {
236		t.Run(tt.name, func(t *testing.T) {
237			result := validator.ExtractDomain(tt.input)
238			if result != tt.expected {
239				t.Errorf("ExtractDomain(%q) = %q, want %q", tt.input, result, tt.expected)
240			}
241		})
242	}
243}
244
245func TestURLValidator_HasFileExtension(t *testing.T) {
246	validator := NewURLValidator()
247
248	tests := []struct {
249		name     string
250		input    string
251		expected bool
252	}{
253		// URLs with file extensions
254		{"HTML file", "https://example.com/index.html", true},
255		{"PDF file", "https://example.com/document.pdf", true},
256		{"Image file", "https://example.com/image.jpg", true},
257		{"File in subdirectory", "https://example.com/docs/readme.txt", true},
258
259		// URLs without file extensions
260		{"Root URL", "https://example.com", false},
261		{"Root with slash", "https://example.com/", false},
262		{"Directory path", "https://example.com/docs/", false},
263		{"Path without extension", "https://example.com/about", false},
264
265		// Edge cases
266		{"Dot at end", "https://example.com/file.", false},
267		{"Multiple dots", "https://example.com/file.name.ext", true},
268		{"Query params", "https://example.com/file.html?q=test", true},
269	}
270
271	for _, tt := range tests {
272		t.Run(tt.name, func(t *testing.T) {
273			result := validator.HasFileExtension(tt.input)
274			if result != tt.expected {
275				t.Errorf("HasFileExtension(%q) = %v, want %v", tt.input, result, tt.expected)
276			}
277		})
278	}
279}
280
281func TestURLValidator_IsLocalhost(t *testing.T) {
282	validator := NewURLValidator()
283
284	tests := []struct {
285		name     string
286		input    string
287		expected bool
288	}{
289		// Localhost variants
290		{"localhost", "http://localhost", true},
291		{"localhost with port", "http://localhost:3000", true},
292		{"localhost HTTPS", "https://localhost:8080", true},
293		{"127.0.0.1", "http://127.0.0.1", true},
294		{"127.0.0.1 with port", "http://127.0.0.1:8080", true},
295		{"IPv6 localhost", "http://[::1]", true},
296		{"Local domain", "http://app.local", true},
297
298		// Non-localhost
299		{"example.com", "https://example.com", false},
300		{"github.com", "https://github.com", false},
301		{"127.0.0.2", "http://127.0.0.2", false},     // Different local IP
302		{"192.168.1.1", "http://192.168.1.1", false}, // Private network but not localhost
303
304		// Edge cases
305		{"Empty string", "", false},
306		{"Just localhost", "localhost", true},
307		{"Just IP", "127.0.0.1", true},
308	}
309
310	for _, tt := range tests {
311		t.Run(tt.name, func(t *testing.T) {
312			result := validator.IsLocalhost(tt.input)
313			if result != tt.expected {
314				t.Errorf("IsLocalhost(%q) = %v, want %v", tt.input, result, tt.expected)
315			}
316		})
317	}
318}
319
320func TestURLValidator_SanitizeInput(t *testing.T) {
321	validator := NewURLValidator()
322
323	tests := []struct {
324		name     string
325		input    string
326		expected string
327	}{
328		{"Normal text", "github.com", "github.com"},
329		{"Text with spaces", "  github.com  ", "github.com"},
330		{"Text with tabs", "\tgithub.com\t", "github.com"},
331		{"Text with newlines", "\ngithub.com\n", "github.com"},
332		{"Unicode text", "github.com/ñoño", "github.com/ñoño"},
333		{"Empty string", "", ""},
334		{"Only whitespace", "   \t\n   ", ""},
335		{"Mixed whitespace and text", " \t github.com \n ", "github.com"},
336	}
337
338	for _, tt := range tests {
339		t.Run(tt.name, func(t *testing.T) {
340			result := validator.SanitizeInput(tt.input)
341			if result != tt.expected {
342				t.Errorf("SanitizeInput(%q) = %q, want %q", tt.input, result, tt.expected)
343			}
344		})
345	}
346}
347
348func TestURLValidator_GetURLType(t *testing.T) {
349	validator := NewURLValidator()
350
351	tests := []struct {
352		name     string
353		input    string
354		expected InputType
355	}{
356		// Direct URLs
357		{"HTTPS URL", "https://github.com", InputTypeDirectURL},
358		{"Domain", "github.com", InputTypeDirectURL},
359		{"IP address", "192.168.1.1", InputTypeDirectURL},
360
361		// Search shortcuts
362		{"Google shortcut", "g: golang tutorial", InputTypeSearchShortcut},
363		{"GitHub shortcut", "gh: cobra cli", InputTypeSearchShortcut},
364
365		// History search (everything else initially)
366		{"Single word", "github", InputTypeHistorySearch},
367		{"Search phrase", "golang tutorial", InputTypeHistorySearch},
368		{"Question", "how to use fuzzy search", InputTypeHistorySearch},
369		{"Empty string", "", InputTypeHistorySearch},
370	}
371
372	for _, tt := range tests {
373		t.Run(tt.name, func(t *testing.T) {
374			result := validator.GetURLType(tt.input)
375			if result != tt.expected {
376				t.Errorf("GetURLType(%q) = %v, want %v", tt.input, result, tt.expected)
377			}
378		})
379	}
380}
381
382func TestURLValidator_IsDomain(t *testing.T) {
383	validator := NewURLValidator()
384
385	tests := []struct {
386		name     string
387		input    string
388		expected bool
389	}{
390		// Valid domains
391		{"Simple domain", "example.com", true},
392		{"Subdomain", "www.example.com", true},
393		{"Multiple subdomains", "api.v1.example.com", true},
394		{"Country TLD", "example.co.uk", true},
395		{"New TLD", "example.dev", true},
396
397		// Invalid domains
398		{"Single word", "example", false},
399		{"No TLD", "www.example", false},
400		{"Starts with dot", ".example.com", false},
401		{"Ends with dot", "example.com.", false},
402		{"Contains spaces", "exam ple.com", false},
403		{"Too long segment", "verylongsubdomainthatexceedssixtyThreecharacterslimitfordomainsegments.com", false},
404		{"Empty string", "", false},
405	}
406
407	for _, tt := range tests {
408		t.Run(tt.name, func(t *testing.T) {
409			result := validator.isDomain(tt.input)
410			if result != tt.expected {
411				t.Errorf("isDomain(%q) = %v, want %v", tt.input, result, tt.expected)
412			}
413		})
414	}
415}
416
417func TestURLValidator_IsIPAddress(t *testing.T) {
418	validator := NewURLValidator()
419
420	tests := []struct {
421		name     string
422		input    string
423		expected bool
424	}{
425		// Valid IPv4
426		{"Standard IPv4", "192.168.1.1", true},
427		{"Localhost IPv4", "127.0.0.1", true},
428		{"Zero IPv4", "0.0.0.0", true},
429		{"Max IPv4", "255.255.255.255", true},
430
431		// Valid IPv6
432		{"Standard IPv6", "2001:db8::1", true},
433		{"Localhost IPv6", "::1", true},
434
435		// Invalid IPs
436		{"Invalid IPv4 (out of range)", "256.256.256.256", false},
437		{"Invalid IPv4 (too many octets)", "192.168.1.1.1", false},
438		{"Invalid IPv4 (too few octets)", "192.168.1", false},
439		{"Not an IP", "example.com", false},
440		{"Empty string", "", false},
441	}
442
443	for _, tt := range tests {
444		t.Run(tt.name, func(t *testing.T) {
445			result := validator.isIPAddress(tt.input)
446			if result != tt.expected {
447				t.Errorf("isIPAddress(%q) = %v, want %v", tt.input, result, tt.expected)
448			}
449		})
450	}
451}
452
453func TestURLValidator_LooksLikeDomain(t *testing.T) {
454	validator := NewURLValidator()
455
456	tests := []struct {
457		name     string
458		input    string
459		expected bool
460	}{
461		// Should look like domains
462		{"Standard domain", "example.com", true},
463		{"Subdomain", "www.example.com", true},
464		{"Uncommon TLD", "example.xyz", true},
465		{"Multiple levels", "a.b.c.example.com", true},
466
467		// Should not look like domains
468		{"No dot", "example", false},
469		{"Has space", "exam ple.com", false},
470		{"Too long", string(make([]rune, 300)), false}, // Very long string
471		{"Empty segment", "example..com", false},
472		{"Starts with non-alphanumeric", "-example.com", false},
473		{"Ends with non-alphanumeric", "example-.com", false},
474		{"Empty string", "", false},
475	}
476
477	for _, tt := range tests {
478		t.Run(tt.name, func(t *testing.T) {
479			result := validator.looksLikeDomain(tt.input)
480			if result != tt.expected {
481				t.Errorf("looksLikeDomain(%q) = %v, want %v", tt.input, result, tt.expected)
482			}
483		})
484	}
485}
486
487func BenchmarkURLValidator_IsValidURL(b *testing.B) {
488	validator := NewURLValidator()
489	testInputs := []string{
490		"https://github.com",
491		"github.com",
492		"192.168.1.1",
493		"g: search query",
494		"just some text",
495	}
496
497	b.ResetTimer()
498	for i := 0; i < b.N; i++ {
499		validator.IsValidURL(testInputs[i%len(testInputs)])
500	}
501}
502
503func BenchmarkURLValidator_IsDirectURL(b *testing.B) {
504	validator := NewURLValidator()
505	testInputs := []string{
506		"https://github.com",
507		"github.com",
508		"search query",
509		"g: shortcut",
510	}
511
512	b.ResetTimer()
513	for i := 0; i < b.N; i++ {
514		validator.IsDirectURL(testInputs[i%len(testInputs)])
515	}
516}
517
518func BenchmarkURLValidator_ExtractDomain(b *testing.B) {
519	validator := NewURLValidator()
520	testURL := "https://api.github.com/users/octocat/repos"
521
522	b.ResetTimer()
523	for i := 0; i < b.N; i++ {
524		validator.ExtractDomain(testURL)
525	}
526}

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