tangled
alpha
login
or
join now
desertthunder.dev
/
noteleaf
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐
charm
leaflet
readability
golang
29
fork
atom
overview
issues
2
pulls
pipelines
feat: edit note command
desertthunder.dev
5 months ago
d9ec13f0
212162be
+244
-2
2 changed files
expand all
collapse all
unified
split
cmd
handlers
notes.go
notes_test.go
+23
-2
cmd/handlers/notes.go
···
6
"os"
7
"os/exec"
8
"path/filepath"
0
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
)
0
0
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)
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
}
0
0
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
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
}