1package parser
2
3import (
4 "database/sql"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/config"
10 "github.com/bnema/dumber/internal/db"
11)
12
13// MockHistoryProvider implements HistoryProvider for testing.
14type MockHistoryProvider struct {
15 history []*db.History
16}
17
18func (m *MockHistoryProvider) GetRecentHistory(limit int) ([]*db.History, error) {
19 if limit >= len(m.history) {
20 return m.history, nil
21 }
22 return m.history[:limit], nil
23}
24
25func (m *MockHistoryProvider) GetAllHistory() ([]*db.History, error) {
26 return m.history, nil
27}
28
29func (m *MockHistoryProvider) SearchHistory(query string, limit int) ([]*db.History, error) {
30 // Simple mock search - return entries that contain the query in URL or title
31 result := make([]*db.History, 0)
32 queryLower := strings.ToLower(query)
33 for _, entry := range m.history {
34 if len(result) >= limit {
35 break
36 }
37 // Check if query matches URL or title
38 if strings.Contains(strings.ToLower(entry.Url), queryLower) ||
39 (entry.Title.Valid && strings.Contains(strings.ToLower(entry.Title.String), queryLower)) {
40 result = append(result, entry)
41 }
42 }
43 return result, nil
44}
45
46func (m *MockHistoryProvider) GetHistoryByURL(url string) (*db.History, error) {
47 for _, entry := range m.history {
48 if entry.Url == url {
49 return entry, nil
50 }
51 }
52 return nil, sql.ErrNoRows
53}
54
55// createMockHistory creates mock history data for testing.
56func createMockHistory() []*db.History {
57 now := time.Now()
58 return []*db.History{
59 {
60 ID: 1,
61 Url: "https://github.com",
62 Title: sql.NullString{String: "GitHub", Valid: true},
63 VisitCount: sql.NullInt64{Int64: 25, Valid: true},
64 LastVisited: sql.NullTime{Time: now.Add(-1 * time.Hour), Valid: true},
65 CreatedAt: sql.NullTime{Time: now.Add(-30 * 24 * time.Hour), Valid: true},
66 },
67 {
68 ID: 2,
69 Url: "https://stackoverflow.com",
70 Title: sql.NullString{String: "Stack Overflow", Valid: true},
71 VisitCount: sql.NullInt64{Int64: 15, Valid: true},
72 LastVisited: sql.NullTime{Time: now.Add(-2 * time.Hour), Valid: true},
73 CreatedAt: sql.NullTime{Time: now.Add(-20 * 24 * time.Hour), Valid: true},
74 },
75 {
76 ID: 3,
77 Url: "https://reddit.com",
78 Title: sql.NullString{String: "Reddit", Valid: true},
79 VisitCount: sql.NullInt64{Int64: 50, Valid: true},
80 LastVisited: sql.NullTime{Time: now.Add(-3 * time.Hour), Valid: true},
81 CreatedAt: sql.NullTime{Time: now.Add(-10 * 24 * time.Hour), Valid: true},
82 },
83 {
84 ID: 4,
85 Url: "https://golang.org",
86 Title: sql.NullString{String: "The Go Programming Language", Valid: true},
87 VisitCount: sql.NullInt64{Int64: 8, Valid: true},
88 LastVisited: sql.NullTime{Time: now.Add(-5 * time.Hour), Valid: true},
89 CreatedAt: sql.NullTime{Time: now.Add(-5 * 24 * time.Hour), Valid: true},
90 },
91 }
92}
93
94// createTestConfig creates a test configuration.
95func createTestConfig() *config.Config {
96 return &config.Config{
97 SearchShortcuts: map[string]config.SearchShortcut{
98 "g": {
99 URL: "https://www.google.com/search?q={query}",
100 Description: "Google Search",
101 },
102 "gh": {
103 URL: "https://github.com/search?q={query}",
104 Description: "GitHub Search",
105 },
106 "so": {
107 URL: "https://stackoverflow.com/search?q={query}",
108 Description: "Stack Overflow Search",
109 },
110 },
111 }
112}
113
114func TestParser_ParseInput_DirectURL(t *testing.T) {
115 mockHistory := createMockHistory()
116 provider := &MockHistoryProvider{history: mockHistory}
117 config := createTestConfig()
118 parser := NewParser(config, provider)
119
120 tests := []struct {
121 name string
122 input string
123 expectType InputType
124 expectURL string
125 expectConf float64
126 }{
127 {
128 name: "HTTPS URL",
129 input: "https://example.com",
130 expectType: InputTypeDirectURL,
131 expectURL: "https://example.com",
132 expectConf: 1.0,
133 },
134 {
135 name: "HTTP URL",
136 input: "http://example.com",
137 expectType: InputTypeDirectURL,
138 expectURL: "http://example.com",
139 expectConf: 1.0,
140 },
141 {
142 name: "Domain only",
143 input: "example.com",
144 expectType: InputTypeDirectURL,
145 expectURL: "https://example.com",
146 expectConf: 1.0,
147 },
148 {
149 name: "Subdomain",
150 input: "www.example.com",
151 expectType: InputTypeDirectURL,
152 expectURL: "https://www.example.com",
153 expectConf: 1.0,
154 },
155 {
156 name: "Domain with path",
157 input: "example.com/path",
158 expectType: InputTypeDirectURL,
159 expectURL: "https://example.com/path",
160 expectConf: 1.0,
161 },
162 }
163
164 for _, tt := range tests {
165 t.Run(tt.name, func(t *testing.T) {
166 result, err := parser.ParseInput(tt.input)
167 if err != nil {
168 t.Fatalf("ParseInput() error = %v", err)
169 }
170
171 if result.Type != tt.expectType {
172 t.Errorf("ParseInput() type = %v, want %v", result.Type, tt.expectType)
173 }
174
175 if result.URL != tt.expectURL {
176 t.Errorf("ParseInput() URL = %v, want %v", result.URL, tt.expectURL)
177 }
178
179 if result.Confidence != tt.expectConf {
180 t.Errorf("ParseInput() confidence = %v, want %v", result.Confidence, tt.expectConf)
181 }
182 })
183 }
184}
185
186func TestParser_ParseInput_SearchShortcut(t *testing.T) {
187 mockHistory := createMockHistory()
188 provider := &MockHistoryProvider{history: mockHistory}
189 config := createTestConfig()
190 parser := NewParser(config, provider)
191
192 tests := []struct {
193 name string
194 input string
195 expectType InputType
196 expectURL string
197 expectKey string
198 expectQuery string
199 }{
200 {
201 name: "Google search",
202 input: "g: golang tutorial",
203 expectType: InputTypeSearchShortcut,
204 expectURL: "https://www.google.com/search?q=golang+tutorial",
205 expectKey: "g",
206 expectQuery: "golang tutorial",
207 },
208 {
209 name: "GitHub search",
210 input: "gh: cobra cli",
211 expectType: InputTypeSearchShortcut,
212 expectURL: "https://github.com/search?q=cobra+cli",
213 expectKey: "gh",
214 expectQuery: "cobra cli",
215 },
216 {
217 name: "Stack Overflow search",
218 input: "so: how to fuzzy search",
219 expectType: InputTypeSearchShortcut,
220 expectURL: "https://stackoverflow.com/search?q=how+to+fuzzy+search",
221 expectKey: "so",
222 expectQuery: "how to fuzzy search",
223 },
224 }
225
226 for _, tt := range tests {
227 t.Run(tt.name, func(t *testing.T) {
228 result, err := parser.ParseInput(tt.input)
229 if err != nil {
230 t.Fatalf("ParseInput() error = %v", err)
231 }
232
233 if result.Type != tt.expectType {
234 t.Errorf("ParseInput() type = %v, want %v", result.Type, tt.expectType)
235 }
236
237 if result.URL != tt.expectURL {
238 t.Errorf("ParseInput() URL = %v, want %v", result.URL, tt.expectURL)
239 }
240
241 if result.Shortcut == nil {
242 t.Fatalf("ParseInput() shortcut is nil")
243 }
244
245 if result.Shortcut.Key != tt.expectKey {
246 t.Errorf("ParseInput() shortcut key = %v, want %v", result.Shortcut.Key, tt.expectKey)
247 }
248
249 if result.Shortcut.Query != tt.expectQuery {
250 t.Errorf("ParseInput() shortcut query = %v, want %v", result.Shortcut.Query, tt.expectQuery)
251 }
252 })
253 }
254}
255
256func TestParser_ParseInput_HistorySearch(t *testing.T) {
257 mockHistory := createMockHistory()
258 provider := &MockHistoryProvider{history: mockHistory}
259 config := createTestConfig()
260 parser := NewParser(config, provider)
261
262 tests := []struct {
263 name string
264 input string
265 expectType InputType
266 expectMatches bool
267 minConfidence float64
268 }{
269 {
270 name: "Exact domain match",
271 input: "github",
272 expectType: InputTypeHistorySearch,
273 expectMatches: true,
274 minConfidence: 0.5,
275 },
276 {
277 name: "Partial title match",
278 input: "stack",
279 expectType: InputTypeHistorySearch,
280 expectMatches: true,
281 minConfidence: 0.3,
282 },
283 {
284 name: "Programming language search",
285 input: "golang",
286 expectType: InputTypeHistorySearch,
287 expectMatches: true,
288 minConfidence: 0.3,
289 },
290 }
291
292 for _, tt := range tests {
293 t.Run(tt.name, func(t *testing.T) {
294 result, err := parser.ParseInput(tt.input)
295 if err != nil {
296 t.Fatalf("ParseInput() error = %v", err)
297 }
298
299 if result.Type != tt.expectType {
300 t.Errorf("ParseInput() type = %v, want %v", result.Type, tt.expectType)
301 }
302
303 if tt.expectMatches && len(result.FuzzyMatches) == 0 {
304 t.Errorf("ParseInput() expected fuzzy matches but got none")
305 }
306
307 if result.Confidence < tt.minConfidence {
308 t.Errorf("ParseInput() confidence = %v, want >= %v", result.Confidence, tt.minConfidence)
309 }
310 })
311 }
312}
313
314func TestParser_FuzzySearchHistory(t *testing.T) {
315 mockHistory := createMockHistory()
316 provider := &MockHistoryProvider{history: mockHistory}
317 config := createTestConfig()
318 parser := NewParser(config, provider)
319
320 tests := []struct {
321 name string
322 query string
323 threshold float64
324 expectMatches int
325 expectFirst string // Expected first match URL
326 }{
327 {
328 name: "GitHub search",
329 query: "github",
330 threshold: 0.3,
331 expectMatches: 1,
332 expectFirst: "https://github.com",
333 },
334 {
335 name: "Programming search",
336 query: "programming",
337 threshold: 0.2,
338 expectMatches: 1,
339 expectFirst: "https://golang.org",
340 },
341 {
342 name: "Stack search",
343 query: "stack",
344 threshold: 0.3,
345 expectMatches: 1,
346 expectFirst: "https://stackoverflow.com",
347 },
348 }
349
350 for _, tt := range tests {
351 t.Run(tt.name, func(t *testing.T) {
352 matches, err := parser.FuzzySearchHistory(tt.query, tt.threshold)
353 if err != nil {
354 t.Fatalf("FuzzySearchHistory() error = %v", err)
355 }
356
357 if len(matches) < tt.expectMatches {
358 t.Errorf("FuzzySearchHistory() matches = %d, want >= %d", len(matches), tt.expectMatches)
359 }
360
361 if len(matches) > 0 && tt.expectFirst != "" {
362 if matches[0].HistoryEntry.Url != tt.expectFirst {
363 t.Errorf("FuzzySearchHistory() first match = %v, want %v", matches[0].HistoryEntry.Url, tt.expectFirst)
364 }
365 }
366 })
367 }
368}
369
370func TestParser_SuggestCompletions(t *testing.T) {
371 mockHistory := createMockHistory()
372 provider := &MockHistoryProvider{history: mockHistory}
373 config := createTestConfig()
374 parser := NewParser(config, provider)
375
376 tests := []struct {
377 name string
378 input string
379 limit int
380 expectCount int
381 }{
382 {
383 name: "GitHub completion",
384 input: "gith",
385 limit: 5,
386 expectCount: 1,
387 },
388 {
389 name: "Shortcut completion",
390 input: "g",
391 limit: 5,
392 expectCount: 2, // "g" and "gh" shortcuts
393 },
394 {
395 name: "Empty input",
396 input: "",
397 limit: 5,
398 expectCount: 0,
399 },
400 }
401
402 for _, tt := range tests {
403 t.Run(tt.name, func(t *testing.T) {
404 completions, err := parser.SuggestCompletions(tt.input, tt.limit)
405 if err != nil {
406 t.Fatalf("SuggestCompletions() error = %v", err)
407 }
408
409 if len(completions) != tt.expectCount {
410 t.Errorf("SuggestCompletions() count = %d, want %d", len(completions), tt.expectCount)
411 }
412 })
413 }
414}
415
416func TestParser_ProcessShortcut(t *testing.T) {
417 config := createTestConfig()
418 mockHistory := createMockHistory()
419 provider := &MockHistoryProvider{history: mockHistory}
420 parser := NewParser(config, provider)
421
422 tests := []struct {
423 name string
424 shortcut string
425 query string
426 expectURL string
427 expectErr bool
428 }{
429 {
430 name: "Google shortcut",
431 shortcut: "g",
432 query: "test query",
433 expectURL: "https://www.google.com/search?q=test+query",
434 expectErr: false,
435 },
436 {
437 name: "GitHub shortcut",
438 shortcut: "gh",
439 query: "golang",
440 expectURL: "https://github.com/search?q=golang",
441 expectErr: false,
442 },
443 {
444 name: "Unknown shortcut",
445 shortcut: "unknown",
446 query: "test",
447 expectURL: "",
448 expectErr: true,
449 },
450 }
451
452 for _, tt := range tests {
453 t.Run(tt.name, func(t *testing.T) {
454 url, err := parser.ProcessShortcut(tt.shortcut, tt.query, config.SearchShortcuts)
455
456 if tt.expectErr {
457 if err == nil {
458 t.Errorf("ProcessShortcut() expected error but got none")
459 }
460 return
461 }
462
463 if err != nil {
464 t.Fatalf("ProcessShortcut() error = %v", err)
465 }
466
467 if url != tt.expectURL {
468 t.Errorf("ProcessShortcut() URL = %v, want %v", url, tt.expectURL)
469 }
470 })
471 }
472}
473
474func TestParser_ValidateConfig(t *testing.T) {
475 mockHistory := createMockHistory()
476 provider := &MockHistoryProvider{history: mockHistory}
477
478 tests := []struct {
479 name string
480 config *config.Config
481 fuzzy *FuzzyConfig
482 provider HistoryProvider
483 expectErr bool
484 }{
485 {
486 name: "Valid configuration",
487 config: createTestConfig(),
488 fuzzy: DefaultFuzzyConfig(),
489 provider: provider,
490 expectErr: false,
491 },
492 {
493 name: "Nil config",
494 config: nil,
495 fuzzy: DefaultFuzzyConfig(),
496 provider: provider,
497 expectErr: true,
498 },
499 {
500 name: "Nil fuzzy config",
501 config: createTestConfig(),
502 fuzzy: nil,
503 provider: provider,
504 expectErr: true,
505 },
506 {
507 name: "Nil history provider",
508 config: createTestConfig(),
509 fuzzy: DefaultFuzzyConfig(),
510 provider: nil,
511 expectErr: true,
512 },
513 }
514
515 for _, tt := range tests {
516 t.Run(tt.name, func(t *testing.T) {
517 parser := NewParser(tt.config, tt.provider, WithFuzzyConfig(tt.fuzzy))
518
519 err := parser.ValidateConfig()
520
521 if tt.expectErr {
522 if err == nil {
523 t.Errorf("ValidateConfig() expected error but got none")
524 }
525 } else {
526 if err != nil {
527 t.Errorf("ValidateConfig() unexpected error = %v", err)
528 }
529 }
530 })
531 }
532}
533
534func TestFuzzyConfig_IsValid(t *testing.T) {
535 tests := []struct {
536 name string
537 config *FuzzyConfig
538 expect bool
539 }{
540 {
541 name: "Default config",
542 config: DefaultFuzzyConfig(),
543 expect: true,
544 },
545 {
546 name: "Invalid threshold (negative)",
547 config: &FuzzyConfig{
548 MinSimilarityThreshold: -0.1,
549 MaxResults: 10,
550 URLWeight: 0.4,
551 TitleWeight: 0.3,
552 RecencyWeight: 0.2,
553 VisitWeight: 0.1,
554 RecencyDecayDays: 30,
555 },
556 expect: false,
557 },
558 {
559 name: "Invalid threshold (>1.0)",
560 config: &FuzzyConfig{
561 MinSimilarityThreshold: 1.1,
562 MaxResults: 10,
563 URLWeight: 0.4,
564 TitleWeight: 0.3,
565 RecencyWeight: 0.2,
566 VisitWeight: 0.1,
567 RecencyDecayDays: 30,
568 },
569 expect: false,
570 },
571 {
572 name: "Invalid MaxResults",
573 config: &FuzzyConfig{
574 MinSimilarityThreshold: 0.3,
575 MaxResults: 0,
576 URLWeight: 0.4,
577 TitleWeight: 0.3,
578 RecencyWeight: 0.2,
579 VisitWeight: 0.1,
580 RecencyDecayDays: 30,
581 },
582 expect: false,
583 },
584 }
585
586 for _, tt := range tests {
587 t.Run(tt.name, func(t *testing.T) {
588 result := tt.config.IsValid()
589 if result != tt.expect {
590 t.Errorf("FuzzyConfig.IsValid() = %v, want %v", result, tt.expect)
591 }
592 })
593 }
594}
595
596func TestInputType_String(t *testing.T) {
597 tests := []struct {
598 inputType InputType
599 expected string
600 }{
601 {InputTypeDirectURL, "direct_url"},
602 {InputTypeSearchShortcut, "search_shortcut"},
603 {InputTypeHistorySearch, "history_search"},
604 {InputTypeFallbackSearch, "fallback_search"},
605 {InputType(99), "unknown"}, // Unknown type
606 }
607
608 for _, tt := range tests {
609 t.Run(tt.expected, func(t *testing.T) {
610 result := tt.inputType.String()
611 if result != tt.expected {
612 t.Errorf("InputType.String() = %v, want %v", result, tt.expected)
613 }
614 })
615 }
616}