From de6c9d6ae5e1e4f1114d1f88e72554abc346a2d4 Mon Sep 17 00:00:00 2001 From: DelphicOkami Date: Thu, 19 Mar 2026 00:44:37 +0000 Subject: [PATCH] Refactoring validation to belong to model --- internal/models/note.go | 19 +++ internal/models/note_errors.go | 42 +++++++ internal/models/note_test.go | 146 +++++++++++++++++++++++ internal/repository/errors.go | 35 ------ internal/repository/notes.go | 27 +---- internal/repository/repository_errors.go | 12 ++ 6 files changed, 224 insertions(+), 57 deletions(-) create mode 100644 internal/models/note_errors.go create mode 100644 internal/models/note_test.go delete mode 100644 internal/repository/errors.go create mode 100644 internal/repository/repository_errors.go diff --git a/internal/models/note.go b/internal/models/note.go index fc72ca5..2cd8ee5 100644 --- a/internal/models/note.go +++ b/internal/models/note.go @@ -19,3 +19,22 @@ type Note struct { // Content is the actual text content of the note. Content string `json:"content"` } + +func (n *Note) Validate() error { + errs := []error{} + if n.Title == "" { + errs = append(errs, &ErrEmptyNoteTitle{}) + } + if n.Content == "" { + errs = append(errs, &ErrEmptyNoteContent{}) + } + if len(errs) != 0 { + return &ErrNoteIncomplete{why: errs} + } + if titleLen := len(n.Title); titleLen > NoteTitleMaxLength { + return &ErrNoteTitleOverflow{ + length: titleLen, + } + } + return nil +} diff --git a/internal/models/note_errors.go b/internal/models/note_errors.go new file mode 100644 index 0000000..2ba7d21 --- /dev/null +++ b/internal/models/note_errors.go @@ -0,0 +1,42 @@ +package models + +import ( + "fmt" + "strings" +) + +type ErrEmptyNoteContent struct{} +type ErrEmptyNoteTitle struct{} +type ErrNoteIncomplete struct { + why []error +} +type ErrNoteTitleOverflow struct { + length int +} + +func (e *ErrEmptyNoteContent) Error() string { + return "content cannot be empty" +} + +func (e *ErrEmptyNoteTitle) Error() string { + return "title cannot be empty" +} + +func (e *ErrNoteIncomplete) Error() string { + if len(e.why) == 0 { + panic("ok so if we use this we need to know why it is incomplete") + } + s := make([]string, len(e.why)) + for i, err := range e.why { + s[i] = err.Error() + } + return "note incomplete: " + strings.Join(s, " & ") +} + +func (e *ErrNoteIncomplete) Unwrap() []error { + return e.why +} + +func (e *ErrNoteTitleOverflow) Error() string { + return fmt.Sprintf("title max length %d, %d provided", NoteTitleMaxLength, e.length) +} diff --git a/internal/models/note_test.go b/internal/models/note_test.go new file mode 100644 index 0000000..4be5053 --- /dev/null +++ b/internal/models/note_test.go @@ -0,0 +1,146 @@ +package models_test + +import ( + "errors" + "fmt" + "math/rand/v2" + "reflect" + "strings" + "testing" + "time" + + "git.hrafn.xyz/aether/notes/internal/models" +) + +func TestContentCompletionNoteValidation(t *testing.T) { + testcases := []struct { + name string + note models.Note + expectedError bool + expectedReasons []error + }{ + { + name: "Completely Empty", + note: models.Note{ + Title: "", + Content: "", + ID: 1, + LastUpdate: time.Now(), + }, + expectedError: true, + expectedReasons: []error{&models.ErrEmptyNoteContent{}, &models.ErrEmptyNoteTitle{}}, + }, + { + name: "Missing title", + note: models.Note{ + Title: "", + Content: "Here we go!", + ID: 1, + LastUpdate: time.Now(), + }, + expectedError: true, + expectedReasons: []error{&models.ErrEmptyNoteTitle{}}, + }, + { + name: "Missing content", + note: models.Note{ + Title: "I am not content with this content", + Content: "", + ID: 1, + LastUpdate: time.Now(), + }, + expectedError: true, + expectedReasons: []error{&models.ErrEmptyNoteContent{}}, + }, + { + name: "Good note", + note: models.Note{ + Title: "I am content with this content", + Content: "this content", + ID: 1, + LastUpdate: time.Now(), + }, + expectedError: false, + expectedReasons: []error{}, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + err := tc.note.Validate() + + // Early out when not needing to test because we're not expecting an error and we didn't get one + if !tc.expectedError && err == nil { + fmt.Printf("Test case '%s' passed.\n", tc.name) + return + } + + var expected *models.ErrNoteIncomplete + if !errors.As(err, &expected) && tc.expectedError { + t.Errorf("expected *ErrNoteIncomplete, got %T", err) + } + for _, reason := range tc.expectedReasons { + if !errors.Is(err, reason) { + t.Errorf(`expected error to contain "%v" but it was missing`, reason) + } + } + rv := reflect.ValueOf(err).Elem() + whyField := rv.FieldByName("why") + if whyField.Len() != len(tc.expectedReasons) { + t.Errorf("expected %d errors, got %d", len(tc.expectedReasons), whyField.Len()) + } + fmt.Printf("Test case '%s' passed.\n", tc.name) + }) + } +} + +func TestTitleLengthNoteValidation(t *testing.T) { + testcases := []struct { + name string + title string + expectedError bool + }{ + { + name: "Max valid title", + title: strings.Repeat("0", models.NoteTitleMaxLength), + expectedError: false, + }, + { + name: "Just over max valid title", + title: strings.Repeat("1", models.NoteTitleMaxLength + 1), + expectedError: true, + }, + { + name: "Double max valid title", + title: strings.Repeat("Bd", models.NoteTitleMaxLength), + expectedError: true, + }, + { + name: "Random overmax valid title length", + title: strings.Repeat("1", models.NoteTitleMaxLength + rand.IntN(150)), + expectedError: true, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + note := models.Note{ + Title: tc.title, + Content: "Perfectly valid content " + tc.title, + LastUpdate: time.Now(), + ID: 1, + } + err := note.Validate() + + if !tc.expectedError { + if err != nil { + t.Errorf("expecting no error, got %v", err) + } + } else { + var expected *models.ErrNoteTitleOverflow + if !errors.As(err, &expected) { + t.Errorf("expected *ErrNoteTitleOverflow, got %T", err) + } + } + fmt.Printf("Test case '%s' passed.\n", tc.name) + }) + } +} \ No newline at end of file diff --git a/internal/repository/errors.go b/internal/repository/errors.go deleted file mode 100644 index 8915d77..0000000 --- a/internal/repository/errors.go +++ /dev/null @@ -1,35 +0,0 @@ -package repository - -import ( - "fmt" - - "git.hrafn.xyz/aether/notes/internal/models" -) - -type EmptyContent struct {} -type EmptyTitle struct {} -type NoOP struct {} -type EmptyUpdate struct{} -type TitleOverflow struct { - length int -} - -func (e *EmptyContent) Error() string { - return "content cannot be empty" -} - -func (e *EmptyTitle) Error() string { - return "title cannot be empty" -} - -func (e *NoOP) Error() string { - return "no operation required" -} - -func (e *EmptyUpdate) Error() string { - return "update cannot be empty" -} - -func (e *TitleOverflow) Error() string { - return fmt.Sprintf("title max length %d, %d provided", models.NoteTitleMaxLength, e.length) -} \ No newline at end of file diff --git a/internal/repository/notes.go b/internal/repository/notes.go index 0140cf4..4218d23 100644 --- a/internal/repository/notes.go +++ b/internal/repository/notes.go @@ -29,7 +29,7 @@ func (r *NoteRepository) CreateNote(ctx context.Context, title string, content s Content: content, LastUpdate: time.Now(), } - if err := isNoteVaid(note); err != nil { + if err := note.Validate(); err != nil { return models.Note{}, err } return r.store.SaveNote(ctx, note) @@ -48,7 +48,7 @@ func (r *NoteRepository) ListNotes(ctx context.Context) ([]models.Note, error) { // UpdateNote updates the content of an existing note. func (r *NoteRepository) UpdateNote(ctx context.Context, id int, update NoteUpdate) (models.Note, error) { if update.Content == "" && update.Title == "" { - return models.Note{}, &EmptyUpdate{} + return models.Note{}, &ErrEmptyUpdate{} } note, err := r.store.GetNoteByID(ctx, id) if err != nil { @@ -56,7 +56,7 @@ func (r *NoteRepository) UpdateNote(ctx context.Context, id int, update NoteUpda } if update.Content == note.Content && update.Title == note.Title { - return models.Note{}, &NoOP{} + return models.Note{}, &ErrNoOP{} } if update.Content != "" { @@ -65,8 +65,8 @@ func (r *NoteRepository) UpdateNote(ctx context.Context, id int, update NoteUpda if update.Title != "" { note.Title = update.Title } - - if err := isNoteVaid(note); err != nil { + + if err := note.Validate(); err != nil { return models.Note{}, err } @@ -78,20 +78,3 @@ func (r *NoteRepository) UpdateNote(ctx context.Context, id int, update NoteUpda func (r *NoteRepository) DeleteNote(ctx context.Context, id int) error { return r.store.DeleteNoteByID(ctx, id) } - - -func isNoteVaid(note models.Note) error { - if note.Title == "" { - return &EmptyTitle{} - } - if note.Content == "" { - return &EmptyContent{} - } - if titleLen := len(note.Title); titleLen > models.NoteTitleMaxLength { - return &TitleOverflow{ - length: titleLen, - } - } - - return nil -} \ No newline at end of file diff --git a/internal/repository/repository_errors.go b/internal/repository/repository_errors.go new file mode 100644 index 0000000..9b07308 --- /dev/null +++ b/internal/repository/repository_errors.go @@ -0,0 +1,12 @@ +package repository + +type ErrNoOP struct{} +type ErrEmptyUpdate struct{} + +func (e *ErrNoOP) Error() string { + return "no operation required" +} + +func (e *ErrEmptyUpdate) Error() string { + return "update cannot be empty" +}