cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 2464 lines 72 kB view raw
1package handlers 2 3import ( 4 "bytes" 5 "context" 6 "os" 7 "runtime" 8 "slices" 9 "strconv" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/google/uuid" 15 "github.com/stormlightlabs/noteleaf/internal/models" 16 "github.com/stormlightlabs/noteleaf/internal/shared" 17 "github.com/stormlightlabs/noteleaf/internal/ui" 18) 19 20func TestTaskHandler(t *testing.T) { 21 ctx := context.Background() 22 t.Run("New", func(t *testing.T) { 23 t.Run("creates handler successfully", func(t *testing.T) { 24 suite := NewHandlerTestSuite(t) 25 defer suite.cleanup() 26 27 handler, err := NewTaskHandler() 28 if err != nil { 29 t.Fatalf("NewTaskHandler failed: %v", err) 30 } 31 if handler == nil { 32 t.Fatal("Handler should not be nil") 33 } 34 defer handler.Close() 35 36 if handler.db == nil { 37 t.Error("Handler database should not be nil") 38 } 39 if handler.config == nil { 40 t.Error("Handler config should not be nil") 41 } 42 if handler.repos == nil { 43 t.Error("Handler repos should not be nil") 44 } 45 }) 46 47 t.Run("handles database initialization error", func(t *testing.T) { 48 originalXDG := os.Getenv("XDG_CONFIG_HOME") 49 originalHome := os.Getenv("HOME") 50 51 if runtime.GOOS == "windows" { 52 originalAppData := os.Getenv("APPDATA") 53 os.Unsetenv("APPDATA") 54 defer os.Setenv("APPDATA", originalAppData) 55 } else { 56 os.Unsetenv("XDG_CONFIG_HOME") 57 os.Unsetenv("HOME") 58 defer os.Setenv("XDG_CONFIG_HOME", originalXDG) 59 defer os.Setenv("HOME", originalHome) 60 } 61 62 handler, err := NewTaskHandler() 63 if err == nil { 64 if handler != nil { 65 handler.Close() 66 } 67 t.Error("Expected error when database initialization fails") 68 } 69 }) 70 }) 71 72 t.Run("Create", func(t *testing.T) { 73 suite := NewHandlerTestSuite(t) 74 defer suite.cleanup() 75 76 handler := CreateHandler(t, NewTaskHandler) 77 78 t.Run("creates task successfully", func(t *testing.T) { 79 desc := "Buy groceries and cook dinner" 80 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{}) 81 shared.AssertNoError(t, err, "CreateTask should succeed") 82 83 tasks, err := handler.repos.Tasks.GetPending(ctx) 84 shared.AssertNoError(t, err, "Failed to get pending tasks") 85 86 if len(tasks) != 1 { 87 t.Errorf("Expected 1 task, got %d", len(tasks)) 88 } 89 90 task := tasks[0] 91 expectedDesc := "Buy groceries and cook dinner" 92 if task.Description != expectedDesc { 93 t.Errorf("Expected description '%s', got '%s'", expectedDesc, task.Description) 94 } 95 96 if task.Status != "pending" { 97 t.Errorf("Expected status 'pending', got '%s'", task.Status) 98 } 99 100 if task.UUID == "" { 101 t.Error("Task should have a UUID") 102 } 103 }) 104 105 t.Run("fails with empty description", func(t *testing.T) { 106 desc := "" 107 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{}) 108 shared.AssertError(t, err, "Expected error for empty description") 109 shared.AssertContains(t, err.Error(), "task description required", "Error message should mention required description") 110 }) 111 112 t.Run("creates task with flags", func(t *testing.T) { 113 description := "Task with flags" 114 priority := "A" 115 project := "test-project" 116 due := "2024-12-31" 117 tags := []string{"urgent", "work"} 118 119 err := handler.Create(ctx, description, priority, project, "test-context", due, "", "", "", "", "", "", tags) 120 if err != nil { 121 t.Errorf("CreateTask with flags failed: %v", err) 122 } 123 124 tasks, err := handler.repos.Tasks.GetPending(ctx) 125 if err != nil { 126 t.Fatalf("Failed to get pending tasks: %v", err) 127 } 128 129 if len(tasks) < 1 { 130 t.Errorf("Expected at least 1 task, got %d", len(tasks)) 131 } 132 133 var task *models.Task 134 for _, t := range tasks { 135 if t.Description == "Task with flags" { 136 task = t 137 break 138 } 139 } 140 141 if task == nil { 142 t.Fatal("Could not find created task") 143 } 144 145 if task.Priority != priority { 146 t.Errorf("Expected priority '%s', got '%s'", priority, task.Priority) 147 } 148 149 if task.Project != project { 150 t.Errorf("Expected project '%s', got '%s'", project, task.Project) 151 } 152 153 if task.Due == nil { 154 t.Error("Expected due date to be set") 155 } else if task.Due.Format("2006-01-02") != due { 156 t.Errorf("Expected due date '%s', got '%s'", due, task.Due.Format("2006-01-02")) 157 } 158 159 if len(task.Tags) != len(tags) { 160 t.Errorf("Expected %d tags, got %d", len(tags), len(task.Tags)) 161 } else { 162 for i, tag := range tags { 163 if task.Tags[i] != tag { 164 t.Errorf("Expected tag '%s' at index %d, got '%s'", tag, i, task.Tags[i]) 165 } 166 } 167 } 168 }) 169 170 t.Run("fails with invalid due date format", func(t *testing.T) { 171 desc := "Task with invalid date" 172 invalidDue := "invalid-date" 173 174 err := handler.Create(ctx, desc, "", "", "", invalidDue, "", "", "", "", "", "", []string{}) 175 if err == nil { 176 t.Error("Expected error for invalid due date format") 177 } 178 179 if !strings.Contains(err.Error(), "invalid due date format") { 180 t.Errorf("Expected error about invalid date format, got: %v", err) 181 } 182 }) 183 184 t.Run("fails when repository Create returns error", func(t *testing.T) { 185 ctx, cancel := context.WithCancel(ctx) 186 cancel() 187 188 err := handler.Create(ctx, "Test task", "", "", "", "", "", "", "", "", "", "", []string{}) 189 if err == nil { 190 t.Error("Expected error when repository Create fails") 191 } 192 193 if !strings.Contains(err.Error(), "failed to create task") { 194 t.Errorf("Expected 'failed to create task' error, got: %v", err) 195 } 196 }) 197 }) 198 199 t.Run("List", func(t *testing.T) { 200 suite := NewHandlerTestSuite(t) 201 defer suite.cleanup() 202 203 handler, err := NewTaskHandler() 204 if err != nil { 205 t.Fatalf("Failed to create handler: %v", err) 206 } 207 defer handler.Close() 208 209 task1 := &models.Task{ 210 UUID: uuid.New().String(), 211 Description: "Task 1", 212 Status: "pending", 213 Priority: "A", 214 Project: "work", 215 } 216 _, err = handler.repos.Tasks.Create(ctx, task1) 217 if err != nil { 218 t.Fatalf("Failed to create task1: %v", err) 219 } 220 221 task2 := &models.Task{ 222 UUID: uuid.New().String(), 223 Description: "Task 2", 224 Status: "completed", 225 } 226 _, err = handler.repos.Tasks.Create(ctx, task2) 227 if err != nil { 228 t.Fatalf("Failed to create task2: %v", err) 229 } 230 231 t.Run("lists pending tasks by default (static mode)", func(t *testing.T) { 232 err := handler.List(ctx, true, false, "", "", "", "", "") 233 if err != nil { 234 t.Errorf("ListTasks failed: %v", err) 235 } 236 }) 237 238 t.Run("filters by status (static mode)", func(t *testing.T) { 239 err := handler.List(ctx, true, false, "completed", "", "", "", "") 240 if err != nil { 241 t.Errorf("ListTasks with status filter failed: %v", err) 242 } 243 }) 244 245 t.Run("filters by priority (static mode)", func(t *testing.T) { 246 err := handler.List(ctx, true, false, "", "A", "", "", "") 247 if err != nil { 248 t.Errorf("ListTasks with priority filter failed: %v", err) 249 } 250 }) 251 252 t.Run("filters by project (static mode)", func(t *testing.T) { 253 err := handler.List(ctx, true, false, "", "", "work", "", "") 254 if err != nil { 255 t.Errorf("ListTasks with project filter failed: %v", err) 256 } 257 }) 258 259 t.Run("show all tasks (static mode)", func(t *testing.T) { 260 err := handler.List(ctx, true, true, "", "", "", "", "") 261 if err != nil { 262 t.Errorf("ListTasks with show all failed: %v", err) 263 } 264 }) 265 266 t.Run("interactive mode path", func(t *testing.T) { 267 if err := TestTaskInteractiveList(t, handler, false, "", "", ""); err != nil { 268 t.Errorf("Interactive task list test failed: %v", err) 269 } 270 }) 271 272 t.Run("interactive mode path with filters", func(t *testing.T) { 273 if err := TestTaskInteractiveList(t, handler, false, "pending", "A", "work"); err != nil { 274 t.Errorf("Interactive task list test with filters failed: %v", err) 275 } 276 }) 277 278 t.Run("interactive mode path show all", func(t *testing.T) { 279 if err := TestTaskInteractiveList(t, handler, true, "", "", ""); err != nil { 280 t.Errorf("Interactive task list test with show all failed: %v", err) 281 } 282 }) 283 }) 284 285 t.Run("Update", func(t *testing.T) { 286 suite := NewHandlerTestSuite(t) 287 defer suite.cleanup() 288 handler, err := NewTaskHandler() 289 if err != nil { 290 t.Fatalf("Failed to create handler: %v", err) 291 } 292 defer handler.Close() 293 294 task := &models.Task{ 295 UUID: uuid.New().String(), 296 Description: "Original description", 297 Status: "pending", 298 } 299 id, err := handler.repos.Tasks.Create(ctx, task) 300 if err != nil { 301 t.Fatalf("Failed to create task: %v", err) 302 } 303 304 t.Run("updates task by ID", func(t *testing.T) { 305 taskID := strconv.FormatInt(id, 10) 306 307 err := handler.Update(ctx, taskID, "Updated description", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") 308 if err != nil { 309 t.Errorf("UpdateTask failed: %v", err) 310 } 311 312 updatedTask, err := handler.repos.Tasks.Get(ctx, id) 313 if err != nil { 314 t.Fatalf("Failed to get updated task: %v", err) 315 } 316 317 if updatedTask.Description != "Updated description" { 318 t.Errorf("Expected description 'Updated description', got '%s'", updatedTask.Description) 319 } 320 }) 321 322 t.Run("updates task by UUID", func(t *testing.T) { 323 taskID := task.UUID 324 err := handler.Update(ctx, taskID, "", "completed", "", "", "", "", "", "", "", []string{}, []string{}, "", "") 325 if err != nil { 326 t.Errorf("UpdateTask by UUID failed: %v", err) 327 } 328 329 updatedTask, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID) 330 if err != nil { 331 t.Fatalf("Failed to get updated task by UUID: %v", err) 332 } 333 334 if updatedTask.Status != "completed" { 335 t.Errorf("Expected status 'completed', got '%s'", updatedTask.Status) 336 } 337 }) 338 339 t.Run("updates multiple fields", func(t *testing.T) { 340 taskID := strconv.FormatInt(id, 10) 341 err := handler.Update(ctx, taskID, "Multiple updates", "", "B", "test", "office", "2024-12-31", "", "", "", []string{}, []string{}, "", "") 342 if err != nil { 343 t.Errorf("UpdateTask with multiple fields failed: %v", err) 344 } 345 346 updatedTask, err := handler.repos.Tasks.Get(ctx, id) 347 if err != nil { 348 t.Fatalf("Failed to get updated task: %v", err) 349 } 350 351 if updatedTask.Description != "Multiple updates" { 352 t.Errorf("Expected description 'Multiple updates', got '%s'", updatedTask.Description) 353 } 354 if updatedTask.Priority != "B" { 355 t.Errorf("Expected priority 'B', got '%s'", updatedTask.Priority) 356 } 357 if updatedTask.Project != "test" { 358 t.Errorf("Expected project 'test', got '%s'", updatedTask.Project) 359 } 360 if updatedTask.Due == nil { 361 t.Error("Expected due date to be set") 362 } 363 }) 364 365 t.Run("adds and removes tags", func(t *testing.T) { 366 taskID := strconv.FormatInt(id, 10) 367 err := handler.Update(ctx, taskID, "", "", "", "", "", "", "", "", "", []string{"work", "urgent"}, []string{}, "", "") 368 if err != nil { 369 t.Errorf("UpdateTask with add tags failed: %v", err) 370 } 371 372 updatedTask, err := handler.repos.Tasks.Get(ctx, id) 373 if err != nil { 374 t.Fatalf("Failed to get updated task: %v", err) 375 } 376 377 if len(updatedTask.Tags) != 2 { 378 t.Errorf("Expected 2 tags, got %d", len(updatedTask.Tags)) 379 } 380 381 taskID = strconv.FormatInt(id, 10) 382 383 err = handler.Update(ctx, taskID, "", "", "", "", "", "", "", "", "", []string{}, []string{"urgent"}, "", "") 384 if err != nil { 385 t.Errorf("UpdateTask with remove tag failed: %v", err) 386 } 387 388 updatedTask, err = handler.repos.Tasks.Get(ctx, id) 389 if err != nil { 390 t.Fatalf("Failed to get updated task: %v", err) 391 } 392 393 if len(updatedTask.Tags) != 1 { 394 t.Errorf("Expected 1 tag after removal, got %d", len(updatedTask.Tags)) 395 } 396 397 if updatedTask.Tags[0] != "work" { 398 t.Errorf("Expected remaining tag 'work', got '%s'", updatedTask.Tags[0]) 399 } 400 }) 401 402 t.Run("fails with missing task ID", func(t *testing.T) { 403 err := handler.Update(ctx, "", "", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") 404 if err == nil { 405 t.Error("Expected error for missing task ID") 406 } 407 408 if !strings.Contains(err.Error(), "failed to find task") { 409 t.Errorf("Expected error about task not found, got: %v", err) 410 } 411 }) 412 413 t.Run("fails with invalid task ID", func(t *testing.T) { 414 taskID := "99999" 415 416 err := handler.Update(ctx, taskID, "test", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") 417 if err == nil { 418 t.Error("Expected error for invalid task ID") 419 } 420 421 if !strings.Contains(err.Error(), "failed to find task") { 422 t.Errorf("Expected error about task not found, got: %v", err) 423 } 424 }) 425 426 t.Run("fails when repository Get fails", func(t *testing.T) { 427 cancelCtx, cancel := context.WithCancel(context.Background()) 428 cancel() 429 430 err := handler.Update(cancelCtx, "1", "test", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") 431 if err == nil { 432 t.Error("Expected error when repository Get fails") 433 } 434 435 if !strings.Contains(err.Error(), "failed to find task") { 436 t.Errorf("Expected 'failed to find task' error, got: %v", err) 437 } 438 }) 439 440 t.Run("fails when repository operations fail with canceled context", func(t *testing.T) { 441 task := &models.Task{ 442 UUID: uuid.New().String(), 443 Description: "Test task", 444 Status: "pending", 445 } 446 id, err := handler.repos.Tasks.Create(ctx, task) 447 if err != nil { 448 t.Fatalf("Failed to create task: %v", err) 449 } 450 451 cancelCtx, cancel := context.WithCancel(context.Background()) 452 cancel() 453 454 taskID := strconv.FormatInt(id, 10) 455 err = handler.Update(cancelCtx, taskID, "Updated", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") 456 if err == nil { 457 t.Error("Expected error with canceled context") 458 } 459 }) 460 }) 461 462 t.Run("Delete", func(t *testing.T) { 463 suite := NewHandlerTestSuite(t) 464 defer suite.cleanup() 465 466 handler := CreateHandler(t, NewTaskHandler) 467 468 task := &models.Task{ 469 UUID: uuid.New().String(), 470 Description: "Task to delete", 471 Status: "pending", 472 } 473 id, err := handler.repos.Tasks.Create(ctx, task) 474 if err != nil { 475 t.Fatalf("Failed to create task: %v", err) 476 } 477 478 t.Run("deletes task by ID", func(t *testing.T) { 479 args := []string{strconv.FormatInt(id, 10)} 480 481 err := handler.Delete(ctx, args) 482 if err != nil { 483 t.Errorf("DeleteTask failed: %v", err) 484 } 485 486 _, err = handler.repos.Tasks.Get(ctx, id) 487 if err == nil { 488 t.Error("Expected error when getting deleted task") 489 } 490 }) 491 492 t.Run("deletes task by UUID", func(t *testing.T) { 493 task2 := &models.Task{ 494 UUID: uuid.New().String(), 495 Description: "Task to delete by UUID", 496 Status: "pending", 497 } 498 _, err := handler.repos.Tasks.Create(ctx, task2) 499 if err != nil { 500 t.Fatalf("Failed to create task2: %v", err) 501 } 502 503 args := []string{task2.UUID} 504 505 err = handler.Delete(ctx, args) 506 if err != nil { 507 t.Errorf("DeleteTask by UUID failed: %v", err) 508 } 509 510 _, err = handler.repos.Tasks.GetByUUID(ctx, task2.UUID) 511 if err == nil { 512 t.Error("Expected error when getting deleted task by UUID") 513 } 514 }) 515 516 t.Run("fails with missing task ID", func(t *testing.T) { 517 args := []string{} 518 519 err := handler.Delete(ctx, args) 520 if err == nil { 521 t.Error("Expected error for missing task ID") 522 } 523 524 if !strings.Contains(err.Error(), "task ID required") { 525 t.Errorf("Expected error about required task ID, got: %v", err) 526 } 527 }) 528 529 t.Run("fails with invalid task ID", func(t *testing.T) { 530 args := []string{"99999"} 531 532 err := handler.Delete(ctx, args) 533 if err == nil { 534 t.Error("Expected error for invalid task ID") 535 } 536 537 if !strings.Contains(err.Error(), "failed to find task") { 538 t.Errorf("Expected error about task not found, got: %v", err) 539 } 540 }) 541 }) 542 543 t.Run("View", func(t *testing.T) { 544 suite := NewHandlerTestSuite(t) 545 defer suite.cleanup() 546 547 handler, err := NewTaskHandler() 548 if err != nil { 549 t.Fatalf("Failed to create handler: %v", err) 550 } 551 defer handler.Close() 552 553 now := time.Now() 554 task := &models.Task{ 555 UUID: uuid.New().String(), 556 Description: "Task to view", 557 Status: "pending", 558 Priority: "A", 559 Project: "test", 560 Tags: []string{"work", "important"}, 561 Entry: now, 562 Modified: now, 563 } 564 id, err := handler.repos.Tasks.Create(ctx, task) 565 if err != nil { 566 t.Fatalf("Failed to create task: %v", err) 567 } 568 569 t.Run("views task by ID", func(t *testing.T) { 570 args := []string{strconv.FormatInt(id, 10)} 571 572 err := handler.View(ctx, args, "detailed", false, false) 573 if err != nil { 574 t.Errorf("ViewTask failed: %v", err) 575 } 576 }) 577 578 t.Run("views task by UUID", func(t *testing.T) { 579 args := []string{task.UUID} 580 581 err := handler.View(ctx, args, "detailed", false, false) 582 if err != nil { 583 t.Errorf("ViewTask by UUID failed: %v", err) 584 } 585 }) 586 587 t.Run("fails with missing task ID", func(t *testing.T) { 588 args := []string{} 589 590 err := handler.View(ctx, args, "detailed", false, false) 591 if err == nil { 592 t.Error("Expected error for missing task ID") 593 } 594 595 if !strings.Contains(err.Error(), "task ID required") { 596 t.Errorf("Expected error about required task ID, got: %v", err) 597 } 598 }) 599 600 t.Run("fails with invalid task ID", func(t *testing.T) { 601 args := []string{"99999"} 602 603 err := handler.View(ctx, args, "detailed", false, false) 604 if err == nil { 605 t.Error("Expected error for invalid task ID") 606 } 607 608 if !strings.Contains(err.Error(), "failed to find task") { 609 t.Errorf("Expected error about task not found, got: %v", err) 610 } 611 }) 612 613 t.Run("uses brief format", func(t *testing.T) { 614 args := []string{strconv.FormatInt(id, 10)} 615 err := handler.View(ctx, args, "brief", false, false) 616 if err != nil { 617 t.Errorf("ViewTask with brief format failed: %v", err) 618 } 619 }) 620 621 t.Run("hides metadata", func(t *testing.T) { 622 args := []string{strconv.FormatInt(id, 10)} 623 err := handler.View(ctx, args, "detailed", false, true) 624 if err != nil { 625 t.Errorf("ViewTask with no-metadata failed: %v", err) 626 } 627 }) 628 629 t.Run("outputs JSON", func(t *testing.T) { 630 args := []string{strconv.FormatInt(id, 10)} 631 err := handler.View(ctx, args, "detailed", true, false) 632 if err != nil { 633 t.Errorf("ViewTask with JSON output failed: %v", err) 634 } 635 }) 636 }) 637 638 t.Run("Done", func(t *testing.T) { 639 suite := NewHandlerTestSuite(t) 640 defer suite.cleanup() 641 642 handler, err := NewTaskHandler() 643 if err != nil { 644 t.Fatalf("Failed to create handler: %v", err) 645 } 646 defer handler.Close() 647 648 task := &models.Task{ 649 UUID: uuid.New().String(), 650 Description: "Task to complete", 651 Status: "pending", 652 } 653 id, err := handler.repos.Tasks.Create(ctx, task) 654 if err != nil { 655 t.Fatalf("Failed to create task: %v", err) 656 } 657 658 t.Run("marks task as done by ID", func(t *testing.T) { 659 args := []string{strconv.FormatInt(id, 10)} 660 661 err := handler.Done(ctx, args) 662 if err != nil { 663 t.Errorf("DoneTask failed: %v", err) 664 } 665 666 completedTask, err := handler.repos.Tasks.Get(ctx, id) 667 if err != nil { 668 t.Fatalf("Failed to get completed task: %v", err) 669 } 670 671 if completedTask.Status != "completed" { 672 t.Errorf("Expected status 'completed', got '%s'", completedTask.Status) 673 } 674 675 if completedTask.End == nil { 676 t.Error("Expected end time to be set") 677 } 678 }) 679 680 t.Run("handles already completed task", func(t *testing.T) { 681 task2 := &models.Task{ 682 UUID: uuid.New().String(), 683 Description: "Already completed task", 684 Status: "completed", 685 } 686 id2, err := handler.repos.Tasks.Create(ctx, task2) 687 if err != nil { 688 t.Fatalf("Failed to create task2: %v", err) 689 } 690 691 args := []string{strconv.FormatInt(id2, 10)} 692 693 err = handler.Done(ctx, args) 694 if err != nil { 695 t.Errorf("DoneTask on completed task failed: %v", err) 696 } 697 }) 698 699 t.Run("marks task as done by UUID", func(t *testing.T) { 700 task3 := &models.Task{ 701 UUID: uuid.New().String(), 702 Description: "Task to complete by UUID", 703 Status: "pending", 704 } 705 _, err := handler.repos.Tasks.Create(ctx, task3) 706 if err != nil { 707 t.Fatalf("Failed to create task3: %v", err) 708 } 709 710 args := []string{task3.UUID} 711 712 err = handler.Done(ctx, args) 713 if err != nil { 714 t.Errorf("DoneTask by UUID failed: %v", err) 715 } 716 717 completedTask, err := handler.repos.Tasks.GetByUUID(ctx, task3.UUID) 718 if err != nil { 719 t.Fatalf("Failed to get completed task by UUID: %v", err) 720 } 721 722 if completedTask.Status != "completed" { 723 t.Errorf("Expected status 'completed', got '%s'", completedTask.Status) 724 } 725 726 if completedTask.End == nil { 727 t.Error("Expected end time to be set") 728 } 729 }) 730 731 t.Run("fails with missing task ID", func(t *testing.T) { 732 args := []string{} 733 734 err := handler.Done(ctx, args) 735 if err == nil { 736 t.Error("Expected error for missing task ID") 737 } 738 739 if !strings.Contains(err.Error(), "task ID required") { 740 t.Errorf("Expected error about required task ID, got: %v", err) 741 } 742 }) 743 744 t.Run("fails with invalid task ID", func(t *testing.T) { 745 args := []string{"99999"} 746 747 err := handler.Done(ctx, args) 748 if err == nil { 749 t.Error("Expected error for invalid task ID") 750 } 751 752 if !strings.Contains(err.Error(), "failed to find task") { 753 t.Errorf("Expected error about task not found, got: %v", err) 754 } 755 }) 756 }) 757 758 t.Run("Helper", func(t *testing.T) { 759 t.Run("removeString function", func(t *testing.T) { 760 slice := []string{"a", "b", "c", "b"} 761 result := removeString(slice, "b") 762 763 if len(result) != 2 { 764 t.Errorf("Expected 2 items after removing 'b', got %d", len(result)) 765 } 766 767 if slices.Contains(result, "b") { 768 t.Error("Expected 'b' to be removed from slice") 769 } 770 771 if !slices.Contains(result, "a") || !slices.Contains(result, "c") { 772 t.Error("Expected 'a' and 'c' to remain in slice") 773 } 774 }) 775 776 t.Run("parseDescription extracts project", func(t *testing.T) { 777 parsed := parseDescription("Buy groceries +shopping") 778 779 if parsed.Description != "Buy groceries" { 780 t.Errorf("Expected description 'Buy groceries', got '%s'", parsed.Description) 781 } 782 if parsed.Project != "shopping" { 783 t.Errorf("Expected project 'shopping', got '%s'", parsed.Project) 784 } 785 }) 786 787 t.Run("parseDescription extracts context", func(t *testing.T) { 788 parsed := parseDescription("Call boss @work") 789 790 if parsed.Description != "Call boss" { 791 t.Errorf("Expected description 'Call boss', got '%s'", parsed.Description) 792 } 793 if parsed.Context != "work" { 794 t.Errorf("Expected context 'work', got '%s'", parsed.Context) 795 } 796 }) 797 798 t.Run("parseDescription extracts tags", func(t *testing.T) { 799 parsed := parseDescription("Fix bug #urgent #backend") 800 801 if parsed.Description != "Fix bug" { 802 t.Errorf("Expected description 'Fix bug', got '%s'", parsed.Description) 803 } 804 if len(parsed.Tags) != 2 { 805 t.Errorf("Expected 2 tags, got %d", len(parsed.Tags)) 806 } 807 if parsed.Tags[0] != "urgent" || parsed.Tags[1] != "backend" { 808 t.Errorf("Expected tags 'urgent' and 'backend', got %v", parsed.Tags) 809 } 810 }) 811 812 t.Run("parseDescription extracts due date", func(t *testing.T) { 813 parsed := parseDescription("Submit report due:2024-12-31") 814 815 if parsed.Description != "Submit report" { 816 t.Errorf("Expected description 'Submit report', got '%s'", parsed.Description) 817 } 818 if parsed.Due != "2024-12-31" { 819 t.Errorf("Expected due '2024-12-31', got '%s'", parsed.Due) 820 } 821 }) 822 823 t.Run("parseDescription extracts recurrence", func(t *testing.T) { 824 parsed := parseDescription("Weekly meeting recur:FREQ=WEEKLY") 825 826 if parsed.Description != "Weekly meeting" { 827 t.Errorf("Expected description 'Weekly meeting', got '%s'", parsed.Description) 828 } 829 if parsed.Recur != "FREQ=WEEKLY" { 830 t.Errorf("Expected recur 'FREQ=WEEKLY', got '%s'", parsed.Recur) 831 } 832 }) 833 834 t.Run("parseDescription extracts until date", func(t *testing.T) { 835 parsed := parseDescription("Daily standup until:2024-12-31") 836 837 if parsed.Description != "Daily standup" { 838 t.Errorf("Expected description 'Daily standup', got '%s'", parsed.Description) 839 } 840 if parsed.Until != "2024-12-31" { 841 t.Errorf("Expected until '2024-12-31', got '%s'", parsed.Until) 842 } 843 }) 844 845 t.Run("parseDescription extracts parent UUID", func(t *testing.T) { 846 parentUUID := "550e8400-e29b-41d4-a716-446655440000" 847 text := "Subtask parent:" + parentUUID 848 parsed := parseDescription(text) 849 850 if parsed.Description != "Subtask" { 851 t.Errorf("Expected description 'Subtask', got '%s'", parsed.Description) 852 } 853 if parsed.ParentUUID != parentUUID { 854 t.Errorf("Expected parent UUID '%s', got '%s'", parentUUID, parsed.ParentUUID) 855 } 856 }) 857 858 t.Run("parseDescription extracts dependencies", func(t *testing.T) { 859 uuid1 := "550e8400-e29b-41d4-a716-446655440000" 860 uuid2 := "660e8400-e29b-41d4-a716-446655440001" 861 text := "Task with deps depends:" + uuid1 + "," + uuid2 862 parsed := parseDescription(text) 863 864 if parsed.Description != "Task with deps" { 865 t.Errorf("Expected description 'Task with deps', got '%s'", parsed.Description) 866 } 867 if len(parsed.DependsOn) != 2 { 868 t.Errorf("Expected 2 dependencies, got %d", len(parsed.DependsOn)) 869 } 870 if parsed.DependsOn[0] != uuid1 || parsed.DependsOn[1] != uuid2 { 871 t.Errorf("Expected dependencies [%s, %s], got %v", uuid1, uuid2, parsed.DependsOn) 872 } 873 }) 874 875 t.Run("parseDescription extracts all metadata", func(t *testing.T) { 876 text := "Complex task +project @context #tag1 #tag2 due:2024-12-31 recur:FREQ=DAILY until:2025-01-31" 877 parsed := parseDescription(text) 878 879 if parsed.Description != "Complex task" { 880 t.Errorf("Expected description 'Complex task', got '%s'", parsed.Description) 881 } 882 if parsed.Project != "project" { 883 t.Errorf("Expected project 'project', got '%s'", parsed.Project) 884 } 885 if parsed.Context != "context" { 886 t.Errorf("Expected context 'context', got '%s'", parsed.Context) 887 } 888 if len(parsed.Tags) != 2 { 889 t.Errorf("Expected 2 tags, got %d", len(parsed.Tags)) 890 } 891 if parsed.Due != "2024-12-31" { 892 t.Errorf("Expected due '2024-12-31', got '%s'", parsed.Due) 893 } 894 if parsed.Recur != "FREQ=DAILY" { 895 t.Errorf("Expected recur 'FREQ=DAILY', got '%s'", parsed.Recur) 896 } 897 if parsed.Until != "2025-01-31" { 898 t.Errorf("Expected until '2025-01-31', got '%s'", parsed.Until) 899 } 900 }) 901 902 t.Run("parseDescription handles plain text without metadata", func(t *testing.T) { 903 parsed := parseDescription("Just a simple task") 904 905 if parsed.Description != "Just a simple task" { 906 t.Errorf("Expected description 'Just a simple task', got '%s'", parsed.Description) 907 } 908 if parsed.Project != "" { 909 t.Errorf("Expected empty project, got '%s'", parsed.Project) 910 } 911 if parsed.Context != "" { 912 t.Errorf("Expected empty context, got '%s'", parsed.Context) 913 } 914 if len(parsed.Tags) != 0 { 915 t.Errorf("Expected no tags, got %d", len(parsed.Tags)) 916 } 917 }) 918 }) 919 920 t.Run("Print", func(t *testing.T) { 921 suite := NewHandlerTestSuite(t) 922 defer suite.cleanup() 923 924 handler, err := NewTaskHandler() 925 if err != nil { 926 t.Fatalf("Failed to create handler: %v", err) 927 } 928 defer handler.Close() 929 930 now := time.Now() 931 due := now.Add(24 * time.Hour) 932 933 task := &models.Task{ 934 ID: 1, 935 UUID: uuid.New().String(), 936 Description: "Test task", 937 Status: "pending", 938 Priority: "A", 939 Project: "test", 940 Tags: []string{"work", "urgent"}, 941 Due: &due, 942 Entry: now, 943 Modified: now, 944 } 945 946 t.Run("printTask outputs basic fields", func(t *testing.T) { 947 var buf bytes.Buffer 948 oldStdout := os.Stdout 949 r, w, _ := os.Pipe() 950 os.Stdout = w 951 952 outputChan := make(chan string, 1) 953 go func() { 954 buf.ReadFrom(r) 955 outputChan <- buf.String() 956 }() 957 958 printTask(task) 959 w.Close() 960 os.Stdout = oldStdout 961 output := <-outputChan 962 963 if !strings.Contains(output, "Test task") { 964 t.Error("Output should contain task description") 965 } 966 if !strings.Contains(output, "[A]") { 967 t.Error("Output should contain priority") 968 } 969 if !strings.Contains(output, "+test") { 970 t.Error("Output should contain project") 971 } 972 }) 973 974 t.Run("printTask outputs context", func(t *testing.T) { 975 taskWithContext := &models.Task{ 976 ID: 1, 977 UUID: uuid.New().String(), 978 Description: "Test task", 979 Status: "pending", 980 Context: "work", 981 } 982 983 var buf bytes.Buffer 984 oldStdout := os.Stdout 985 r, w, _ := os.Pipe() 986 os.Stdout = w 987 988 outputChan := make(chan string, 1) 989 go func() { 990 buf.ReadFrom(r) 991 outputChan <- buf.String() 992 }() 993 994 printTask(taskWithContext) 995 w.Close() 996 os.Stdout = oldStdout 997 output := <-outputChan 998 999 if !strings.Contains(output, "@work") { 1000 t.Error("Output should contain context") 1001 } 1002 }) 1003 1004 t.Run("printTask outputs recur indicator", func(t *testing.T) { 1005 taskWithRecur := &models.Task{ 1006 ID: 1, 1007 UUID: uuid.New().String(), 1008 Description: "Recurring task", 1009 Status: "pending", 1010 Recur: "FREQ=DAILY", 1011 } 1012 1013 var buf bytes.Buffer 1014 oldStdout := os.Stdout 1015 r, w, _ := os.Pipe() 1016 os.Stdout = w 1017 1018 outputChan := make(chan string, 1) 1019 go func() { 1020 buf.ReadFrom(r) 1021 outputChan <- buf.String() 1022 }() 1023 1024 printTask(taskWithRecur) 1025 w.Close() 1026 os.Stdout = oldStdout 1027 output := <-outputChan 1028 1029 if !strings.Contains(output, "\u21bb") { 1030 t.Error("Output should contain recurrence indicator") 1031 } 1032 }) 1033 1034 t.Run("printTask outputs dependency count", func(t *testing.T) { 1035 taskWithDeps := &models.Task{ 1036 ID: 1, 1037 UUID: uuid.New().String(), 1038 Description: "Task with dependencies", 1039 Status: "pending", 1040 DependsOn: []string{"uuid1", "uuid2"}, 1041 } 1042 1043 var buf bytes.Buffer 1044 oldStdout := os.Stdout 1045 r, w, _ := os.Pipe() 1046 os.Stdout = w 1047 1048 outputChan := make(chan string, 1) 1049 go func() { 1050 buf.ReadFrom(r) 1051 outputChan <- buf.String() 1052 }() 1053 1054 printTask(taskWithDeps) 1055 w.Close() 1056 os.Stdout = oldStdout 1057 output := <-outputChan 1058 1059 if !strings.Contains(output, "\u29372") { 1060 t.Error("Output should contain dependency count") 1061 } 1062 }) 1063 1064 t.Run("printTaskDetail outputs context", func(t *testing.T) { 1065 taskWithContext := &models.Task{ 1066 ID: 1, 1067 UUID: uuid.New().String(), 1068 Description: "Test task", 1069 Status: "pending", 1070 Context: "office", 1071 Entry: now, 1072 Modified: now, 1073 } 1074 1075 var buf bytes.Buffer 1076 oldStdout := os.Stdout 1077 r, w, _ := os.Pipe() 1078 os.Stdout = w 1079 1080 outputChan := make(chan string, 1) 1081 go func() { 1082 buf.ReadFrom(r) 1083 outputChan <- buf.String() 1084 }() 1085 1086 printTaskDetail(taskWithContext, false) 1087 w.Close() 1088 os.Stdout = oldStdout 1089 output := <-outputChan 1090 1091 if !strings.Contains(output, "Context: office") { 1092 t.Error("Output should contain context field") 1093 } 1094 }) 1095 1096 t.Run("printTaskDetail outputs recurrence", func(t *testing.T) { 1097 taskWithRecur := &models.Task{ 1098 ID: 1, 1099 UUID: uuid.New().String(), 1100 Description: "Recurring task", 1101 Status: "pending", 1102 Recur: "FREQ=WEEKLY", 1103 Entry: now, 1104 Modified: now, 1105 } 1106 1107 var buf bytes.Buffer 1108 oldStdout := os.Stdout 1109 r, w, _ := os.Pipe() 1110 os.Stdout = w 1111 1112 outputChan := make(chan string, 1) 1113 go func() { 1114 buf.ReadFrom(r) 1115 outputChan <- buf.String() 1116 }() 1117 1118 printTaskDetail(taskWithRecur, false) 1119 w.Close() 1120 os.Stdout = oldStdout 1121 output := <-outputChan 1122 1123 if !strings.Contains(output, "Recurrence: FREQ=WEEKLY") { 1124 t.Error("Output should contain recurrence field") 1125 } 1126 }) 1127 1128 t.Run("printTaskDetail outputs until date", func(t *testing.T) { 1129 until := now.Add(30 * 24 * time.Hour) 1130 taskWithUntil := &models.Task{ 1131 ID: 1, 1132 UUID: uuid.New().String(), 1133 Description: "Task with until", 1134 Status: "pending", 1135 Until: &until, 1136 Entry: now, 1137 Modified: now, 1138 } 1139 1140 var buf bytes.Buffer 1141 oldStdout := os.Stdout 1142 r, w, _ := os.Pipe() 1143 os.Stdout = w 1144 1145 outputChan := make(chan string, 1) 1146 go func() { 1147 buf.ReadFrom(r) 1148 outputChan <- buf.String() 1149 }() 1150 1151 printTaskDetail(taskWithUntil, false) 1152 w.Close() 1153 os.Stdout = oldStdout 1154 output := <-outputChan 1155 1156 if !strings.Contains(output, "Recur Until:") { 1157 t.Error("Output should contain recur until field") 1158 } 1159 }) 1160 1161 t.Run("printTaskDetail outputs parent UUID", func(t *testing.T) { 1162 parentUUID := "550e8400-e29b-41d4-a716-446655440000" 1163 taskWithParent := &models.Task{ 1164 ID: 1, 1165 UUID: uuid.New().String(), 1166 Description: "Subtask", 1167 Status: "pending", 1168 ParentUUID: &parentUUID, 1169 Entry: now, 1170 Modified: now, 1171 } 1172 1173 var buf bytes.Buffer 1174 oldStdout := os.Stdout 1175 r, w, _ := os.Pipe() 1176 os.Stdout = w 1177 1178 outputChan := make(chan string, 1) 1179 go func() { 1180 buf.ReadFrom(r) 1181 outputChan <- buf.String() 1182 }() 1183 1184 printTaskDetail(taskWithParent, false) 1185 w.Close() 1186 os.Stdout = oldStdout 1187 output := <-outputChan 1188 1189 if !strings.Contains(output, "Parent Task:") { 1190 t.Error("Output should contain parent task field") 1191 } 1192 if !strings.Contains(output, parentUUID) { 1193 t.Error("Output should contain parent UUID") 1194 } 1195 }) 1196 1197 t.Run("printTaskDetail outputs dependencies", func(t *testing.T) { 1198 taskWithDeps := &models.Task{ 1199 ID: 1, 1200 UUID: uuid.New().String(), 1201 Description: "Task with deps", 1202 Status: "pending", 1203 DependsOn: []string{"uuid1", "uuid2"}, 1204 Entry: now, 1205 Modified: now, 1206 } 1207 1208 var buf bytes.Buffer 1209 oldStdout := os.Stdout 1210 r, w, _ := os.Pipe() 1211 os.Stdout = w 1212 1213 outputChan := make(chan string, 1) 1214 go func() { 1215 buf.ReadFrom(r) 1216 outputChan <- buf.String() 1217 }() 1218 1219 printTaskDetail(taskWithDeps, false) 1220 w.Close() 1221 os.Stdout = oldStdout 1222 output := <-outputChan 1223 1224 if !strings.Contains(output, "Depends On:") { 1225 t.Error("Output should contain depends on field") 1226 } 1227 if !strings.Contains(output, "uuid1") || !strings.Contains(output, "uuid2") { 1228 t.Error("Output should contain dependency UUIDs") 1229 } 1230 }) 1231 1232 t.Run("printTaskDetail outputs start time", func(t *testing.T) { 1233 start := now.Add(-1 * time.Hour) 1234 taskWithStart := &models.Task{ 1235 ID: 1, 1236 UUID: uuid.New().String(), 1237 Description: "Started task", 1238 Status: "pending", 1239 Start: &start, 1240 Entry: now, 1241 Modified: now, 1242 } 1243 1244 var buf bytes.Buffer 1245 oldStdout := os.Stdout 1246 r, w, _ := os.Pipe() 1247 os.Stdout = w 1248 1249 outputChan := make(chan string, 1) 1250 go func() { 1251 buf.ReadFrom(r) 1252 outputChan <- buf.String() 1253 }() 1254 1255 printTaskDetail(taskWithStart, false) 1256 w.Close() 1257 os.Stdout = oldStdout 1258 output := <-outputChan 1259 1260 if !strings.Contains(output, "Started:") { 1261 t.Error("Output should contain started field") 1262 } 1263 }) 1264 1265 t.Run("printTaskDetail outputs end time", func(t *testing.T) { 1266 end := now.Add(-1 * time.Hour) 1267 taskWithEnd := &models.Task{ 1268 ID: 1, 1269 UUID: uuid.New().String(), 1270 Description: "Completed task", 1271 Status: "completed", 1272 End: &end, 1273 Entry: now, 1274 Modified: now, 1275 } 1276 1277 var buf bytes.Buffer 1278 oldStdout := os.Stdout 1279 r, w, _ := os.Pipe() 1280 os.Stdout = w 1281 1282 outputChan := make(chan string, 1) 1283 go func() { 1284 buf.ReadFrom(r) 1285 outputChan <- buf.String() 1286 }() 1287 1288 printTaskDetail(taskWithEnd, false) 1289 w.Close() 1290 os.Stdout = oldStdout 1291 output := <-outputChan 1292 1293 if !strings.Contains(output, "Completed:") { 1294 t.Error("Output should contain completed field") 1295 } 1296 }) 1297 1298 t.Run("printTaskDetail outputs annotations", func(t *testing.T) { 1299 taskWithAnnotations := &models.Task{ 1300 ID: 1, 1301 UUID: uuid.New().String(), 1302 Description: "Task with notes", 1303 Status: "pending", 1304 Annotations: []string{"Note 1", "Note 2"}, 1305 Entry: now, 1306 Modified: now, 1307 } 1308 1309 var buf bytes.Buffer 1310 oldStdout := os.Stdout 1311 r, w, _ := os.Pipe() 1312 os.Stdout = w 1313 1314 outputChan := make(chan string, 1) 1315 go func() { 1316 buf.ReadFrom(r) 1317 outputChan <- buf.String() 1318 }() 1319 1320 printTaskDetail(taskWithAnnotations, false) 1321 w.Close() 1322 os.Stdout = oldStdout 1323 output := <-outputChan 1324 1325 if !strings.Contains(output, "Annotations:") { 1326 t.Error("Output should contain annotations field") 1327 } 1328 if !strings.Contains(output, "Note 1") || !strings.Contains(output, "Note 2") { 1329 t.Error("Output should contain annotation texts") 1330 } 1331 }) 1332 }) 1333 1334 t.Run("ListProjects", func(t *testing.T) { 1335 suite := NewHandlerTestSuite(t) 1336 defer suite.cleanup() 1337 1338 handler, err := NewTaskHandler() 1339 if err != nil { 1340 t.Fatalf("Failed to create handler: %v", err) 1341 } 1342 defer handler.Close() 1343 1344 tasks := []*models.Task{ 1345 {UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Project: "web-app"}, 1346 {UUID: uuid.New().String(), Description: "Task 2", Status: "completed", Project: "web-app"}, 1347 {UUID: uuid.New().String(), Description: "Task 3", Status: "pending", Project: "mobile-app"}, 1348 {UUID: uuid.New().String(), Description: "Task 4", Status: "pending", Project: ""}, 1349 } 1350 1351 for _, task := range tasks { 1352 _, err := handler.repos.Tasks.Create(ctx, task) 1353 if err != nil { 1354 t.Fatalf("Failed to create task: %v", err) 1355 } 1356 } 1357 1358 t.Run("lists projects successfully", func(t *testing.T) { 1359 err := handler.ListProjects(ctx, true) 1360 if err != nil { 1361 t.Errorf("ListProjects failed: %v", err) 1362 } 1363 }) 1364 1365 t.Run("returns no projects when none exist", func(t *testing.T) { 1366 suite := NewHandlerTestSuite(t) 1367 defer suite.cleanup() 1368 1369 err := handler.ListProjects(ctx, true) 1370 if err != nil { 1371 t.Errorf("ListProjects with no projects failed: %v", err) 1372 } 1373 }) 1374 1375 t.Run("fails when repository List fails", func(t *testing.T) { 1376 cancelCtx, cancel := context.WithCancel(context.Background()) 1377 cancel() 1378 1379 err := handler.ListProjects(cancelCtx, true) 1380 if err == nil { 1381 t.Error("Expected error when repository List fails") 1382 } 1383 1384 if !strings.Contains(err.Error(), "failed to list tasks for projects") { 1385 t.Errorf("Expected 'failed to list tasks for projects' error, got: %v", err) 1386 } 1387 }) 1388 }) 1389 1390 t.Run("ListTags", func(t *testing.T) { 1391 suite := NewHandlerTestSuite(t) 1392 defer suite.cleanup() 1393 1394 handler, err := NewTaskHandler() 1395 if err != nil { 1396 t.Fatalf("Failed to create handler: %v", err) 1397 } 1398 defer handler.Close() 1399 1400 tasks := []*models.Task{ 1401 {UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Tags: []string{"frontend", "urgent"}}, 1402 {UUID: uuid.New().String(), Description: "Task 2", Status: "completed", Tags: []string{"backend", "database"}}, 1403 {UUID: uuid.New().String(), Description: "Task 3", Status: "pending", Tags: []string{"frontend", "ios"}}, 1404 {UUID: uuid.New().String(), Description: "Task 4", Status: "pending", Tags: []string{}}, 1405 } 1406 1407 for _, task := range tasks { 1408 _, err := handler.repos.Tasks.Create(ctx, task) 1409 if err != nil { 1410 t.Fatalf("Failed to create task: %v", err) 1411 } 1412 } 1413 1414 t.Run("lists tags successfully", func(t *testing.T) { 1415 err := handler.ListTags(ctx, true) 1416 if err != nil { 1417 t.Errorf("ListTags failed: %v", err) 1418 } 1419 }) 1420 1421 t.Run("returns no tags when none exist", func(t *testing.T) { 1422 suite := NewHandlerTestSuite(t) 1423 defer suite.cleanup() 1424 1425 err := handler.ListTags(ctx, true) 1426 if err != nil { 1427 t.Errorf("ListTags with no tags failed: %v", err) 1428 } 1429 }) 1430 1431 t.Run("fails when repository List fails", func(t *testing.T) { 1432 cancelCtx, cancel := context.WithCancel(context.Background()) 1433 cancel() 1434 1435 err := handler.ListTags(cancelCtx, true) 1436 if err == nil { 1437 t.Error("Expected error when repository List fails") 1438 } 1439 1440 if !strings.Contains(err.Error(), "failed to list tasks for tags") { 1441 t.Errorf("Expected 'failed to list tasks for tags' error, got: %v", err) 1442 } 1443 }) 1444 }) 1445 1446 t.Run("Pluralize", func(t *testing.T) { 1447 t.Run("returns empty string for singular", func(t *testing.T) { 1448 result := pluralize(1) 1449 if result != "" { 1450 t.Errorf("Expected empty string for 1, got '%s'", result) 1451 } 1452 }) 1453 1454 t.Run("returns 's' for plural", func(t *testing.T) { 1455 result := pluralize(0) 1456 if result != "s" { 1457 t.Errorf("Expected 's' for 0, got '%s'", result) 1458 } 1459 1460 result = pluralize(2) 1461 if result != "s" { 1462 t.Errorf("Expected 's' for 2, got '%s'", result) 1463 } 1464 1465 result = pluralize(10) 1466 if result != "s" { 1467 t.Errorf("Expected 's' for 10, got '%s'", result) 1468 } 1469 }) 1470 }) 1471 1472 t.Run("InteractiveComponentsStatic", func(t *testing.T) { 1473 suite := NewHandlerTestSuite(t) 1474 defer suite.cleanup() 1475 1476 handler, err := NewTaskHandler() 1477 if err != nil { 1478 t.Fatalf("Failed to create task handler: %v", err) 1479 } 1480 defer handler.Close() 1481 1482 err = handler.Create(ctx, "Test Task 1", "high", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag1"}) 1483 if err != nil { 1484 t.Fatalf("Failed to create test task: %v", err) 1485 } 1486 1487 err = handler.Create(ctx, "Test Task 2", "medium", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag2"}) 1488 if err != nil { 1489 t.Fatalf("Failed to create test task: %v", err) 1490 } 1491 1492 t.Run("taskListStaticMode", func(t *testing.T) { 1493 var output bytes.Buffer 1494 1495 t.Run("lists all tasks", func(t *testing.T) { 1496 output.Reset() 1497 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, true, "", "", "") 1498 err := taskTable.Browse(ctx) 1499 if err != nil { 1500 t.Errorf("Static task list should succeed: %v", err) 1501 } 1502 if !strings.Contains(output.String(), "Test Task 1") { 1503 t.Error("Output should contain Test Task 1") 1504 } 1505 if !strings.Contains(output.String(), "Test Task 2") { 1506 t.Error("Output should contain Test Task 2") 1507 } 1508 }) 1509 1510 t.Run("filters by status", func(t *testing.T) { 1511 output.Reset() 1512 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "pending", "", "") 1513 err := taskTable.Browse(ctx) 1514 if err != nil { 1515 t.Errorf("Static task list with status filter should succeed: %v", err) 1516 } 1517 }) 1518 1519 t.Run("filters by priority", func(t *testing.T) { 1520 output.Reset() 1521 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "high", "") 1522 err := taskTable.Browse(ctx) 1523 if err != nil { 1524 t.Errorf("Static task list with priority filter should succeed: %v", err) 1525 } 1526 }) 1527 1528 t.Run("filters by project", func(t *testing.T) { 1529 output.Reset() 1530 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "", "test-project") 1531 err := taskTable.Browse(ctx) 1532 if err != nil { 1533 t.Errorf("Static task list with project filter should succeed: %v", err) 1534 } 1535 }) 1536 }) 1537 1538 t.Run("projectListStaticMode", func(t *testing.T) { 1539 var output bytes.Buffer 1540 1541 t.Run("lists projects", func(t *testing.T) { 1542 output.Reset() 1543 projectTable := ui.NewProjectListFromTable(handler.repos.Tasks, &output, os.Stdin, true) 1544 err := projectTable.Browse(ctx) 1545 if err != nil { 1546 t.Errorf("Static project list should succeed: %v", err) 1547 } 1548 if !strings.Contains(output.String(), "test-project") { 1549 t.Error("Output should contain test-project") 1550 } 1551 }) 1552 }) 1553 1554 t.Run("tagListStaticMode", func(t *testing.T) { 1555 var output bytes.Buffer 1556 1557 t.Run("lists tags", func(t *testing.T) { 1558 output.Reset() 1559 tagTable := ui.NewTagListFromTable(handler.repos.Tasks, &output, os.Stdin, true) 1560 err := tagTable.Browse(ctx) 1561 if err != nil { 1562 t.Errorf("Static tag list should succeed: %v", err) 1563 } 1564 if !strings.Contains(output.String(), "tag1") { 1565 t.Error("Output should contain tag1") 1566 } 1567 }) 1568 }) 1569 1570 t.Run("contextListStaticMode", func(t *testing.T) { 1571 oldStdout := os.Stdout 1572 defer func() { os.Stdout = oldStdout }() 1573 1574 r, w, _ := os.Pipe() 1575 os.Stdout = w 1576 1577 outputChan := make(chan string, 1) 1578 go func() { 1579 var buf bytes.Buffer 1580 buf.ReadFrom(r) 1581 outputChan <- buf.String() 1582 }() 1583 1584 t.Run("lists contexts with tasks", func(t *testing.T) { 1585 err := handler.listContextsStatic(ctx, false) 1586 w.Close() 1587 capturedOutput := <-outputChan 1588 1589 if err != nil { 1590 t.Errorf("listContextsStatic should succeed: %v", err) 1591 } 1592 if !strings.Contains(capturedOutput, "test-context") { 1593 t.Error("Output should contain 'test-context' context") 1594 } 1595 }) 1596 1597 r, w, _ = os.Pipe() 1598 os.Stdout = w 1599 go func() { 1600 var buf bytes.Buffer 1601 buf.ReadFrom(r) 1602 outputChan <- buf.String() 1603 }() 1604 1605 t.Run("lists contexts with todo.txt format", func(t *testing.T) { 1606 err := handler.listContextsStatic(ctx, true) 1607 w.Close() 1608 capturedOutput := <-outputChan 1609 1610 if err != nil { 1611 t.Errorf("listContextsStatic with todoTxt should succeed: %v", err) 1612 } 1613 if !strings.Contains(capturedOutput, "@test-context") { 1614 t.Error("Output should contain '@test-context' in todo.txt format") 1615 } 1616 }) 1617 1618 t.Run("handles no contexts", func(t *testing.T) { 1619 suite := NewHandlerTestSuite(t) 1620 defer suite.cleanup() 1621 1622 handler2, err := NewTaskHandler() 1623 if err != nil { 1624 t.Fatalf("Failed to create handler: %v", err) 1625 } 1626 defer handler2.Close() 1627 1628 r, w, _ = os.Pipe() 1629 os.Stdout = w 1630 go func() { 1631 var buf bytes.Buffer 1632 buf.ReadFrom(r) 1633 outputChan <- buf.String() 1634 }() 1635 1636 err = handler2.listContextsStatic(ctx, false) 1637 w.Close() 1638 capturedOutput := <-outputChan 1639 1640 if err != nil { 1641 t.Errorf("listContextsStatic with no contexts should succeed: %v", err) 1642 } 1643 if !strings.Contains(capturedOutput, "No contexts found") { 1644 t.Error("Output should contain 'No contexts found'") 1645 } 1646 }) 1647 1648 t.Run("handles repository error", func(t *testing.T) { 1649 cancelCtx, cancel := context.WithCancel(ctx) 1650 cancel() 1651 1652 err := handler.listContextsStatic(cancelCtx, false) 1653 if err == nil { 1654 t.Error("Expected error with cancelled context") 1655 } 1656 if !strings.Contains(err.Error(), "failed to list tasks for contexts") { 1657 t.Errorf("Expected specific error message, got: %v", err) 1658 } 1659 }) 1660 1661 t.Run("counts tasks per context correctly", func(t *testing.T) { 1662 r, w, _ = os.Pipe() 1663 os.Stdout = w 1664 go func() { 1665 var buf bytes.Buffer 1666 buf.ReadFrom(r) 1667 outputChan <- buf.String() 1668 }() 1669 1670 err := handler.listContextsStatic(ctx, false) 1671 w.Close() 1672 capturedOutput := <-outputChan 1673 1674 if err != nil { 1675 t.Errorf("listContextsStatic should succeed: %v", err) 1676 } 1677 1678 if !strings.Contains(capturedOutput, "test-context (2 tasks)") { 1679 t.Error("Output should show correct count for test-context context") 1680 } 1681 }) 1682 }) 1683 }) 1684 1685 t.Run("ListContexts", func(t *testing.T) { 1686 suite := NewHandlerTestSuite(t) 1687 defer suite.cleanup() 1688 1689 handler, err := NewTaskHandler() 1690 if err != nil { 1691 t.Fatalf("Failed to create handler: %v", err) 1692 } 1693 defer handler.Close() 1694 1695 tasks := []*models.Task{ 1696 {UUID: uuid.New().String(), Description: "Task with context 1", Status: "pending", Context: "test-context"}, 1697 {UUID: uuid.New().String(), Description: "Task with context 2", Status: "pending", Context: "work-context"}, 1698 {UUID: uuid.New().String(), Description: "Task without context", Status: "pending"}, 1699 } 1700 1701 for _, task := range tasks { 1702 _, err := handler.repos.Tasks.Create(ctx, task) 1703 if err != nil { 1704 t.Fatalf("Failed to create task: %v", err) 1705 } 1706 } 1707 1708 t.Run("lists contexts in static mode", func(t *testing.T) { 1709 err := handler.ListContexts(ctx, true) 1710 if err != nil { 1711 t.Errorf("ListContexts static mode failed: %v", err) 1712 } 1713 }) 1714 1715 t.Run("lists contexts in interactive mode (falls back to static)", func(t *testing.T) { 1716 err := handler.ListContexts(ctx, false) 1717 if err != nil { 1718 t.Errorf("ListContexts interactive mode failed: %v", err) 1719 } 1720 }) 1721 1722 t.Run("lists contexts with todoTxt flag true", func(t *testing.T) { 1723 err := handler.ListContexts(ctx, true, true) 1724 if err != nil { 1725 t.Errorf("ListContexts with todoTxt=true failed: %v", err) 1726 } 1727 }) 1728 1729 t.Run("lists contexts with todoTxt flag false", func(t *testing.T) { 1730 err := handler.ListContexts(ctx, true, false) 1731 if err != nil { 1732 t.Errorf("ListContexts with todoTxt=false failed: %v", err) 1733 } 1734 }) 1735 1736 t.Run("handles database error in static mode", func(t *testing.T) { 1737 cancelCtx, cancel := context.WithCancel(ctx) 1738 cancel() 1739 1740 err := handler.ListContexts(cancelCtx, true) 1741 if err == nil { 1742 t.Error("Expected error with cancelled context in static mode") 1743 } 1744 if !strings.Contains(err.Error(), "failed to list tasks for contexts") { 1745 t.Errorf("Expected specific error message, got: %v", err) 1746 } 1747 }) 1748 1749 t.Run("handles database error in interactive mode", func(t *testing.T) { 1750 cancelCtx, cancel := context.WithCancel(ctx) 1751 cancel() 1752 1753 err := handler.ListContexts(cancelCtx, false) 1754 if err == nil { 1755 t.Error("Expected error with cancelled context in interactive mode") 1756 } 1757 if !strings.Contains(err.Error(), "failed to list tasks for contexts") { 1758 t.Errorf("Expected specific error message, got: %v", err) 1759 } 1760 }) 1761 1762 t.Run("returns no contexts when none exist", func(t *testing.T) { 1763 suite := NewHandlerTestSuite(t) 1764 defer suite.cleanup() 1765 1766 handler_, err := NewTaskHandler() 1767 if err != nil { 1768 t.Fatalf("Failed to create handler: %v", err) 1769 } 1770 defer handler_.Close() 1771 1772 err = handler_.ListContexts(ctx, true) 1773 if err != nil { 1774 t.Errorf("ListContexts with no contexts failed: %v", err) 1775 } 1776 }) 1777 }) 1778 1779 t.Run("SetRecur", func(t *testing.T) { 1780 suite := NewHandlerTestSuite(t) 1781 defer suite.cleanup() 1782 1783 handler, err := NewTaskHandler() 1784 if err != nil { 1785 t.Fatalf("Failed to create handler: %v", err) 1786 } 1787 defer handler.Close() 1788 1789 id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1790 UUID: uuid.New().String(), Description: "Test task", Status: "pending", 1791 }) 1792 if err != nil { 1793 t.Fatalf("Failed to create task: %v", err) 1794 } 1795 1796 t.Run("sets recurrence rule", func(t *testing.T) { 1797 err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=DAILY", "2025-12-31") 1798 if err != nil { 1799 t.Errorf("SetRecur failed: %v", err) 1800 } 1801 1802 task, err := handler.repos.Tasks.Get(ctx, id) 1803 if err != nil { 1804 t.Fatalf("Failed to get task: %v", err) 1805 } 1806 1807 if task.Recur != "FREQ=DAILY" { 1808 t.Errorf("Expected recur to be 'FREQ=DAILY', got '%s'", task.Recur) 1809 } 1810 1811 if task.Until == nil { 1812 t.Error("Expected until to be set") 1813 } 1814 }) 1815 1816 t.Run("handles invalid until date", func(t *testing.T) { 1817 err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=WEEKLY", "invalid-date") 1818 if err == nil { 1819 t.Error("Expected error for invalid until date") 1820 } 1821 }) 1822 1823 t.Run("fails when repository Get fails", func(t *testing.T) { 1824 cancelCtx, cancel := context.WithCancel(context.Background()) 1825 cancel() 1826 1827 err := handler.SetRecur(cancelCtx, "1", "FREQ=DAILY", "") 1828 if err == nil { 1829 t.Error("Expected error when repository Get fails") 1830 } 1831 1832 if !strings.Contains(err.Error(), "failed to find task") { 1833 t.Errorf("Expected 'failed to find task' error, got: %v", err) 1834 } 1835 }) 1836 1837 t.Run("fails with canceled context", func(t *testing.T) { 1838 task := &models.Task{ 1839 UUID: uuid.New().String(), 1840 Description: "Test task", 1841 Status: "pending", 1842 } 1843 id, err := handler.repos.Tasks.Create(ctx, task) 1844 if err != nil { 1845 t.Fatalf("Failed to create task: %v", err) 1846 } 1847 1848 cancelCtx, cancel := context.WithCancel(context.Background()) 1849 cancel() 1850 1851 err = handler.SetRecur(cancelCtx, strconv.FormatInt(id, 10), "FREQ=DAILY", "") 1852 if err == nil { 1853 t.Error("Expected error with canceled context") 1854 } 1855 }) 1856 }) 1857 1858 t.Run("ClearRecur", func(t *testing.T) { 1859 suite := NewHandlerTestSuite(t) 1860 defer suite.cleanup() 1861 1862 handler, err := NewTaskHandler() 1863 if err != nil { 1864 t.Fatalf("Failed to create handler: %v", err) 1865 } 1866 defer handler.Close() 1867 1868 until := time.Now() 1869 id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1870 UUID: uuid.New().String(), 1871 Description: "Test task", 1872 Status: "pending", 1873 Recur: "FREQ=DAILY", 1874 Until: &until, 1875 }) 1876 if err != nil { 1877 t.Fatalf("Failed to create task: %v", err) 1878 } 1879 1880 if err = handler.ClearRecur(ctx, strconv.FormatInt(id, 10)); err != nil { 1881 t.Errorf("ClearRecur failed: %v", err) 1882 } 1883 1884 task, err := handler.repos.Tasks.Get(ctx, id) 1885 if err != nil { 1886 t.Fatalf("Failed to get task: %v", err) 1887 } 1888 1889 if task.Recur != "" { 1890 t.Errorf("Expected recur to be cleared, got '%s'", task.Recur) 1891 } 1892 1893 if task.Until != nil { 1894 t.Error("Expected until to be cleared") 1895 } 1896 1897 t.Run("fails when repository Get fails", func(t *testing.T) { 1898 cancelCtx, cancel := context.WithCancel(context.Background()) 1899 cancel() 1900 1901 err := handler.ClearRecur(cancelCtx, "1") 1902 if err == nil { 1903 t.Error("Expected error when repository Get fails") 1904 } 1905 1906 if !strings.Contains(err.Error(), "failed to find task") { 1907 t.Errorf("Expected 'failed to find task' error, got: %v", err) 1908 } 1909 }) 1910 1911 t.Run("fails with canceled context", func(t *testing.T) { 1912 task := &models.Task{ 1913 UUID: uuid.New().String(), 1914 Description: "Test task", 1915 Status: "pending", 1916 Recur: "FREQ=DAILY", 1917 } 1918 id, err := handler.repos.Tasks.Create(ctx, task) 1919 if err != nil { 1920 t.Fatalf("Failed to create task: %v", err) 1921 } 1922 1923 cancelCtx, cancel := context.WithCancel(context.Background()) 1924 cancel() 1925 1926 if err = handler.ClearRecur(cancelCtx, strconv.FormatInt(id, 10)); err == nil { 1927 t.Error("Expected error with canceled context") 1928 } 1929 }) 1930 }) 1931 1932 t.Run("ShowRecur", func(t *testing.T) { 1933 suite := NewHandlerTestSuite(t) 1934 defer suite.cleanup() 1935 1936 handler, err := NewTaskHandler() 1937 if err != nil { 1938 t.Fatalf("Failed to create handler: %v", err) 1939 } 1940 defer handler.Close() 1941 1942 until := time.Now() 1943 id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1944 UUID: uuid.New().String(), 1945 Description: "Test task", 1946 Status: "pending", 1947 Recur: "FREQ=DAILY", 1948 Until: &until, 1949 }) 1950 if err != nil { 1951 t.Fatalf("Failed to create task: %v", err) 1952 } 1953 1954 err = handler.ShowRecur(ctx, strconv.FormatInt(id, 10)) 1955 if err != nil { 1956 t.Errorf("ShowRecur failed: %v", err) 1957 } 1958 1959 t.Run("fails when repository Get fails", func(t *testing.T) { 1960 cancelCtx, cancel := context.WithCancel(context.Background()) 1961 cancel() 1962 1963 err := handler.ShowRecur(cancelCtx, "1") 1964 if err == nil { 1965 t.Error("Expected error when repository Get fails") 1966 } 1967 1968 if !strings.Contains(err.Error(), "failed to find task") { 1969 t.Errorf("Expected 'failed to find task' error, got: %v", err) 1970 } 1971 }) 1972 }) 1973 1974 t.Run("AddDep", func(t *testing.T) { 1975 suite := NewHandlerTestSuite(t) 1976 defer suite.cleanup() 1977 1978 handler, err := NewTaskHandler() 1979 if err != nil { 1980 t.Fatalf("Failed to create handler: %v", err) 1981 } 1982 defer handler.Close() 1983 1984 task1UUID := uuid.New().String() 1985 task2UUID := uuid.New().String() 1986 1987 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1988 UUID: task1UUID, Description: "Task 1", Status: "pending", 1989 }) 1990 if err != nil { 1991 t.Fatalf("Failed to create task 1: %v", err) 1992 } 1993 1994 if _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 1995 UUID: task2UUID, Description: "Task 2", Status: "pending", 1996 }); err != nil { 1997 t.Fatalf("Failed to create task 2: %v", err) 1998 } 1999 2000 err = handler.AddDep(ctx, strconv.FormatInt(id1, 10), task2UUID) 2001 if err != nil { 2002 t.Errorf("AddDep failed: %v", err) 2003 } 2004 2005 task, err := handler.repos.Tasks.Get(ctx, id1) 2006 if err != nil { 2007 t.Fatalf("Failed to get task: %v", err) 2008 } 2009 2010 if len(task.DependsOn) != 1 { 2011 t.Errorf("Expected 1 dependency, got %d", len(task.DependsOn)) 2012 } 2013 2014 if task.DependsOn[0] != task2UUID { 2015 t.Errorf("Expected dependency to be '%s', got '%s'", task2UUID, task.DependsOn[0]) 2016 } 2017 }) 2018 2019 t.Run("RemoveDep", func(t *testing.T) { 2020 suite := NewHandlerTestSuite(t) 2021 defer suite.cleanup() 2022 2023 handler, err := NewTaskHandler() 2024 if err != nil { 2025 t.Fatalf("Failed to create handler: %v", err) 2026 } 2027 defer handler.Close() 2028 2029 task1UUID := uuid.New().String() 2030 task2UUID := uuid.New().String() 2031 2032 _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 2033 UUID: task2UUID, Description: "Task 2", Status: "pending", 2034 }) 2035 if err != nil { 2036 t.Fatalf("Failed to create task 2: %v", err) 2037 } 2038 2039 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2040 UUID: task1UUID, 2041 Description: "Task 1", 2042 Status: "pending", 2043 DependsOn: []string{task2UUID}, 2044 }) 2045 if err != nil { 2046 t.Fatalf("Failed to create task 1: %v", err) 2047 } 2048 2049 err = handler.RemoveDep(ctx, strconv.FormatInt(id1, 10), task2UUID) 2050 if err != nil { 2051 t.Errorf("RemoveDep failed: %v", err) 2052 } 2053 2054 task, err := handler.repos.Tasks.Get(ctx, id1) 2055 if err != nil { 2056 t.Fatalf("Failed to get task: %v", err) 2057 } 2058 2059 if len(task.DependsOn) != 0 { 2060 t.Errorf("Expected 0 dependencies, got %d", len(task.DependsOn)) 2061 } 2062 }) 2063 2064 t.Run("ListDeps", func(t *testing.T) { 2065 suite := NewHandlerTestSuite(t) 2066 defer suite.cleanup() 2067 2068 handler, err := NewTaskHandler() 2069 if err != nil { 2070 t.Fatalf("Failed to create handler: %v", err) 2071 } 2072 defer handler.Close() 2073 2074 task1UUID := uuid.New().String() 2075 task2UUID := uuid.New().String() 2076 2077 _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 2078 UUID: task2UUID, Description: "Task 2", Status: "pending", 2079 }) 2080 if err != nil { 2081 t.Fatalf("Failed to create task 2: %v", err) 2082 } 2083 2084 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2085 UUID: task1UUID, 2086 Description: "Task 1", 2087 Status: "pending", 2088 DependsOn: []string{task2UUID}, 2089 }) 2090 if err != nil { 2091 t.Fatalf("Failed to create task 1: %v", err) 2092 } 2093 2094 err = handler.ListDeps(ctx, strconv.FormatInt(id1, 10)) 2095 if err != nil { 2096 t.Errorf("ListDeps failed: %v", err) 2097 } 2098 }) 2099 2100 t.Run("BlockedByDep", func(t *testing.T) { 2101 suite := NewHandlerTestSuite(t) 2102 defer suite.cleanup() 2103 2104 handler, err := NewTaskHandler() 2105 if err != nil { 2106 t.Fatalf("Failed to create handler: %v", err) 2107 } 2108 defer handler.Close() 2109 2110 task1UUID := uuid.New().String() 2111 task2UUID := uuid.New().String() 2112 2113 id2, err := handler.repos.Tasks.Create(ctx, &models.Task{UUID: task2UUID, Description: "Task 2", Status: "pending"}) 2114 if err != nil { 2115 t.Fatalf("Failed to create task 2: %v", err) 2116 } 2117 2118 if _, err = handler.repos.Tasks.Create(ctx, &models.Task{UUID: task1UUID, Description: "Task 1", Status: "pending", DependsOn: []string{task2UUID}}); err != nil { 2119 t.Fatalf("Failed to create task 1: %v", err) 2120 } 2121 2122 if err = handler.BlockedByDep(ctx, strconv.FormatInt(id2, 10)); err != nil { 2123 t.Errorf("BlockedByDep failed: %v", err) 2124 } 2125 }) 2126 2127 t.Run("Annotate", func(t *testing.T) { 2128 suite := NewHandlerTestSuite(t) 2129 defer suite.cleanup() 2130 2131 handler, err := NewTaskHandler() 2132 if err != nil { 2133 t.Fatalf("Failed to create handler: %v", err) 2134 } 2135 defer handler.Close() 2136 2137 id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2138 UUID: uuid.New().String(), 2139 Description: "Test task", 2140 Status: "pending", 2141 }) 2142 if err != nil { 2143 t.Fatalf("Failed to create task: %v", err) 2144 } 2145 2146 t.Run("adds annotation successfully", func(t *testing.T) { 2147 err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "First annotation") 2148 shared.AssertNoError(t, err, "Annotate should succeed") 2149 2150 task, err := handler.repos.Tasks.Get(ctx, id) 2151 shared.AssertNoError(t, err, "Get should succeed") 2152 shared.AssertEqual(t, 1, len(task.Annotations), "should have 1 annotation") 2153 shared.AssertEqual(t, "First annotation", task.Annotations[0], "annotation text should match") 2154 }) 2155 2156 t.Run("adds multiple annotations", func(t *testing.T) { 2157 err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "Second annotation") 2158 shared.AssertNoError(t, err, "Annotate should succeed") 2159 2160 task, err := handler.repos.Tasks.Get(ctx, id) 2161 shared.AssertNoError(t, err, "Get should succeed") 2162 shared.AssertEqual(t, 2, len(task.Annotations), "should have 2 annotations") 2163 }) 2164 2165 t.Run("fails with empty annotation", func(t *testing.T) { 2166 err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "") 2167 shared.AssertError(t, err, "should fail with empty annotation") 2168 shared.AssertContains(t, err.Error(), "annotation text required", "error message") 2169 }) 2170 2171 t.Run("fails with invalid task ID", func(t *testing.T) { 2172 err := handler.Annotate(ctx, "99999", "Test annotation") 2173 shared.AssertError(t, err, "should fail with invalid task ID") 2174 shared.AssertContains(t, err.Error(), "failed to find task", "error message") 2175 }) 2176 2177 t.Run("works with UUID", func(t *testing.T) { 2178 task := &models.Task{ 2179 UUID: uuid.New().String(), 2180 Description: "UUID task", 2181 Status: "pending", 2182 } 2183 _, err := handler.repos.Tasks.Create(ctx, task) 2184 shared.AssertNoError(t, err, "Create should succeed") 2185 2186 err = handler.Annotate(ctx, task.UUID, "UUID annotation") 2187 shared.AssertNoError(t, err, "Annotate with UUID should succeed") 2188 2189 retrieved, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID) 2190 shared.AssertNoError(t, err, "GetByUUID should succeed") 2191 shared.AssertEqual(t, 1, len(retrieved.Annotations), "should have 1 annotation") 2192 }) 2193 }) 2194 2195 t.Run("ListAnnotations", func(t *testing.T) { 2196 suite := NewHandlerTestSuite(t) 2197 defer suite.cleanup() 2198 2199 handler, err := NewTaskHandler() 2200 if err != nil { 2201 t.Fatalf("Failed to create handler: %v", err) 2202 } 2203 defer handler.Close() 2204 2205 t.Run("lists annotations successfully", func(t *testing.T) { 2206 task := &models.Task{ 2207 UUID: uuid.New().String(), 2208 Description: "Test task", 2209 Status: "pending", 2210 Annotations: []string{"Annotation 1", "Annotation 2", "Annotation 3"}, 2211 } 2212 id, err := handler.repos.Tasks.Create(ctx, task) 2213 shared.AssertNoError(t, err, "Create should succeed") 2214 2215 err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10)) 2216 shared.AssertNoError(t, err, "ListAnnotations should succeed") 2217 }) 2218 2219 t.Run("handles task with no annotations", func(t *testing.T) { 2220 task := &models.Task{ 2221 UUID: uuid.New().String(), 2222 Description: "Task without annotations", 2223 Status: "pending", 2224 } 2225 id, err := handler.repos.Tasks.Create(ctx, task) 2226 shared.AssertNoError(t, err, "Create should succeed") 2227 2228 err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10)) 2229 shared.AssertNoError(t, err, "ListAnnotations should succeed for empty annotations") 2230 }) 2231 2232 t.Run("fails with invalid task ID", func(t *testing.T) { 2233 err := handler.ListAnnotations(ctx, "99999") 2234 shared.AssertError(t, err, "should fail with invalid task ID") 2235 shared.AssertContains(t, err.Error(), "failed to find task", "error message") 2236 }) 2237 }) 2238 2239 t.Run("RemoveAnnotation", func(t *testing.T) { 2240 suite := NewHandlerTestSuite(t) 2241 defer suite.cleanup() 2242 2243 handler, err := NewTaskHandler() 2244 if err != nil { 2245 t.Fatalf("Failed to create handler: %v", err) 2246 } 2247 defer handler.Close() 2248 2249 t.Run("removes annotation successfully", func(t *testing.T) { 2250 task := &models.Task{ 2251 UUID: uuid.New().String(), 2252 Description: "Test task", 2253 Status: "pending", 2254 Annotations: []string{"First", "Second", "Third"}, 2255 } 2256 id, err := handler.repos.Tasks.Create(ctx, task) 2257 shared.AssertNoError(t, err, "Create should succeed") 2258 2259 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 2) 2260 shared.AssertNoError(t, err, "RemoveAnnotation should succeed") 2261 2262 retrieved, err := handler.repos.Tasks.Get(ctx, id) 2263 shared.AssertNoError(t, err, "Get should succeed") 2264 shared.AssertEqual(t, 2, len(retrieved.Annotations), "should have 2 annotations") 2265 shared.AssertEqual(t, "First", retrieved.Annotations[0], "first annotation should remain") 2266 shared.AssertEqual(t, "Third", retrieved.Annotations[1], "third annotation should be second") 2267 }) 2268 2269 t.Run("fails with invalid index (too low)", func(t *testing.T) { 2270 task := &models.Task{ 2271 UUID: uuid.New().String(), 2272 Description: "Test task", 2273 Status: "pending", 2274 Annotations: []string{"First"}, 2275 } 2276 id, err := handler.repos.Tasks.Create(ctx, task) 2277 shared.AssertNoError(t, err, "Create should succeed") 2278 2279 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 0) 2280 shared.AssertError(t, err, "should fail with index 0") 2281 shared.AssertContains(t, err.Error(), "index out of range", "error message") 2282 }) 2283 2284 t.Run("fails with invalid index (too high)", func(t *testing.T) { 2285 task := &models.Task{ 2286 UUID: uuid.New().String(), 2287 Description: "Test task", 2288 Status: "pending", 2289 Annotations: []string{"First"}, 2290 } 2291 id, err := handler.repos.Tasks.Create(ctx, task) 2292 shared.AssertNoError(t, err, "Create should succeed") 2293 2294 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 5) 2295 shared.AssertError(t, err, "should fail with index > len") 2296 shared.AssertContains(t, err.Error(), "index out of range", "error message") 2297 }) 2298 2299 t.Run("fails when task has no annotations", func(t *testing.T) { 2300 task := &models.Task{ 2301 UUID: uuid.New().String(), 2302 Description: "Task without annotations", 2303 Status: "pending", 2304 } 2305 id, err := handler.repos.Tasks.Create(ctx, task) 2306 shared.AssertNoError(t, err, "Create should succeed") 2307 2308 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 1) 2309 shared.AssertError(t, err, "should fail when no annotations") 2310 shared.AssertContains(t, err.Error(), "has no annotations", "error message") 2311 }) 2312 2313 t.Run("fails with invalid task ID", func(t *testing.T) { 2314 err := handler.RemoveAnnotation(ctx, "99999", 1) 2315 shared.AssertError(t, err, "should fail with invalid task ID") 2316 shared.AssertContains(t, err.Error(), "failed to find task", "error message") 2317 }) 2318 }) 2319 2320 t.Run("BulkEdit", func(t *testing.T) { 2321 suite := NewHandlerTestSuite(t) 2322 defer suite.cleanup() 2323 2324 handler, err := NewTaskHandler() 2325 if err != nil { 2326 t.Fatalf("Failed to create handler: %v", err) 2327 } 2328 defer handler.Close() 2329 2330 t.Run("updates multiple tasks successfully", func(t *testing.T) { 2331 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2332 UUID: uuid.New().String(), 2333 Description: "Task 1", 2334 Status: "pending", 2335 }) 2336 shared.AssertNoError(t, err, "Create should succeed") 2337 2338 id2, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2339 UUID: uuid.New().String(), 2340 Description: "Task 2", 2341 Status: "pending", 2342 }) 2343 shared.AssertNoError(t, err, "Create should succeed") 2344 2345 taskIDs := []string{strconv.FormatInt(id1, 10), strconv.FormatInt(id2, 10)} 2346 err = handler.BulkEdit(ctx, taskIDs, "done", "high", "test-project", "", []string{}, false, false) 2347 shared.AssertNoError(t, err, "BulkEdit should succeed") 2348 2349 task1, err := handler.repos.Tasks.Get(ctx, id1) 2350 shared.AssertNoError(t, err, "Get should succeed") 2351 shared.AssertEqual(t, "done", task1.Status, "task 1 status should be updated") 2352 shared.AssertEqual(t, "high", task1.Priority, "task 1 priority should be updated") 2353 shared.AssertEqual(t, "test-project", task1.Project, "task 1 project should be updated") 2354 2355 task2, err := handler.repos.Tasks.Get(ctx, id2) 2356 shared.AssertNoError(t, err, "Get should succeed") 2357 shared.AssertEqual(t, "done", task2.Status, "task 2 status should be updated") 2358 shared.AssertEqual(t, "high", task2.Priority, "task 2 priority should be updated") 2359 shared.AssertEqual(t, "test-project", task2.Project, "task 2 project should be updated") 2360 }) 2361 2362 t.Run("updates with tag replacement", func(t *testing.T) { 2363 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2364 UUID: uuid.New().String(), 2365 Description: "Task 1", 2366 Status: "pending", 2367 Tags: []string{"old-tag"}, 2368 }) 2369 shared.AssertNoError(t, err, "Create should succeed") 2370 2371 taskIDs := []string{strconv.FormatInt(id1, 10)} 2372 err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag1", "new-tag2"}, false, false) 2373 shared.AssertNoError(t, err, "BulkEdit should succeed") 2374 2375 task, err := handler.repos.Tasks.Get(ctx, id1) 2376 shared.AssertNoError(t, err, "Get should succeed") 2377 shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") 2378 shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag1"), "should contain new-tag1") 2379 shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag2"), "should contain new-tag2") 2380 }) 2381 2382 t.Run("adds tags with add-tags flag", func(t *testing.T) { 2383 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2384 UUID: uuid.New().String(), 2385 Description: "Task 1", 2386 Status: "pending", 2387 Tags: []string{"existing-tag"}, 2388 }) 2389 shared.AssertNoError(t, err, "Create should succeed") 2390 2391 taskIDs := []string{strconv.FormatInt(id1, 10)} 2392 err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag"}, true, false) 2393 shared.AssertNoError(t, err, "BulkEdit should succeed") 2394 2395 task, err := handler.repos.Tasks.Get(ctx, id1) 2396 shared.AssertNoError(t, err, "Get should succeed") 2397 shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") 2398 shared.AssertTrue(t, slices.Contains(task.Tags, "existing-tag"), "should contain existing-tag") 2399 shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag"), "should contain new-tag") 2400 }) 2401 2402 t.Run("removes tags with remove-tags flag", func(t *testing.T) { 2403 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2404 UUID: uuid.New().String(), 2405 Description: "Task 1", 2406 Status: "pending", 2407 Tags: []string{"tag1", "tag2", "tag3"}, 2408 }) 2409 shared.AssertNoError(t, err, "Create should succeed") 2410 2411 taskIDs := []string{strconv.FormatInt(id1, 10)} 2412 err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"tag2"}, false, true) 2413 shared.AssertNoError(t, err, "BulkEdit should succeed") 2414 2415 task, err := handler.repos.Tasks.Get(ctx, id1) 2416 shared.AssertNoError(t, err, "Get should succeed") 2417 shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") 2418 shared.AssertTrue(t, slices.Contains(task.Tags, "tag1"), "should contain tag1") 2419 shared.AssertTrue(t, slices.Contains(task.Tags, "tag3"), "should contain tag3") 2420 shared.AssertFalse(t, slices.Contains(task.Tags, "tag2"), "should not contain tag2") 2421 }) 2422 2423 t.Run("fails with no task IDs", func(t *testing.T) { 2424 err := handler.BulkEdit(ctx, []string{}, "done", "", "", "", []string{}, false, false) 2425 shared.AssertError(t, err, "should fail with no task IDs") 2426 shared.AssertContains(t, err.Error(), "no task IDs provided", "error message") 2427 }) 2428 2429 t.Run("fails with invalid task ID", func(t *testing.T) { 2430 err := handler.BulkEdit(ctx, []string{"99999"}, "done", "", "", "", []string{}, false, false) 2431 shared.AssertError(t, err, "should fail with invalid task ID") 2432 }) 2433 2434 t.Run("works with UUIDs", func(t *testing.T) { 2435 task1 := &models.Task{ 2436 UUID: uuid.New().String(), 2437 Description: "UUID task 1", 2438 Status: "pending", 2439 } 2440 _, err := handler.repos.Tasks.Create(ctx, task1) 2441 shared.AssertNoError(t, err, "Create should succeed") 2442 2443 task2 := &models.Task{ 2444 UUID: uuid.New().String(), 2445 Description: "UUID task 2", 2446 Status: "pending", 2447 } 2448 _, err = handler.repos.Tasks.Create(ctx, task2) 2449 shared.AssertNoError(t, err, "Create should succeed") 2450 2451 taskIDs := []string{task1.UUID, task2.UUID} 2452 err = handler.BulkEdit(ctx, taskIDs, "done", "", "", "", []string{}, false, false) 2453 shared.AssertNoError(t, err, "BulkEdit with UUIDs should succeed") 2454 2455 retrieved1, err := handler.repos.Tasks.GetByUUID(ctx, task1.UUID) 2456 shared.AssertNoError(t, err, "GetByUUID should succeed") 2457 shared.AssertEqual(t, "done", retrieved1.Status, "task 1 status should be updated") 2458 2459 retrieved2, err := handler.repos.Tasks.GetByUUID(ctx, task2.UUID) 2460 shared.AssertNoError(t, err, "GetByUUID should succeed") 2461 shared.AssertEqual(t, "done", retrieved2.Status, "task 2 status should be updated") 2462 }) 2463 }) 2464}