cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

feat: edit note command

+244 -2
+23 -2
cmd/handlers/notes.go
··· 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 11 "github.com/stormlightlabs/noteleaf/internal/models" ··· 13 "github.com/stormlightlabs/noteleaf/internal/store" 14 "github.com/stormlightlabs/noteleaf/internal/utils" 15 ) 16 17 // NoteHandler handles all note-related commands 18 type NoteHandler struct { ··· 79 // New is an alias for Create 80 func New(ctx context.Context, args []string) error { 81 return Create(ctx, args) 82 } 83 84 func (h *NoteHandler) createInteractive(ctx context.Context) error { ··· 279 280 return "" 281 } 282 - 283 - type editorFunc func(editor, filePath string) error 284 285 func defaultOpenInEditor(editor, filePath string) error { 286 cmd := exec.Command(editor, filePath)
··· 6 "os" 7 "os/exec" 8 "path/filepath" 9 + "strconv" 10 "strings" 11 12 "github.com/stormlightlabs/noteleaf/internal/models" ··· 14 "github.com/stormlightlabs/noteleaf/internal/store" 15 "github.com/stormlightlabs/noteleaf/internal/utils" 16 ) 17 + 18 + type editorFunc func(editor, filePath string) error 19 20 // NoteHandler handles all note-related commands 21 type NoteHandler struct { ··· 82 // New is an alias for Create 83 func New(ctx context.Context, args []string) error { 84 return Create(ctx, args) 85 + } 86 + 87 + // Edit handles note editing by ID 88 + func Edit(ctx context.Context, args []string) error { 89 + if len(args) != 1 { 90 + return fmt.Errorf("edit requires exactly one argument: note ID") 91 + } 92 + 93 + id, err := strconv.ParseInt(args[0], 10, 64) 94 + if err != nil { 95 + return fmt.Errorf("invalid note ID: %s", args[0]) 96 + } 97 + 98 + handler, err := NewNoteHandler() 99 + if err != nil { 100 + return err 101 + } 102 + defer handler.Close() 103 + 104 + return handler.editNote(ctx, id) 105 } 106 107 func (h *NoteHandler) createInteractive(ctx context.Context) error { ··· 302 303 return "" 304 } 305 306 func defaultOpenInEditor(editor, filePath string) error { 307 cmd := exec.Command(editor, filePath)
+221
cmd/handlers/notes_test.go
··· 695 t.Errorf("Close should handle nil database gracefully: %v", err) 696 } 697 }
··· 695 t.Errorf("Close should handle nil database gracefully: %v", err) 696 } 697 } 698 + 699 + func TestEdit(t *testing.T) { 700 + t.Run("validates argument count", func(t *testing.T) { 701 + _, cleanup := setupNoteTest(t) 702 + defer cleanup() 703 + 704 + ctx := context.Background() 705 + 706 + err := Edit(ctx, []string{}) 707 + if err == nil { 708 + t.Error("Edit should fail with no arguments") 709 + } 710 + if !strings.Contains(err.Error(), "edit requires exactly one argument") { 711 + t.Errorf("Expected argument count error, got: %v", err) 712 + } 713 + 714 + err = Edit(ctx, []string{"1", "2"}) 715 + if err == nil { 716 + t.Error("Edit should fail with too many arguments") 717 + } 718 + if !strings.Contains(err.Error(), "edit requires exactly one argument") { 719 + t.Errorf("Expected argument count error, got: %v", err) 720 + } 721 + }) 722 + 723 + t.Run("validates note ID format", func(t *testing.T) { 724 + _, cleanup := setupNoteTest(t) 725 + defer cleanup() 726 + 727 + ctx := context.Background() 728 + 729 + err := Edit(ctx, []string{"invalid"}) 730 + if err == nil { 731 + t.Error("Edit should fail with invalid note ID") 732 + } 733 + if !strings.Contains(err.Error(), "invalid note ID") { 734 + t.Errorf("Expected invalid ID error, got: %v", err) 735 + } 736 + 737 + err = Edit(ctx, []string{"-1"}) 738 + if err == nil { 739 + t.Error("Edit should fail with negative note ID") 740 + } 741 + 742 + if !strings.Contains(err.Error(), "failed to get note") { 743 + t.Errorf("Expected note not found error for negative ID, got: %v", err) 744 + } 745 + }) 746 + 747 + t.Run("handles non-existent note", func(t *testing.T) { 748 + _, cleanup := setupNoteTest(t) 749 + defer cleanup() 750 + 751 + ctx := context.Background() 752 + 753 + err := Edit(ctx, []string{"999"}) 754 + if err == nil { 755 + t.Error("Edit should fail with non-existent note ID") 756 + } 757 + if !strings.Contains(err.Error(), "failed to get note") { 758 + t.Errorf("Expected note not found error, got: %v", err) 759 + } 760 + }) 761 + 762 + t.Run("handles no editor configured", func(t *testing.T) { 763 + _, cleanup := setupNoteTest(t) 764 + defer cleanup() 765 + 766 + originalEditor := os.Getenv("EDITOR") 767 + originalPath := os.Getenv("PATH") 768 + os.Setenv("EDITOR", "") 769 + os.Setenv("PATH", "") 770 + defer func() { 771 + os.Setenv("EDITOR", originalEditor) 772 + os.Setenv("PATH", originalPath) 773 + }() 774 + 775 + ctx := context.Background() 776 + 777 + err := Create(ctx, []string{"Test Note", "Test content"}) 778 + if err != nil { 779 + t.Fatalf("Failed to create test note: %v", err) 780 + } 781 + 782 + err = Edit(ctx, []string{"1"}) 783 + if err == nil { 784 + t.Error("Edit should fail when no editor is configured") 785 + } 786 + 787 + if !strings.Contains(err.Error(), "no editor configured") && !strings.Contains(err.Error(), "failed to open editor") { 788 + t.Errorf("Expected no editor or editor failure error, got: %v", err) 789 + } 790 + }) 791 + 792 + t.Run("handles editor command failure", func(t *testing.T) { 793 + _, cleanup := setupNoteTest(t) 794 + defer cleanup() 795 + 796 + originalEditor := os.Getenv("EDITOR") 797 + os.Setenv("EDITOR", "nonexistent-editor-12345") 798 + defer os.Setenv("EDITOR", originalEditor) 799 + 800 + ctx := context.Background() 801 + 802 + err := Create(ctx, []string{"Test Note", "Test content"}) 803 + if err != nil { 804 + t.Fatalf("Failed to create test note: %v", err) 805 + } 806 + 807 + err = Edit(ctx, []string{"1"}) 808 + if err == nil { 809 + t.Error("Edit should fail when editor command fails") 810 + } 811 + if !strings.Contains(err.Error(), "failed to open editor") { 812 + t.Errorf("Expected editor failure error, got: %v", err) 813 + } 814 + }) 815 + 816 + t.Run("edits note successfully with mocked editor", func(t *testing.T) { 817 + _, cleanup := setupNoteTest(t) 818 + defer cleanup() 819 + 820 + originalEditor := os.Getenv("EDITOR") 821 + os.Setenv("EDITOR", "test-editor") 822 + defer os.Setenv("EDITOR", originalEditor) 823 + 824 + ctx := context.Background() 825 + 826 + err := Create(ctx, []string{"Original Title", "Original content"}) 827 + if err != nil { 828 + t.Fatalf("Failed to create test note: %v", err) 829 + } 830 + 831 + handler, err := NewNoteHandler() 832 + if err != nil { 833 + t.Fatalf("NewNoteHandler failed: %v", err) 834 + } 835 + defer handler.Close() 836 + 837 + handler.openInEditorFunc = func(editor, filePath string) error { 838 + newContent := `# Updated Title 839 + 840 + This is updated content. 841 + 842 + <!-- Tags: updated, test -->` 843 + return os.WriteFile(filePath, []byte(newContent), 0644) 844 + } 845 + 846 + err = handler.editNote(ctx, 1) 847 + if err != nil { 848 + t.Errorf("Edit should succeed with mocked editor: %v", err) 849 + } 850 + 851 + note, err := handler.repos.Notes.Get(ctx, 1) 852 + if err != nil { 853 + t.Fatalf("Failed to get updated note: %v", err) 854 + } 855 + 856 + if note.Title != "Updated Title" { 857 + t.Errorf("Expected title 'Updated Title', got %q", note.Title) 858 + } 859 + 860 + if !strings.Contains(note.Content, "This is updated content") { 861 + t.Errorf("Expected content to contain 'This is updated content', got %q", note.Content) 862 + } 863 + 864 + expectedTags := []string{"updated", "test"} 865 + if len(note.Tags) != len(expectedTags) { 866 + t.Errorf("Expected %d tags, got %d", len(expectedTags), len(note.Tags)) 867 + } 868 + for i, tag := range expectedTags { 869 + if i >= len(note.Tags) || note.Tags[i] != tag { 870 + t.Errorf("Expected tag %q at index %d, got %q", tag, i, note.Tags[i]) 871 + } 872 + } 873 + }) 874 + 875 + t.Run("handles editor cancellation (no changes)", func(t *testing.T) { 876 + _, cleanup := setupNoteTest(t) 877 + defer cleanup() 878 + 879 + originalEditor := os.Getenv("EDITOR") 880 + os.Setenv("EDITOR", "test-editor") 881 + defer os.Setenv("EDITOR", originalEditor) 882 + 883 + ctx := context.Background() 884 + 885 + err := Create(ctx, []string{"Test Note", "Test content"}) 886 + if err != nil { 887 + t.Fatalf("Failed to create test note: %v", err) 888 + } 889 + 890 + handler, err := NewNoteHandler() 891 + if err != nil { 892 + t.Fatalf("NewNoteHandler failed: %v", err) 893 + } 894 + defer handler.Close() 895 + 896 + handler.openInEditorFunc = func(editor, filePath string) error { 897 + return nil 898 + } 899 + 900 + err = handler.editNote(ctx, 1) 901 + if err != nil { 902 + t.Errorf("Edit should handle cancellation gracefully: %v", err) 903 + } 904 + 905 + note, err := handler.repos.Notes.Get(ctx, 1) 906 + if err != nil { 907 + t.Fatalf("Failed to get note: %v", err) 908 + } 909 + 910 + if note.Title != "Test Note" { 911 + t.Errorf("Expected title 'Test Note', got %q", note.Title) 912 + } 913 + 914 + if note.Content != "Test content" { 915 + t.Errorf("Expected content 'Test content', got %q", note.Content) 916 + } 917 + }) 918 + }