From 8749ff3c03b16e8156edb0dfc96984cbbabe89ed Mon Sep 17 00:00:00 2001 From: DelphicOkami Date: Wed, 18 Mar 2026 22:22:04 +0000 Subject: [PATCH] Adding structure and testing around title support --- internal/models/note.go | 6 ++ internal/repository/notes.go | 7 +- internal/repository/notes_test.go | 157 +++++++++++++++++++++++++----- server/server.go | 6 +- 4 files changed, 147 insertions(+), 29 deletions(-) diff --git a/internal/models/note.go b/internal/models/note.go index 76865fe..fc72ca5 100644 --- a/internal/models/note.go +++ b/internal/models/note.go @@ -4,10 +4,16 @@ import ( "time" ) +const ( + NoteTitleMaxLength = 50 +) + // Note represents a single note with its metadata. type Note struct { // ID is the unique identifier of the note. ID int `json:"id"` + // Title is a short title for human identification of a note + Title string `json:"title"` // LastUpdate is the timestamp of when the note was last modified. LastUpdate time.Time `json:"last_update"` // Content is the actual text content of the note. diff --git a/internal/repository/notes.go b/internal/repository/notes.go index b05b787..3629438 100644 --- a/internal/repository/notes.go +++ b/internal/repository/notes.go @@ -13,13 +13,18 @@ type NoteRepository struct { store NoteStore } +type NoteUpdate struct { + Title string + Content string +} + // NewNoteRepository creates and returns a new NoteRepository instance. func NewNoteRepository(store NoteStore) *NoteRepository { return &NoteRepository{store: store} } // CreateNote creates a new note with the given content. -func (r *NoteRepository) CreateNote(ctx context.Context, content string) (models.Note, error) { +func (r *NoteRepository) CreateNote(ctx context.Context, title string, content string) (models.Note, error) { if content == "" { return models.Note{}, fmt.Errorf("content cannot be empty") } diff --git a/internal/repository/notes_test.go b/internal/repository/notes_test.go index 3f2d7e1..3aac8a7 100644 --- a/internal/repository/notes_test.go +++ b/internal/repository/notes_test.go @@ -6,51 +6,93 @@ import ( "context" "fmt" + "strings" "time" "git.hrafn.xyz/aether/notes/internal/models" "git.hrafn.xyz/aether/notes/internal/repository" ) +var fixedTestDate = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) //Date is set by synctest to this specific time + func TestCreateNote(t *testing.T) { testCases := []struct { name string + title string content string expectedNote models.Note expectedError bool }{ { name: "Good note", + title: "Good", content: "This is a good note with valid content.", expectedNote: models.Note{ ID: 1, - LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), //LastUpdate is set by synctest to this specific time + LastUpdate: fixedTestDate, + Title: "Good", Content: "This is a good note with valid content.", }, expectedError: false, }, { name: "Empty content", + title: "Empty", content: "", expectedNote: models.Note{}, expectedError: true, }, { name: "Long content", + title: "Long", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", expectedNote: models.Note{ ID: 2, - LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + LastUpdate: fixedTestDate, + Title: "Long", Content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", }, expectedError: false, }, + { + name: "Empty title", + title: "", + content: "Some content sure", + expectedNote: models.Note{}, + expectedError: true, + }, + { + name: "Fully empty", + title: "", + content: "", + expectedNote: models.Note{}, + expectedError: true, + }, + { + name: "Title too long", + title: strings.Repeat("A", models.NoteTitleMaxLength+1), + content: "", + expectedNote: models.Note{}, + expectedError: true, + }, + { + name: "Title accept max", + title: strings.Repeat("U", models.NoteTitleMaxLength), + content: `That's a whole lot of "U"s`, + expectedNote: models.Note{ + ID: 3, + LastUpdate: fixedTestDate, + Title: strings.Repeat("U", models.NoteTitleMaxLength), + Content: `That's a whole lot of "U"s`, + }, + expectedError: false, + }, } repo := repository.NewNoteRepository(&mockNoteStore{}) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { synctest.Test(t, func(t *testing.T) { - note, err := repo.CreateNote(context.Background(), tc.content) + note, err := repo.CreateNote(context.Background(), tc.title, tc.content) if tc.expectedError { if err == nil { t.Errorf("expected an error but got none") @@ -63,6 +105,9 @@ func TestCreateNote(t *testing.T) { if note.Content != tc.expectedNote.Content { t.Errorf("expected content %q but got %q", tc.expectedNote.Content, note.Content) } + if note.Title != tc.expectedNote.Title { + t.Errorf("expected title %q but got %q", tc.expectedNote.Title, note.Title) + } if note.ID != tc.expectedNote.ID { t.Errorf("expected ID %d but got %d", tc.expectedNote.ID, note.ID) } @@ -77,11 +122,11 @@ func TestCreateNote(t *testing.T) { func TestGetNotes(t *testing.T) { notes := []models.Note{ - {ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, - {ID: 3, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Third note"}, + {ID: 1, LastUpdate: fixedTestDate, Content: "First note", Title: "One"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note", Title: "Two"}, + {ID: 3, LastUpdate: fixedTestDate, Content: "Third note", Title: "Three"}, // 4th note was clearly deleted, :sadface: - {ID: 5, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Lorem ipsum note dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, + {ID: 5, LastUpdate: fixedTestDate, Content: "Lorem ipsum note dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", Title: "Lorem"}, } store := &mockNoteStore{Notes: notes} repo := repository.NewNoteRepository(store) @@ -157,14 +202,14 @@ func TestListNotes(t *testing.T) { { name: "Multiple notes", notes: []models.Note{ - {ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, - {ID: 3, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Third note"}, + {ID: 1, LastUpdate: fixedTestDate, Content: "First note", Title: "Seven"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note", Title: "Nine"}, + {ID: 3, LastUpdate: fixedTestDate, Content: "Third note", Title: "FourtyThree"}, }, expectedNotes: []models.Note{ - {ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, - {ID: 3, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Third note"}, + {ID: 1, LastUpdate: fixedTestDate, Content: "First note", Title: "Seven"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note", Title: "Nine"}, + {ID: 3, LastUpdate: fixedTestDate, Content: "Third note", Title: "FourtyThree"}, }, expectedError: false, }, @@ -190,6 +235,9 @@ func TestListNotes(t *testing.T) { if note.Content != tc.expectedNotes[i].Content { t.Errorf("expected content %q but got %q", tc.expectedNotes[i].Content, note.Content) } + if note.Title != tc.expectedNotes[i].Title { + t.Errorf("expected title %q but got %q", tc.expectedNotes[i].Title, note.Title) + } if note.ID != tc.expectedNotes[i].ID { t.Errorf("expected ID %d but got %d", tc.expectedNotes[i].ID, note.ID) } @@ -201,40 +249,91 @@ func TestListNotes(t *testing.T) { func TestUpdateNote(t *testing.T) { notes := []models.Note{ - {ID: 1, LastUpdate: time.Date(1984, 11, 20, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2018, 6, 8, 0, 0, 0, 0, time.UTC), Content: "Second note"}, + {ID: 1, LastUpdate: time.Date(1984, 11, 20, 0, 0, 0, 0, time.UTC), Content: "First note", Title: "Joe"}, + {ID: 2, LastUpdate: time.Date(2018, 6, 8, 0, 0, 0, 0, time.UTC), Content: "Second note", Title: "Momma"}, + {ID: 3, LastUpdate: time.Date(2018, 6, 8, 0, 0, 0, 0, time.UTC), Content: "Third note", Title: "So"}, + {ID: 4, LastUpdate: time.Date(2018, 6, 8, 0, 0, 0, 0, time.UTC), Content: "Four note", Title: "Lovely"}, + {ID: 5, LastUpdate: time.Date(2018, 6, 8, 0, 0, 0, 0, time.UTC), Content: "Fifth note", Title: "No, really"}, + {ID: 6, LastUpdate: time.Date(2018, 6, 8, 0, 0, 0, 0, time.UTC), Content: "six note", Title: "She is!"}, } store := &mockNoteStore{Notes: notes} repo := repository.NewNoteRepository(store) testcases := []struct { name string id int + title string content string expectedNote models.Note expectedError bool }{ { - name: "Update existing note", + name: "Update existing note content only", id: 1, + title: "", content: "Updated first note", expectedNote: models.Note{ ID: 1, - LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + LastUpdate: fixedTestDate, Content: "Updated first note", + Title: "Joe", }, expectedError: false, }, { name: "Update non-existing note", id: 999, + title: "Does it matter?", content: "This note does not exist", expectedNote: models.Note{}, expectedError: true, }, { - name: "Update with empty content", + name: "Update with empty content and title", id: 2, content: "", + title: "", + expectedNote: models.Note{}, + expectedError: true, + }, + { + name: "Update with unchanged content", + id: 3, + content: "Third note", + title: "So", + expectedNote: models.Note{}, + expectedError: true, + }, + { + name: "Update with title only", + id: 4, + content: "", + title: "Davey crocket", + expectedNote: models.Note{ + ID: 4, + LastUpdate: fixedTestDate, + Content: "Four note", + Title: "Davey crocket", + }, + expectedError: false, + }, + { + name: "Update with content only", + id: 5, + content: "Just internal screaming", + title: "", + expectedNote: models.Note{ + ID: 5, + LastUpdate: fixedTestDate, + Content: "Just internal screaming", + Title: "No, really", + }, + expectedError: false, + }, + { + name: "Update title too long", + id: 6, + content: "Kinda irrelavant, right?", + title: strings.Repeat("F", models.NoteTitleMaxLength+1), expectedNote: models.Note{}, expectedError: true, }, @@ -242,7 +341,10 @@ func TestUpdateNote(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { synctest.Test(t, func(t *testing.T) { - note, err := repo.UpdateNote(context.Background(), tc.id, tc.content) + note, err := repo.UpdateNote(context.Background(), tc.id, repository.NoteUpdate{ + Content: tc.content, + Title: tc.title, + }) if tc.expectedError { if err == nil { t.Errorf("expected an error but got none") @@ -255,6 +357,9 @@ func TestUpdateNote(t *testing.T) { if note.Content != tc.expectedNote.Content { t.Errorf("expected content %q but got %q", tc.expectedNote.Content, note.Content) } + if note.Title != tc.expectedNote.Title { + t.Errorf("expected title %q but got %q", tc.expectedNote.Title, note.Title) + } if note.ID != tc.expectedNote.ID { t.Errorf("expected ID %d but got %d", tc.expectedNote.ID, note.ID) } @@ -279,11 +384,11 @@ func TestDeleteNote(t *testing.T) { name: "Delete existing note", id: 1, setupNotes: []models.Note{ - {ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, + {ID: 1, LastUpdate: fixedTestDate, Content: "First note"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note"}, }, expectedNotes: []models.Note{ - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note"}, }, expectedError: false, }, @@ -291,12 +396,12 @@ func TestDeleteNote(t *testing.T) { name: "Delete non-existing note", id: 999, setupNotes: []models.Note{ - {ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, + {ID: 1, LastUpdate: fixedTestDate, Content: "First note"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note"}, }, expectedNotes: []models.Note{ - {ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, - {ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, + {ID: 1, LastUpdate: fixedTestDate, Content: "First note"}, + {ID: 2, LastUpdate: fixedTestDate, Content: "Second note"}, }, expectedError: true, }, diff --git a/server/server.go b/server/server.go index 6832d16..8ac4a8c 100644 --- a/server/server.go +++ b/server/server.go @@ -85,12 +85,13 @@ func (s *Server) getNotes(w http.ResponseWriter, r *http.Request) { func (s *Server) createNote(w http.ResponseWriter, r *http.Request) { note := struct { Content string `json:"content"` + Title string `json:"title"` }{} if err := json.NewDecoder(r.Body).Decode(¬e); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } - createdNote, err := s.noteRepo.CreateNote(r.Context(), note.Content) + createdNote, err := s.noteRepo.CreateNote(r.Context(), note.Content, note.Title) if err != nil { s.getErrorResponse(w, err) return @@ -122,12 +123,13 @@ func (s *Server) updateNoteByID(w http.ResponseWriter, r *http.Request) { } note := struct { Content string `json:"content"` + Title string `json:"title"` }{} if err := json.NewDecoder(r.Body).Decode(¬e); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } - updatedNote, err := s.noteRepo.UpdateNote(r.Context(), id, note.Content) + updatedNote, err := s.noteRepo.UpdateNote(r.Context(), id, repository.NoteUpdate{Content: note.Content, Title: note.Title}) if err != nil { s.getErrorResponse(w, err) return