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}