cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 662 lines 18 kB view raw
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}