cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package handlers
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "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}