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