cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package handlers
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strconv"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/stormlightlabs/noteleaf/internal/models"
14 "github.com/stormlightlabs/noteleaf/internal/repo"
15 "github.com/stormlightlabs/noteleaf/internal/services"
16)
17
18func createTestTVHandler(t *testing.T) *TVHandler {
19 handler, err := NewTVHandler()
20 if err != nil {
21 t.Fatalf("Failed to create test TV handler: %v", err)
22 }
23 return handler
24}
25
26func createTestTVShow() *models.TVShow {
27 now := time.Now()
28 return &models.TVShow{
29 ID: 1,
30 Title: "Test TV Show",
31 Season: 1,
32 Status: "queued",
33 Rating: 4.5,
34 Notes: "Test notes",
35 Added: now,
36 }
37}
38
39func TestTVHandler(t *testing.T) {
40 t.Run("New", func(t *testing.T) {
41 handler := createTestTVHandler(t)
42 defer handler.Close()
43
44 if handler.db == nil {
45 t.Error("Expected database to be initialized")
46 }
47 if handler.config == nil {
48 t.Error("Expected config to be initialized")
49 }
50 if handler.repos == nil {
51 t.Error("Expected repositories to be initialized")
52 }
53 if handler.service == nil {
54 t.Error("Expected service to be initialized")
55 }
56 })
57
58 t.Run("Close", func(t *testing.T) {
59 handler := createTestTVHandler(t)
60
61 err := handler.Close()
62 if err != nil {
63 t.Errorf("Expected no error when closing handler, got: %v", err)
64 }
65 })
66
67 t.Run("Search and Add", func(t *testing.T) {
68 t.Run("Empty Query", func(t *testing.T) {
69 handler := createTestTVHandler(t)
70 defer handler.Close()
71
72 err := handler.SearchAndAdd(context.Background(), "", false)
73 if err == nil {
74 t.Error("Expected error for empty query")
75 }
76 if err.Error() != "search query cannot be empty" {
77 t.Errorf("Expected 'search query cannot be empty', got: %v", err)
78 }
79 })
80
81 t.Run("Context Cancellation During Search", func(t *testing.T) {
82 handler := createTestTVHandler(t)
83 defer handler.Close()
84
85 ctx, cancel := context.WithCancel(context.Background())
86 cancel()
87
88 err := handler.SearchAndAdd(ctx, "test tv show", false)
89 if err == nil {
90 t.Error("Expected error for cancelled context")
91 }
92 })
93
94 t.Run("Search Service Error", func(t *testing.T) {
95 handler := createTestTVHandler(t)
96 defer handler.Close()
97
98 mockFetcher := &MockMediaFetcher{
99 ShouldError: true,
100 ErrorMessage: "network error",
101 }
102
103 handler.service = CreateTestTVService(mockFetcher)
104
105 err := handler.SearchAndAdd(context.Background(), "test tv show", false)
106 if err == nil {
107 t.Error("Expected error when search service fails")
108 }
109
110 if !strings.Contains(err.Error(), "search failed") {
111 t.Errorf("Expected search failure error, got: %v", err)
112 }
113 })
114
115 t.Run("Empty Search Results", func(t *testing.T) {
116 handler := createTestTVHandler(t)
117 defer handler.Close()
118
119 mockFetcher := &MockMediaFetcher{SearchResults: []services.Media{}}
120
121 handler.service = CreateTestTVService(mockFetcher)
122
123 err := handler.SearchAndAdd(context.Background(), "nonexistent tv show", false)
124 if err != nil {
125 t.Errorf("Expected no error for empty results, got: %v", err)
126 }
127 })
128
129 t.Run("Search Results with No TV Shows", func(t *testing.T) {
130 handler := createTestTVHandler(t)
131 defer handler.Close()
132
133 mockFetcher := &MockMediaFetcher{
134 SearchResults: []services.Media{
135 {Title: "Test Movie", Link: "/m/test_movie", Type: "movie"},
136 },
137 }
138
139 handler.service = CreateTestTVService(mockFetcher)
140
141 if err := handler.SearchAndAdd(context.Background(), "movie title", false); err != nil {
142 t.Errorf("Expected no error for movie-only results, got: %v", err)
143 }
144 })
145
146 t.Run("Interactive Mode Path", func(t *testing.T) {
147 handler := createTestTVHandler(t)
148 defer handler.Close()
149
150 ctx := context.Background()
151 if _, err := handler.repos.TV.Create(ctx, &models.TVShow{
152 Title: "Test TV Show 1", Season: 1, Status: "queued",
153 }); err != nil {
154 t.Fatalf("Failed to create test TV show: %v", err)
155 }
156
157 if _, err := handler.repos.TV.Create(ctx, &models.TVShow{
158 Title: "Test TV Show 2", Season: 2, Status: "watching",
159 }); err != nil {
160 t.Fatalf("Failed to create test TV show: %v", err)
161 }
162
163 if err := TestTVInteractiveList(t, handler, ""); err != nil {
164 t.Errorf("Interactive TV list test failed: %v", err)
165 }
166 })
167
168 t.Run("successful search and add with user selection", func(t *testing.T) {
169 tempDir, err := os.MkdirTemp("", "noteleaf-tv-test-*")
170 if err != nil {
171 t.Fatalf("Failed to create temp dir: %v", err)
172 }
173 defer os.RemoveAll(tempDir)
174
175 oldNoteleafConfig := os.Getenv("NOTELEAF_CONFIG")
176 oldNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR")
177 os.Setenv("NOTELEAF_CONFIG", filepath.Join(tempDir, ".noteleaf.conf.toml"))
178 os.Setenv("NOTELEAF_DATA_DIR", tempDir)
179 defer func() {
180 os.Setenv("NOTELEAF_CONFIG", oldNoteleafConfig)
181 os.Setenv("NOTELEAF_DATA_DIR", oldNoteleafDataDir)
182 }()
183
184 ctx := context.Background()
185 err = Setup(ctx, []string{})
186 if err != nil {
187 t.Fatalf("Failed to setup database: %v", err)
188 }
189
190 handler, err := NewTVHandler()
191 if err != nil {
192 t.Fatalf("Failed to create handler: %v", err)
193 }
194 defer handler.Close()
195
196 mockFetcher := &MockMediaFetcher{
197 SearchResults: []services.Media{
198 {Title: "Test TV Show 1", Link: "/tv/test_show_1", Type: "tv", CriticScore: "90%"},
199 {Title: "Test TV Show 2", Link: "/tv/test_show_2", Type: "tv", CriticScore: "80%"},
200 },
201 }
202
203 handler.service = CreateTestTVService(mockFetcher)
204 handler.SetInputReader(MenuSelection(1))
205
206 if err = handler.SearchAndAdd(ctx, "test tv show", false); err != nil {
207 t.Errorf("Expected successful search and add, got error: %v", err)
208 }
209
210 shows, err := handler.repos.TV.List(ctx, repo.TVListOptions{})
211 if err != nil {
212 t.Fatalf("Failed to list TV shows: %v", err)
213 }
214 if len(shows) != 1 {
215 t.Errorf("Expected 1 TV show in database, got %d", len(shows))
216 }
217 if len(shows) > 0 && shows[0].Title != "Test TV Show 1" {
218 t.Errorf("Expected TV show title 'Test TV Show 1', got '%s'", shows[0].Title)
219 }
220 })
221
222 t.Run("successful search with user cancellation", func(t *testing.T) {
223 tempDir, err := os.MkdirTemp("", "noteleaf-tv-test-*")
224 if err != nil {
225 t.Fatalf("Failed to create temp dir: %v", err)
226 }
227 defer os.RemoveAll(tempDir)
228
229 oldNoteleafConfig := os.Getenv("NOTELEAF_CONFIG")
230 oldNoteleafDataDir := os.Getenv("NOTELEAF_DATA_DIR")
231 os.Setenv("NOTELEAF_CONFIG", filepath.Join(tempDir, ".noteleaf.conf.toml"))
232 os.Setenv("NOTELEAF_DATA_DIR", tempDir)
233 defer func() {
234 os.Setenv("NOTELEAF_CONFIG", oldNoteleafConfig)
235 os.Setenv("NOTELEAF_DATA_DIR", oldNoteleafDataDir)
236 }()
237
238 ctx := context.Background()
239 if err = Setup(ctx, []string{}); err != nil {
240 t.Fatalf("Failed to setup database: %v", err)
241 }
242
243 handler, err := NewTVHandler()
244 if err != nil {
245 t.Fatalf("Failed to create handler: %v", err)
246 }
247 defer handler.Close()
248
249 mockFetcher := &MockMediaFetcher{
250 SearchResults: []services.Media{
251 {Title: "Another TV Show", Link: "/tv/another_show", Type: "tv", CriticScore: "95%"},
252 },
253 }
254
255 handler.service = CreateTestTVService(mockFetcher)
256 handler.SetInputReader(MenuCancel())
257
258 if err = handler.SearchAndAdd(ctx, "another tv show", false); err != nil {
259 t.Errorf("Expected no error on cancellation, got: %v", err)
260 }
261
262 shows, err := handler.repos.TV.List(ctx, repo.TVListOptions{})
263 if err != nil {
264 t.Fatalf("Failed to list TV shows: %v", err)
265 }
266
267 expected := 0
268 if len(shows) != expected {
269 t.Errorf("Expected %d TV shows in database after cancellation, got %d", expected, len(shows))
270 }
271 })
272
273 t.Run("invalid user choice", func(t *testing.T) {
274 handler := createTestTVHandler(t)
275 defer handler.Close()
276
277 mockFetcher := &MockMediaFetcher{
278 SearchResults: []services.Media{
279 {Title: "Choice Test Show", Link: "/tv/choice_test", Type: "tv", CriticScore: "85%"},
280 },
281 }
282
283 handler.service = CreateTestTVService(mockFetcher)
284
285 handler.SetInputReader(MenuSelection(3))
286
287 err := handler.SearchAndAdd(context.Background(), "choice test", false)
288 if err == nil {
289 t.Error("Expected error for invalid choice")
290 }
291 if err != nil && !strings.Contains(err.Error(), "invalid choice") {
292 t.Errorf("Expected 'invalid choice' error, got: %v", err)
293 }
294 })
295
296 })
297
298 t.Run("List", func(t *testing.T) {
299 t.Run("Invalid Status", func(t *testing.T) {
300 handler := createTestTVHandler(t)
301 defer handler.Close()
302
303 err := handler.List(context.Background(), "invalid_status")
304 if err == nil {
305 t.Error("Expected error for invalid status")
306 }
307 if err.Error() != "invalid status: invalid_status (use: queued, watching, watched, or leave empty for all)" {
308 t.Errorf("Expected invalid status error, got: %v", err)
309 }
310 })
311
312 t.Run("All Shows", func(t *testing.T) {
313 handler := createTestTVHandler(t)
314 defer handler.Close()
315
316 err := handler.List(context.Background(), "")
317 if err != nil {
318 t.Errorf("Expected no error for listing all TV shows, got: %v", err)
319 }
320 })
321
322 t.Run("Queued Shows", func(t *testing.T) {
323 handler := createTestTVHandler(t)
324 defer handler.Close()
325
326 err := handler.List(context.Background(), "queued")
327 if err != nil {
328 t.Errorf("Expected no error for listing queued TV shows, got: %v", err)
329 }
330 })
331
332 t.Run("Watching Shows", func(t *testing.T) {
333 handler := createTestTVHandler(t)
334 defer handler.Close()
335
336 err := handler.List(context.Background(), "watching")
337 if err != nil {
338 t.Errorf("Expected no error for listing watching TV shows, got: %v", err)
339 }
340 })
341
342 t.Run("Watched Shows", func(t *testing.T) {
343 handler := createTestTVHandler(t)
344 defer handler.Close()
345
346 err := handler.List(context.Background(), "watched")
347 if err != nil {
348 t.Errorf("Expected no error for listing watched TV shows, got: %v", err)
349 }
350 })
351 })
352
353 t.Run("View", func(t *testing.T) {
354 t.Run("Show Not Found", func(t *testing.T) {
355 handler := createTestTVHandler(t)
356 defer handler.Close()
357
358 err := handler.View(context.Background(), "999")
359 if err == nil {
360 t.Error("Expected error for non-existent TV show")
361 }
362 })
363
364 t.Run("Invalid ID", func(t *testing.T) {
365 handler := createTestTVHandler(t)
366 defer handler.Close()
367
368 err := handler.View(context.Background(), "invalid")
369 if err == nil {
370 t.Error("Expected error for invalid TV show ID")
371 }
372 if err.Error() != "invalid TV show ID: invalid" {
373 t.Errorf("Expected 'invalid TV show ID: invalid', got: %v", err)
374 }
375 })
376 })
377
378 t.Run("Update", func(t *testing.T) {
379 t.Run("Update Status", func(t *testing.T) {
380 t.Run("Invalid", func(t *testing.T) {
381 handler := createTestTVHandler(t)
382 defer handler.Close()
383
384 err := handler.UpdateStatus(context.Background(), "1", "invalid")
385 if err == nil {
386 t.Error("Expected error for invalid status")
387 }
388 if err.Error() != "invalid status: invalid (valid: queued, watching, watched, removed)" {
389 t.Errorf("Expected invalid status error, got: %v", err)
390 }
391 })
392
393 t.Run("Show Not Found", func(t *testing.T) {
394 handler := createTestTVHandler(t)
395 defer handler.Close()
396
397 err := handler.UpdateStatus(context.Background(), "999", "watched")
398 if err == nil {
399 t.Error("Expected error for non-existent TV show")
400 }
401 })
402 })
403 })
404
405 t.Run("MarkWatching_ShowNotFound", func(t *testing.T) {
406 handler := createTestTVHandler(t)
407 defer handler.Close()
408
409 err := handler.MarkWatching(context.Background(), "999")
410 if err == nil {
411 t.Error("Expected error for non-existent TV show")
412 }
413 })
414
415 t.Run("MarkWatched_ShowNotFound", func(t *testing.T) {
416 handler := createTestTVHandler(t)
417 defer handler.Close()
418
419 err := handler.MarkWatched(context.Background(), "999")
420 if err == nil {
421 t.Error("Expected error for non-existent TV show")
422 }
423 })
424
425 t.Run("Remove_ShowNotFound", func(t *testing.T) {
426 handler := createTestTVHandler(t)
427 defer handler.Close()
428
429 err := handler.Remove(context.Background(), "999")
430 if err == nil {
431 t.Error("Expected error for non-existent TV show")
432 }
433 })
434
435 t.Run("UpdateTVShowStatus_InvalidID", func(t *testing.T) {
436 handler := createTestTVHandler(t)
437 defer handler.Close()
438
439 err := handler.UpdateTVShowStatus(context.Background(), "invalid", "watched")
440 if err == nil {
441 t.Error("Expected error for invalid TV show ID")
442 }
443 })
444
445 t.Run("MarkTVShowWatching_InvalidID", func(t *testing.T) {
446 handler := createTestTVHandler(t)
447 defer handler.Close()
448
449 err := handler.MarkTVShowWatching(context.Background(), "invalid")
450 if err == nil {
451 t.Error("Expected error for invalid TV show ID")
452 }
453 })
454
455 t.Run("MarkWatched_InvalidID", func(t *testing.T) {
456 handler := createTestTVHandler(t)
457 defer handler.Close()
458
459 err := handler.MarkWatched(context.Background(), "invalid")
460 if err == nil {
461 t.Error("Expected error for invalid TV show ID")
462 }
463 })
464
465 t.Run("Remove_InvalidID", func(t *testing.T) {
466 handler := createTestTVHandler(t)
467 defer handler.Close()
468
469 err := handler.Remove(context.Background(), "invalid")
470 if err == nil {
471 t.Error("Expected error for invalid TV show ID")
472 }
473 if err.Error() != "invalid TV show ID: invalid" {
474 t.Errorf("Expected 'invalid TV show ID: invalid', got: %v", err)
475 }
476 })
477
478 t.Run("print", func(t *testing.T) {
479 handler := createTestTVHandler(t)
480 defer handler.Close()
481
482 show := createTestTVShow()
483
484 handler.print(show)
485
486 minimalShow := &models.TVShow{
487 ID: 2,
488 Title: "Minimal Show",
489 }
490 handler.print(minimalShow)
491
492 watchedShow := &models.TVShow{
493 ID: 3,
494 Title: "Watched Show",
495 Season: 2,
496 Episode: 5,
497 Status: "watched",
498 Rating: 3.5,
499 }
500 handler.print(watchedShow)
501 })
502
503 t.Run("Integration", func(t *testing.T) {
504 t.Run("CreateAndRetrieve", func(t *testing.T) {
505 handler := createTestTVHandler(t)
506 defer handler.Close()
507
508 show := createTestTVShow()
509 show.ID = 0
510
511 id, err := handler.repos.TV.Create(context.Background(), show)
512 if err != nil {
513 t.Errorf("Failed to create TV show: %v", err)
514 return
515 }
516
517 err = handler.View(context.Background(), strconv.Itoa(int(id)))
518 if err != nil {
519 t.Errorf("Failed to view created TV show: %v", err)
520 }
521
522 err = handler.UpdateStatus(context.Background(), strconv.Itoa(int(id)), "watching")
523 if err != nil {
524 t.Errorf("Failed to update TV show status: %v", err)
525 }
526
527 err = handler.MarkWatched(context.Background(), strconv.Itoa(int(id)))
528 if err != nil {
529 t.Errorf("Failed to mark TV show as watched: %v", err)
530 }
531
532 err = handler.MarkWatching(context.Background(), strconv.Itoa(int(id)))
533 if err != nil {
534 t.Errorf("Failed to mark TV show as watching: %v", err)
535 }
536
537 err = handler.Remove(context.Background(), strconv.Itoa(int(id)))
538 if err != nil {
539 t.Errorf("Failed to remove TV show: %v", err)
540 }
541 })
542
543 t.Run("StatusFiltering", func(t *testing.T) {
544 handler := createTestTVHandler(t)
545 defer handler.Close()
546
547 queuedShow := &models.TVShow{
548 Title: "Queued Show",
549 Status: "queued",
550 Added: time.Now(),
551 }
552 watchingShow := &models.TVShow{
553 Title: "Watching Show",
554 Status: "watching",
555 Added: time.Now(),
556 }
557 watchedShow := &models.TVShow{
558 Title: "Watched Show",
559 Status: "watched",
560 Added: time.Now(),
561 }
562
563 id1, err := handler.repos.TV.Create(context.Background(), queuedShow)
564 if err != nil {
565 t.Errorf("Failed to create queued show: %v", err)
566 return
567 }
568 defer handler.repos.TV.Delete(context.Background(), id1)
569
570 id2, err := handler.repos.TV.Create(context.Background(), watchingShow)
571 if err != nil {
572 t.Errorf("Failed to create watching show: %v", err)
573 return
574 }
575 defer handler.repos.TV.Delete(context.Background(), id2)
576
577 id3, err := handler.repos.TV.Create(context.Background(), watchedShow)
578 if err != nil {
579 t.Errorf("Failed to create watched show: %v", err)
580 return
581 }
582 defer handler.repos.TV.Delete(context.Background(), id3)
583
584 testCases := []string{"", "queued", "watching", "watched"}
585 for _, status := range testCases {
586 err = handler.List(context.Background(), status)
587 if err != nil {
588 t.Errorf("Failed to list TV shows with status '%s': %v", status, err)
589 }
590 }
591 })
592 })
593
594 t.Run("ErrorPaths", func(t *testing.T) {
595 handler := createTestTVHandler(t)
596 defer handler.Close()
597
598 ctx := context.Background()
599 nonExistentID := int64(999999)
600
601 tt := []struct {
602 name string
603 fn func() error
604 }{
605 {
606 name: "View non-existent show",
607 fn: func() error { return handler.View(ctx, strconv.Itoa(int(nonExistentID))) },
608 },
609 {
610 name: "Update status of non-existent show",
611 fn: func() error { return handler.UpdateStatus(ctx, strconv.Itoa(int(nonExistentID)), "watched") },
612 },
613 {
614 name: "Mark non-existent show as watching",
615 fn: func() error { return handler.MarkWatching(ctx, strconv.Itoa(int(nonExistentID))) },
616 },
617 {
618 name: "Mark non-existent show as watched",
619 fn: func() error { return handler.MarkWatched(ctx, strconv.Itoa(int(nonExistentID))) },
620 },
621 {
622 name: "Remove non-existent show",
623 fn: func() error { return handler.Remove(ctx, strconv.Itoa(int(nonExistentID))) },
624 },
625 }
626
627 for _, tc := range tt {
628 t.Run(tc.name, func(t *testing.T) {
629 err := tc.fn()
630 if err == nil {
631 t.Errorf("Expected error for %s", tc.name)
632 }
633 })
634 }
635 })
636
637 t.Run("ValidStatusValues", func(t *testing.T) {
638 handler := createTestTVHandler(t)
639 defer handler.Close()
640
641 valid := []string{"queued", "watching", "watched", "removed"}
642 invalid := []string{"invalid", "pending", "completed", ""}
643
644 for _, status := range valid {
645 if err := handler.UpdateStatus(context.Background(), "999", status); err != nil &&
646 err.Error() == fmt.Sprintf("invalid status: %s (valid: queued, watching, watched, removed)", status) {
647 t.Errorf("Status '%s' should be valid but was rejected", status)
648 }
649 }
650
651 for _, status := range invalid {
652 err := handler.UpdateStatus(context.Background(), "1", status)
653 if err == nil {
654 t.Errorf("Status '%s' should be invalid but was accepted", status)
655 }
656 got := fmt.Sprintf("invalid status: %s (valid: queued, watching, watched, removed)", status)
657 if err.Error() != got {
658 t.Errorf("Expected '%s', got: %v", got, err)
659 }
660 }
661 })
662}