cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

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}