cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 921 lines 29 kB view raw
1package handlers 2 3import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/shared" 15 "github.com/stormlightlabs/noteleaf/internal/store" 16) 17 18func createTestMarkdownFile(t *testing.T, dir, filename, content string) string { 19 filePath := filepath.Join(dir, filename) 20 err := os.WriteFile(filePath, []byte(content), 0644) 21 if err != nil { 22 t.Fatalf("Failed to create test file: %v", err) 23 } 24 return filePath 25} 26 27func TestNoteHandler(t *testing.T) { 28 suite := NewHandlerTestSuite(t) 29 30 handler, err := NewNoteHandler() 31 if err != nil { 32 t.Fatalf("Failed to create note handler: %v", err) 33 } 34 defer handler.Close() 35 36 t.Run("New", func(t *testing.T) { 37 t.Run("creates handler successfully", func(t *testing.T) { 38 testHandler, err := NewNoteHandler() 39 shared.AssertNoError(t, err, "NewNoteHandler should succeed") 40 if testHandler == nil { 41 t.Fatal("Handler should not be nil") 42 } 43 defer testHandler.Close() 44 45 if testHandler.db == nil { 46 t.Error("Handler database should not be nil") 47 } 48 if testHandler.config == nil { 49 t.Error("Handler config should not be nil") 50 } 51 if testHandler.repos == nil { 52 t.Error("Handler repos should not be nil") 53 } 54 }) 55 56 t.Run("handles database initialization error", func(t *testing.T) { 57 envHelper := NewEnvironmentTestHelper() 58 defer envHelper.RestoreEnv() 59 60 if runtime.GOOS == "windows" { 61 envHelper.UnsetEnv("APPDATA") 62 } else { 63 envHelper.UnsetEnv("XDG_CONFIG_HOME") 64 envHelper.UnsetEnv("HOME") 65 } 66 envHelper.UnsetEnv("NOTELEAF_CONFIG") 67 envHelper.UnsetEnv("NOTELEAF_DATA_DIR") 68 69 _, err := NewNoteHandler() 70 shared.AssertErrorContains(t, err, "failed to initialize database", "NewNoteHandler should fail when database initialization fails") 71 }) 72 }) 73 74 t.Run("Create", func(t *testing.T) { 75 ctx := context.Background() 76 77 t.Run("creates note from title only", func(t *testing.T) { 78 err := handler.Create(ctx, "Test Note 1", "", "", false) 79 shared.AssertNoError(t, err, "Create should succeed") 80 }) 81 82 t.Run("creates note from title and content", func(t *testing.T) { 83 err := handler.Create(ctx, "Test Note 2", "This is test content", "", false) 84 shared.AssertNoError(t, err, "Create should succeed") 85 }) 86 87 t.Run("creates note from markdown file", func(t *testing.T) { 88 content := `# My Test Note 89<!-- tags: personal, work --> 90 91This is the content of my note.` 92 filePath := createTestMarkdownFile(t, suite.TempDir(), "test.md", content) 93 94 err := handler.Create(ctx, "", "", filePath, false) 95 shared.AssertNoError(t, err, "Create from file should succeed") 96 }) 97 98 t.Run("handles non-existent file", func(t *testing.T) { 99 err := handler.Create(ctx, "", "", "/non/existent/file.md", false) 100 shared.AssertErrorContains(t, err, "", "Create should fail with non-existent file") 101 }) 102 }) 103 104 t.Run("Edit", func(t *testing.T) { 105 ctx := context.Background() 106 107 t.Run("handles non-existent note", func(t *testing.T) { 108 err := handler.Edit(ctx, 999) 109 shared.AssertErrorContains(t, err, "failed to get note", "Edit should fail with non-existent note ID") 110 }) 111 112 t.Run("handles no editor configured", func(t *testing.T) { 113 envHelper := NewEnvironmentTestHelper() 114 defer envHelper.RestoreEnv() 115 116 envHelper.SetEnv("EDITOR", "") 117 envHelper.SetEnv("PATH", "") 118 119 err := handler.Edit(ctx, 1) 120 shared.AssertErrorContains(t, err, "failed to open editor", "Edit should fail when no editor is configured") 121 }) 122 123 t.Run("handles database connection error", func(t *testing.T) { 124 handler.db.Close() 125 defer func() { 126 var err error 127 handler.db, err = store.NewDatabase() 128 shared.AssertNoError(t, err, "Failed to reconnect to database") 129 }() 130 131 err := handler.Edit(ctx, 1) 132 shared.AssertErrorContains(t, err, "failed to get note", "Edit should fail when database is closed") 133 }) 134 135 t.Run("handles temp file creation error", func(t *testing.T) { 136 testHandler, err := NewNoteHandler() 137 shared.AssertNoError(t, err, "Failed to create test handler") 138 defer testHandler.Close() 139 140 err = testHandler.Create(ctx, "Temp File Test Note", "Test content", "", false) 141 shared.AssertNoError(t, err, "Failed to create test note") 142 143 envHelper := NewEnvironmentTestHelper() 144 defer envHelper.RestoreEnv() 145 envHelper.SetEnv("TMPDIR", "/non/existent/path") 146 147 err = testHandler.Edit(ctx, 1) 148 shared.AssertErrorContains(t, err, "failed to create temporary file", "Edit should fail when temp file creation fails") 149 }) 150 151 t.Run("handles editor failure", func(t *testing.T) { 152 testHandler, err := NewNoteHandler() 153 shared.AssertNoError(t, err, "Failed to create test handler") 154 defer testHandler.Close() 155 156 err = testHandler.Create(ctx, "Editor Failure Test Note", "Test content", "", false) 157 shared.AssertNoError(t, err, "Failed to create test note") 158 159 mockEditor := NewMockEditor().WithFailure("editor process failed") 160 testHandler.openInEditorFunc = mockEditor.GetEditorFunc() 161 162 err = testHandler.Edit(ctx, 1) 163 shared.AssertErrorContains(t, err, "failed to open editor", "Edit should fail when editor fails") 164 }) 165 166 t.Run("handles temp file write error", func(t *testing.T) { 167 originalHandler := handler.openInEditorFunc 168 defer func() { handler.openInEditorFunc = originalHandler }() 169 170 mockEditor := NewMockEditor().WithReadOnly() 171 handler.openInEditorFunc = mockEditor.GetEditorFunc() 172 173 err := handler.Edit(ctx, 1) 174 shared.AssertErrorContains(t, err, "", "Edit should handle temp file write issues") 175 }) 176 177 t.Run("handles file read error after editing", func(t *testing.T) { 178 testHandler, err := NewNoteHandler() 179 shared.AssertNoError(t, err, "Failed to create test handler") 180 defer testHandler.Close() 181 182 err = testHandler.Create(ctx, "File Read Error Test Note", "Test content", "", false) 183 shared.AssertNoError(t, err, "Failed to create test note") 184 185 mockEditor := NewMockEditor().WithFileDeleted() 186 testHandler.openInEditorFunc = mockEditor.GetEditorFunc() 187 188 err = testHandler.Edit(ctx, 1) 189 shared.AssertErrorContains(t, err, "failed to read edited content", "Edit should fail when temp file is deleted") 190 }) 191 192 t.Run("handles database update error", func(t *testing.T) { 193 handler := NewHandlerTestHelper(t) 194 id := handler.CreateTestNote(t, "Database Update Error Test Note", "Test content", nil) 195 196 dbHelper := NewDatabaseTestHelper(handler) 197 dbHelper.DropNotesTable() 198 199 mockEditor := NewMockEditor().WithContent(`# Modified Note 200 201Modified content here.`) 202 handler.openInEditorFunc = mockEditor.GetEditorFunc() 203 204 err := handler.Edit(ctx, id) 205 shared.AssertErrorContains(t, err, "failed to get note", "Edit should fail when database is corrupted") 206 }) 207 208 t.Run("handles validation error - corrupted note content", func(t *testing.T) { 209 handler := NewHandlerTestHelper(t) 210 id := handler.CreateTestNote(t, "Corrupted Content Test Note", "Test content", nil) 211 212 invalidContent := string([]byte{0, 1, 2, 255, 254, 253}) 213 mockEditor := NewMockEditor().WithContent(invalidContent) 214 handler.openInEditorFunc = mockEditor.GetEditorFunc() 215 216 err := handler.Edit(ctx, id) 217 if err != nil && !strings.Contains(err.Error(), "failed to update note") { 218 t.Errorf("Edit should handle corrupted content gracefully, got: %v", err) 219 } 220 }) 221 222 t.Run("handles validation error - empty note after edit", func(t *testing.T) { 223 mockEditor := func(editor, filePath string) error { 224 return os.WriteFile(filePath, []byte(""), 0644) 225 } 226 handler.openInEditorFunc = mockEditor 227 228 err := handler.Edit(ctx, 1) 229 if err != nil { 230 t.Logf("Edit with empty content handled: %v", err) 231 } 232 }) 233 234 t.Run("handles database transaction rollback", func(t *testing.T) { 235 handler.db.Close() 236 var dbErr error 237 handler.db, dbErr = store.NewDatabase() 238 if dbErr != nil { 239 t.Fatalf("Failed to reconnect: %v", dbErr) 240 } 241 242 handler.db.Exec("BEGIN TRANSACTION") 243 handler.db.Exec("UPDATE notes SET title = 'locked' WHERE id = 1") 244 245 db2, err2 := store.NewDatabase() 246 if err2 != nil { 247 t.Fatalf("Failed to create second connection: %v", err2) 248 } 249 defer db2.Close() 250 251 oldDB := handler.db 252 handler.db = db2 253 254 mockEditor := func(editor, filePath string) error { 255 content := `# Modified Title 256 257Modified content.` 258 return os.WriteFile(filePath, []byte(content), 0644) 259 } 260 handler.openInEditorFunc = mockEditor 261 262 err := handler.Edit(ctx, 1) 263 264 oldDB.Exec("ROLLBACK") 265 handler.db = oldDB 266 267 if err == nil { 268 t.Log("Edit succeeded despite transaction conflict") 269 } 270 }) 271 272 t.Run("handles successful edit", func(t *testing.T) { 273 handler := NewHandlerTestHelper(t) 274 id := handler.CreateTestNote(t, "Edit Test Note", "Original content", nil) 275 276 mockEditor := NewMockEditor().WithContent(`# Modified Edit Test Note 277 278This is the modified content. 279 280<!-- Tags: modified, test -->`) 281 handler.openInEditorFunc = mockEditor.GetEditorFunc() 282 283 err := handler.Edit(ctx, id) 284 shared.AssertNoError(t, err, "Edit should succeed") 285 }) 286 287 t.Run("Edit Errors", func(t *testing.T) { 288 289 t.Run("Validation Errors", func(t *testing.T) { 290 t.Run("handles corrupted note content", func(t *testing.T) { 291 handler := NewHandlerTestHelper(t) 292 ctx := context.Background() 293 294 noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) 295 296 invalidContent := string([]byte{0, 1, 2, 255, 254, 253}) 297 mockEditor := NewMockEditor().WithContent(invalidContent) 298 handler.openInEditorFunc = mockEditor.GetEditorFunc() 299 300 err := handler.Edit(ctx, noteID) 301 if err != nil && !strings.Contains(err.Error(), "failed to update note") { 302 t.Errorf("Edit should handle corrupted content gracefully, got: %v", err) 303 } 304 }) 305 306 t.Run("handles empty note after edit", func(t *testing.T) { 307 handler := NewHandlerTestHelper(t) 308 ctx := context.Background() 309 310 noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) 311 312 mockEditor := NewMockEditor().WithContent("") 313 handler.openInEditorFunc = mockEditor.GetEditorFunc() 314 315 err := handler.Edit(ctx, noteID) 316 if err != nil { 317 t.Logf("Edit with empty content handled: %v", err) 318 } 319 }) 320 321 t.Run("handles very large content", func(t *testing.T) { 322 handler := NewHandlerTestHelper(t) 323 ctx := context.Background() 324 325 noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) 326 327 largeContent := fmt.Sprintf("# Large Note\n\n%s", strings.Repeat("Large content ", 70000)) 328 mockEditor := NewMockEditor().WithContent(largeContent) 329 handler.openInEditorFunc = mockEditor.GetEditorFunc() 330 331 err := handler.Edit(ctx, noteID) 332 if err != nil { 333 t.Logf("Edit with large content handled: %v", err) 334 } else { 335 t.Log("Edit succeeded with large content") 336 } 337 }) 338 }) 339 340 t.Run("Success Cases", func(t *testing.T) { 341 t.Run("handles successful edit with title and tags", func(t *testing.T) { 342 handler := NewHandlerTestHelper(t) 343 ctx := context.Background() 344 noteID := handler.CreateTestNote(t, "Original Note", "Original content", []string{"original"}) 345 mockEditor := NewMockEditor().WithContent(`# Modified Note 346 347This is the modified content. 348 349<!-- Tags: modified, test -->`) 350 handler.openInEditorFunc = mockEditor.GetEditorFunc() 351 err := handler.Edit(ctx, noteID) 352 353 shared.AssertNoError(t, err, "Edit should succeed") 354 AssertExists(t, handler.repos.Notes.Get, noteID, "note") 355 }) 356 357 t.Run("handles no changes made", func(t *testing.T) { 358 handler := NewHandlerTestHelper(t) 359 ctx := context.Background() 360 noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) 361 originalContent := handler.formatNoteForEdit(&models.Note{ 362 ID: noteID, 363 Title: "Test Note", 364 Content: "Test content", 365 Tags: nil, 366 }) 367 mockEditor := NewMockEditor().WithContent(originalContent) 368 handler.openInEditorFunc = mockEditor.GetEditorFunc() 369 370 err := handler.Edit(ctx, noteID) 371 shared.AssertNoError(t, err, "Edit should succeed even with no changes") 372 }) 373 374 t.Run("handles content without title", func(t *testing.T) { 375 handler := NewHandlerTestHelper(t) 376 ctx := context.Background() 377 378 noteID := handler.CreateTestNote(t, "Original Title", "Original content", nil) 379 380 mockEditor := NewMockEditor().WithContent("Just some content without a title") 381 handler.openInEditorFunc = mockEditor.GetEditorFunc() 382 383 err := handler.Edit(ctx, noteID) 384 shared.AssertNoError(t, err, "Edit should succeed without title") 385 }) 386 }) 387 }) 388 }) 389 390 t.Run("Read/View", func(t *testing.T) { 391 ctx := context.Background() 392 393 t.Run("views note successfully", func(t *testing.T) { 394 testHandler, err := NewNoteHandler() 395 if err != nil { 396 t.Fatalf("Failed to create test handler: %v", err) 397 } 398 defer testHandler.Close() 399 400 err = testHandler.Create(ctx, "View Test Note", "Test content for viewing", "", false) 401 if err != nil { 402 t.Fatalf("Failed to create test note: %v", err) 403 } 404 405 err = testHandler.View(ctx, 1) 406 if err != nil { 407 t.Errorf("View should succeed: %v", err) 408 } 409 }) 410 411 t.Run("handles non-existent note", func(t *testing.T) { 412 err := handler.View(ctx, 999) 413 if err == nil { 414 t.Error("View should fail with non-existent note ID") 415 } 416 if !strings.Contains(err.Error(), "failed to get note") && !strings.Contains(err.Error(), "failed to find note") { 417 t.Errorf("Expected note not found error, got: %v", err) 418 } 419 }) 420 421 }) 422 423 t.Run("List", func(t *testing.T) { 424 ctx := context.Background() 425 426 t.Run("lists with archived filter", func(t *testing.T) { 427 testHandler, err := NewNoteHandler() 428 if err != nil { 429 t.Fatalf("Failed to create test handler: %v", err) 430 } 431 defer testHandler.Close() 432 433 err = testHandler.List(ctx, true, true, nil) 434 if err != nil { 435 t.Errorf("List with archived filter should succeed: %v", err) 436 } 437 }) 438 439 t.Run("lists with tag filter", func(t *testing.T) { 440 testHandler, err := NewNoteHandler() 441 if err != nil { 442 t.Fatalf("Failed to create test handler: %v", err) 443 } 444 defer testHandler.Close() 445 446 err = testHandler.List(ctx, true, false, []string{"work", "personal"}) 447 if err != nil { 448 t.Errorf("List with tag filter should succeed: %v", err) 449 } 450 }) 451 452 t.Run("handles empty note list", func(t *testing.T) { 453 _ = NewHandlerTestSuite(t) 454 455 emptyHandler, err := NewNoteHandler() 456 if err != nil { 457 t.Fatalf("Failed to create empty handler: %v", err) 458 } 459 defer emptyHandler.Close() 460 461 err = emptyHandler.List(ctx, true, false, nil) 462 if err != nil { 463 t.Errorf("ListStatic should succeed with empty list: %v", err) 464 } 465 }) 466 467 t.Run("interactive mode path", func(t *testing.T) { 468 _ = NewHandlerTestSuite(t) 469 470 testHandler, err := NewNoteHandler() 471 if err != nil { 472 t.Fatalf("Failed to create test handler: %v", err) 473 } 474 defer testHandler.Close() 475 476 if err := testHandler.Create(ctx, "Interactive Test Note 1", "Test content for interactive mode", "", false); err != nil { 477 t.Fatalf("Failed to create test note 1: %v", err) 478 } 479 480 if err := testHandler.Create(ctx, "Interactive Test Note 2", "Test content with tags", "", false); err != nil { 481 t.Fatalf("Failed to create test note 2: %v", err) 482 } 483 484 if err := TestNoteInteractiveList(t, testHandler, false, nil); err != nil { 485 t.Errorf("Interactive note list test failed: %v", err) 486 } 487 }) 488 489 t.Run("interactive mode path with filters", func(t *testing.T) { 490 _ = NewHandlerTestSuite(t) 491 492 testHandler, err := NewNoteHandler() 493 if err != nil { 494 t.Fatalf("Failed to create test handler: %v", err) 495 } 496 defer testHandler.Close() 497 498 if err := testHandler.Create(ctx, "Tagged Note", "Test content with work tag", "", false); err != nil { 499 t.Fatalf("Failed to create tagged note: %v", err) 500 } 501 502 if err := TestNoteInteractiveList(t, testHandler, true, []string{"work"}); err != nil { 503 t.Errorf("Interactive note list test with filters failed: %v", err) 504 } 505 }) 506 }) 507 508 t.Run("Delete", func(t *testing.T) { 509 ctx := context.Background() 510 511 t.Run("handles non-existent note", func(t *testing.T) { 512 testHandler, err := NewNoteHandler() 513 if err != nil { 514 t.Fatalf("Failed to create test handler: %v", err) 515 } 516 defer testHandler.Close() 517 518 err = testHandler.Delete(ctx, 999) 519 if err == nil { 520 t.Error("Delete should fail with non-existent note ID") 521 } 522 if !strings.Contains(err.Error(), "failed to get note") && !strings.Contains(err.Error(), "failed to find note") { 523 t.Errorf("Expected note not found error, got: %v", err) 524 } 525 }) 526 527 t.Run("deletes note successfully", func(t *testing.T) { 528 testHandler, err := NewNoteHandler() 529 if err != nil { 530 t.Fatalf("Failed to create test handler: %v", err) 531 } 532 defer testHandler.Close() 533 534 err = testHandler.Create(ctx, "Note to Delete", "This will be deleted", "", false) 535 if err != nil { 536 t.Fatalf("Failed to create test note: %v", err) 537 } 538 539 err = testHandler.Delete(ctx, 1) 540 if err != nil { 541 t.Errorf("Delete should succeed: %v", err) 542 } 543 544 err = testHandler.View(ctx, 1) 545 if err == nil { 546 t.Error("Note should be gone after deletion") 547 } 548 }) 549 550 t.Run("deletes note with file path", func(t *testing.T) { 551 testSuite := NewHandlerTestSuite(t) 552 553 testHandler, err := NewNoteHandler() 554 if err != nil { 555 t.Fatalf("Failed to create test handler: %v", err) 556 } 557 defer testHandler.Close() 558 559 filePath := createTestMarkdownFile(t, testSuite.TempDir(), "delete-test.md", "# Test Note\n\nTest content") 560 561 err = testHandler.Create(ctx, "", "", filePath, false) 562 if err != nil { 563 t.Fatalf("Failed to create test note from file: %v", err) 564 } 565 566 err = testHandler.Delete(ctx, 1) 567 if err != nil { 568 t.Errorf("Delete should succeed: %v", err) 569 } 570 571 err = testHandler.View(ctx, 1) 572 if err == nil { 573 t.Error("Note should be gone after deletion") 574 } 575 }) 576 }) 577 578 t.Run("Close", func(t *testing.T) { 579 t.Run("closes handler resources successfully", func(t *testing.T) { 580 testHandler, err := NewNoteHandler() 581 if err != nil { 582 t.Fatalf("Failed to create test handler: %v", err) 583 } 584 585 if err = testHandler.Close(); err != nil { 586 t.Errorf("Close should succeed: %v", err) 587 } 588 }) 589 590 t.Run("handles nil database", func(t *testing.T) { 591 testHandler, err := NewNoteHandler() 592 if err != nil { 593 t.Fatalf("Failed to create test handler: %v", err) 594 } 595 testHandler.db = nil 596 597 if err = testHandler.Close(); err != nil { 598 t.Errorf("Close should succeed with nil database: %v", err) 599 } 600 }) 601 }) 602 603 t.Run("Helper Methods", func(t *testing.T) { 604 t.Run("parseNoteContent", func(t *testing.T) { 605 tt := []struct { 606 name string 607 content string 608 expectedTitle string 609 expectedContent string 610 expectedTags []string 611 }{ 612 { 613 name: "note with title and tags", 614 content: "# My Note\n<!-- tags: work, personal -->\n\nContent here", 615 expectedTitle: "My Note", 616 expectedContent: "# My Note\n<!-- tags: work, personal -->\n\nContent here", 617 expectedTags: nil, 618 }, 619 { 620 name: "note without title", 621 content: "Just some content without title", 622 expectedTitle: "", 623 expectedContent: "Just some content without title", 624 expectedTags: nil, 625 }, 626 { 627 name: "note without tags", 628 content: "# Title Only\n\nContent here", 629 expectedTitle: "Title Only", 630 expectedContent: "# Title Only\n\nContent here", 631 expectedTags: nil, 632 }, 633 } 634 635 for _, tc := range tt { 636 t.Run(tc.name, func(t *testing.T) { 637 title, content, tags := handler.parseNoteContent(tc.content) 638 if title != tc.expectedTitle { 639 t.Errorf("Expected title %q, got %q", tc.expectedTitle, title) 640 } 641 if content != tc.expectedContent { 642 t.Errorf("Expected content %q, got %q", tc.expectedContent, content) 643 } 644 if len(tags) != len(tc.expectedTags) { 645 t.Errorf("Expected %d tags, got %d", len(tc.expectedTags), len(tags)) 646 } 647 for i, tag := range tc.expectedTags { 648 if i < len(tags) && tags[i] != tag { 649 t.Errorf("Expected tag %q, got %q", tag, tags[i]) 650 } 651 } 652 }) 653 } 654 }) 655 656 t.Run("getEditor", func(t *testing.T) { 657 originalEditor := os.Getenv("EDITOR") 658 defer os.Setenv("EDITOR", originalEditor) 659 660 t.Run("uses EDITOR environment variable", func(t *testing.T) { 661 os.Setenv("EDITOR", "test-editor") 662 editor := handler.getEditor() 663 if editor != "test-editor" { 664 t.Errorf("Expected 'test-editor', got %q", editor) 665 } 666 }) 667 668 t.Run("finds available editor", func(t *testing.T) { 669 os.Unsetenv("EDITOR") 670 editor := handler.getEditor() 671 if editor == "" { 672 t.Skip("No editors available in PATH") 673 } 674 }) 675 676 t.Run("returns empty when no editor available", func(t *testing.T) { 677 os.Unsetenv("EDITOR") 678 originalPath := os.Getenv("PATH") 679 os.Setenv("PATH", "") 680 defer os.Setenv("PATH", originalPath) 681 682 editor := handler.getEditor() 683 if editor != "" { 684 t.Errorf("Expected empty editor, got %q", editor) 685 } 686 }) 687 }) 688 }) 689 690 t.Run("CreateInteractive", func(t *testing.T) { 691 ctx := context.Background() 692 693 t.Run("creates note successfully", func(t *testing.T) { 694 handler := NewHandlerTestHelper(t) 695 mockEditor := NewMockEditor().WithContent(`# Test Interactive Note 696 697This is content from the interactive editor. 698 699<!-- Tags: interactive, test -->`) 700 handler.openInEditorFunc = mockEditor.GetEditorFunc() 701 702 err := handler.createInteractive(ctx) 703 shared.AssertNoError(t, err, "createInteractive should succeed") 704 }) 705 706 t.Run("handles cancelled note creation", func(t *testing.T) { 707 handler := NewHandlerTestHelper(t) 708 mockEditor := NewMockEditor().WithContent("") // Empty content simulates cancellation 709 handler.openInEditorFunc = mockEditor.GetEditorFunc() 710 711 err := handler.createInteractive(ctx) 712 shared.AssertNoError(t, err, "createInteractive should succeed even when cancelled") 713 }) 714 715 t.Run("handles editor error", func(t *testing.T) { 716 handler := NewHandlerTestHelper(t) 717 mockEditor := NewMockEditor().WithFailure("editor failed to open") 718 handler.openInEditorFunc = mockEditor.GetEditorFunc() 719 720 err := handler.createInteractive(ctx) 721 shared.AssertErrorContains(t, err, "failed to open editor", "createInteractive should fail when editor fails") 722 }) 723 724 t.Run("handles no editor configured", func(t *testing.T) { 725 handler := NewHandlerTestHelper(t) 726 envHelper := NewEnvironmentTestHelper() 727 defer envHelper.RestoreEnv() 728 729 envHelper.UnsetEnv("EDITOR") 730 envHelper.SetEnv("PATH", "") 731 732 err := handler.createInteractive(ctx) 733 shared.AssertErrorContains(t, err, "no editor configured", "createInteractive should fail when no editor is configured") 734 }) 735 736 t.Run("handles file read error after editing", func(t *testing.T) { 737 handler := NewHandlerTestHelper(t) 738 mockEditor := NewMockEditor().WithFileDeleted() 739 handler.openInEditorFunc = mockEditor.GetEditorFunc() 740 741 err := handler.createInteractive(ctx) 742 shared.AssertErrorContains(t, err, "failed to read edited content", "createInteractive should fail when temp file is deleted") 743 }) 744 }) 745 746 t.Run("CreateWithOptions", func(t *testing.T) { 747 ctx := context.Background() 748 749 t.Run("creates note successfully without editor prompt", func(t *testing.T) { 750 handler := NewHandlerTestHelper(t) 751 err := handler.CreateWithOptions(ctx, "Test Note", "Test content", "", false, false) 752 shared.AssertNoError(t, err, "CreateWithOptions should succeed") 753 }) 754 755 t.Run("creates note successfully with editor prompt disabled", func(t *testing.T) { 756 handler := NewHandlerTestHelper(t) 757 err := handler.CreateWithOptions(ctx, "Another Test Note", "More content", "", false, false) 758 shared.AssertNoError(t, err, "CreateWithOptions should succeed") 759 }) 760 761 t.Run("handles database error during creation", func(t *testing.T) { 762 handler := NewHandlerTestHelper(t) 763 cancelCtx, cancel := context.WithCancel(ctx) 764 cancel() 765 766 err := handler.CreateWithOptions(cancelCtx, "Test Note", "Test content", "", false, false) 767 shared.AssertErrorContains(t, err, "failed to create note", "CreateWithOptions should fail with cancelled context") 768 }) 769 770 t.Run("creates note with empty content", func(t *testing.T) { 771 handler := NewHandlerTestHelper(t) 772 err := handler.CreateWithOptions(ctx, "Empty Content Note", "", "", false, false) 773 shared.AssertNoError(t, err, "CreateWithOptions should succeed with empty content") 774 }) 775 776 t.Run("creates note with empty title", func(t *testing.T) { 777 handler := NewHandlerTestHelper(t) 778 err := handler.CreateWithOptions(ctx, "", "Content without title", "", false, false) 779 shared.AssertNoError(t, err, "CreateWithOptions should succeed with empty title") 780 }) 781 782 t.Run("handles editor prompt with no editor available", func(t *testing.T) { 783 handler := NewHandlerTestHelper(t) 784 envHelper := NewEnvironmentTestHelper() 785 defer envHelper.RestoreEnv() 786 787 envHelper.UnsetEnv("EDITOR") 788 envHelper.SetEnv("PATH", "") 789 790 err := handler.CreateWithOptions(ctx, "Test Note", "Test content", "", false, true) 791 shared.AssertNoError(t, err, "CreateWithOptions should succeed even when no editor is available") 792 }) 793 }) 794 795 t.Run("formatNoteForView", func(t *testing.T) { 796 t.Run("formats note with title and content", func(t *testing.T) { 797 note := &models.Note{ 798 Title: "Test Note", 799 Content: "This is test content", 800 Tags: []string{}, 801 Created: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 802 Modified: time.Date(2023, 1, 2, 11, 0, 0, 0, time.UTC), 803 } 804 805 result := handler.formatNoteForView(note) 806 807 if !strings.Contains(result, "# Test Note") { 808 t.Error("Formatted note should contain title") 809 } 810 if !strings.Contains(result, "This is test content") { 811 t.Error("Formatted note should contain content") 812 } 813 if !strings.Contains(result, "**Created:**") { 814 t.Error("Formatted note should contain created timestamp") 815 } 816 if !strings.Contains(result, "**Modified:**") { 817 t.Error("Formatted note should contain modified timestamp") 818 } 819 }) 820 821 t.Run("formats note with tags", func(t *testing.T) { 822 note := &models.Note{ 823 Title: "Tagged Note", 824 Content: "Content with tags", 825 Tags: []string{"work", "important", "personal"}, 826 Created: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 827 Modified: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 828 } 829 830 result := handler.formatNoteForView(note) 831 832 if !strings.Contains(result, "**Tags:**") { 833 t.Error("Formatted note should contain tags section") 834 } 835 if !strings.Contains(result, "`work`") { 836 t.Error("Formatted note should contain work tag") 837 } 838 if !strings.Contains(result, "`important`") { 839 t.Error("Formatted note should contain important tag") 840 } 841 if !strings.Contains(result, "`personal`") { 842 t.Error("Formatted note should contain personal tag") 843 } 844 }) 845 846 t.Run("formats note with no tags", func(t *testing.T) { 847 note := &models.Note{ 848 Title: "Untagged Note", 849 Content: "Content without tags", 850 Tags: []string{}, 851 Created: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 852 Modified: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 853 } 854 855 result := handler.formatNoteForView(note) 856 857 if strings.Contains(result, "**Tags:**") { 858 t.Error("Formatted note should not contain tags section when no tags exist") 859 } 860 }) 861 862 t.Run("handles content with existing title", func(t *testing.T) { 863 note := &models.Note{ 864 Title: "Note Title", 865 Content: "# Duplicate Title\nContent after title", 866 Tags: []string{}, 867 Created: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 868 Modified: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 869 } 870 871 result := handler.formatNoteForView(note) 872 873 if !strings.Contains(result, "Content after title") { 874 t.Error("Formatted note should contain content after duplicate title removal") 875 } 876 contentLines := strings.Split(result, "\n") 877 duplicateTitleCount := 0 878 for _, line := range contentLines { 879 if strings.Contains(line, "# Duplicate Title") { 880 duplicateTitleCount++ 881 } 882 } 883 if duplicateTitleCount > 0 { 884 t.Error("Formatted note should not contain duplicate title from content") 885 } 886 }) 887 888 t.Run("handles empty content", func(t *testing.T) { 889 note := &models.Note{ 890 Title: "Empty Content Note", 891 Content: "", 892 Tags: []string{}, 893 Created: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 894 Modified: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 895 } 896 897 result := handler.formatNoteForView(note) 898 899 if !strings.Contains(result, "# Empty Content Note") { 900 t.Error("Formatted note should contain title even with empty content") 901 } 902 if !strings.Contains(result, "---") { 903 t.Error("Formatted note should contain separator") 904 } 905 }) 906 907 t.Run("handles content with only title line", func(t *testing.T) { 908 note := &models.Note{ 909 Title: "Single Line", 910 Content: "# Single Line", 911 Tags: []string{}, 912 Created: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 913 Modified: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), 914 } 915 916 if !strings.Contains(handler.formatNoteForView(note), "# Single Line") { 917 t.Error("Formatted note should contain title") 918 } 919 }) 920 }) 921}