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