Adding structure and testing around title support

This commit is contained in:
2026-03-18 22:22:04 +00:00
parent 0935788b69
commit 8749ff3c03
4 changed files with 147 additions and 29 deletions

View File

@@ -4,10 +4,16 @@ import (
"time" "time"
) )
const (
NoteTitleMaxLength = 50
)
// Note represents a single note with its metadata. // Note represents a single note with its metadata.
type Note struct { type Note struct {
// ID is the unique identifier of the note. // ID is the unique identifier of the note.
ID int `json:"id"` 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 is the timestamp of when the note was last modified.
LastUpdate time.Time `json:"last_update"` LastUpdate time.Time `json:"last_update"`
// Content is the actual text content of the note. // Content is the actual text content of the note.

View File

@@ -13,13 +13,18 @@ type NoteRepository struct {
store NoteStore store NoteStore
} }
type NoteUpdate struct {
Title string
Content string
}
// NewNoteRepository creates and returns a new NoteRepository instance. // NewNoteRepository creates and returns a new NoteRepository instance.
func NewNoteRepository(store NoteStore) *NoteRepository { func NewNoteRepository(store NoteStore) *NoteRepository {
return &NoteRepository{store: store} return &NoteRepository{store: store}
} }
// CreateNote creates a new note with the given content. // 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 == "" { if content == "" {
return models.Note{}, fmt.Errorf("content cannot be empty") return models.Note{}, fmt.Errorf("content cannot be empty")
} }

View File

@@ -6,51 +6,93 @@ import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"git.hrafn.xyz/aether/notes/internal/models" "git.hrafn.xyz/aether/notes/internal/models"
"git.hrafn.xyz/aether/notes/internal/repository" "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) { func TestCreateNote(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
title string
content string content string
expectedNote models.Note expectedNote models.Note
expectedError bool expectedError bool
}{ }{
{ {
name: "Good note", name: "Good note",
title: "Good",
content: "This is a good note with valid content.", content: "This is a good note with valid content.",
expectedNote: models.Note{ expectedNote: models.Note{
ID: 1, 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.", Content: "This is a good note with valid content.",
}, },
expectedError: false, expectedError: false,
}, },
{ {
name: "Empty content", name: "Empty content",
title: "Empty",
content: "", content: "",
expectedNote: models.Note{}, expectedNote: models.Note{},
expectedError: true, expectedError: true,
}, },
{ {
name: "Long content", 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.", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
expectedNote: models.Note{ expectedNote: models.Note{
ID: 2, 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.", Content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
}, },
expectedError: false, 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{}) repo := repository.NewNoteRepository(&mockNoteStore{})
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
synctest.Test(t, 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 tc.expectedError {
if err == nil { if err == nil {
t.Errorf("expected an error but got none") t.Errorf("expected an error but got none")
@@ -63,6 +105,9 @@ func TestCreateNote(t *testing.T) {
if note.Content != tc.expectedNote.Content { if note.Content != tc.expectedNote.Content {
t.Errorf("expected content %q but got %q", tc.expectedNote.Content, note.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 { if note.ID != tc.expectedNote.ID {
t.Errorf("expected ID %d but got %d", tc.expectedNote.ID, note.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) { func TestGetNotes(t *testing.T) {
notes := []models.Note{ notes := []models.Note{
{ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, {ID: 1, LastUpdate: fixedTestDate, Content: "First note", Title: "One"},
{ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, {ID: 2, LastUpdate: fixedTestDate, Content: "Second note", Title: "Two"},
{ID: 3, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Third note"}, {ID: 3, LastUpdate: fixedTestDate, Content: "Third note", Title: "Three"},
// 4th note was clearly deleted, :sadface: // 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} store := &mockNoteStore{Notes: notes}
repo := repository.NewNoteRepository(store) repo := repository.NewNoteRepository(store)
@@ -157,14 +202,14 @@ func TestListNotes(t *testing.T) {
{ {
name: "Multiple notes", name: "Multiple notes",
notes: []models.Note{ notes: []models.Note{
{ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, {ID: 1, LastUpdate: fixedTestDate, Content: "First note", Title: "Seven"},
{ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, {ID: 2, LastUpdate: fixedTestDate, Content: "Second note", Title: "Nine"},
{ID: 3, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Third note"}, {ID: 3, LastUpdate: fixedTestDate, Content: "Third note", Title: "FourtyThree"},
}, },
expectedNotes: []models.Note{ expectedNotes: []models.Note{
{ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, {ID: 1, LastUpdate: fixedTestDate, Content: "First note", Title: "Seven"},
{ID: 2, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Second note"}, {ID: 2, LastUpdate: fixedTestDate, Content: "Second note", Title: "Nine"},
{ID: 3, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "Third note"}, {ID: 3, LastUpdate: fixedTestDate, Content: "Third note", Title: "FourtyThree"},
}, },
expectedError: false, expectedError: false,
}, },
@@ -190,6 +235,9 @@ func TestListNotes(t *testing.T) {
if note.Content != tc.expectedNotes[i].Content { if note.Content != tc.expectedNotes[i].Content {
t.Errorf("expected content %q but got %q", tc.expectedNotes[i].Content, note.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 { if note.ID != tc.expectedNotes[i].ID {
t.Errorf("expected ID %d but got %d", tc.expectedNotes[i].ID, note.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) { func TestUpdateNote(t *testing.T) {
notes := []models.Note{ notes := []models.Note{
{ID: 1, LastUpdate: time.Date(1984, 11, 20, 0, 0, 0, 0, time.UTC), Content: "First 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"}, {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} store := &mockNoteStore{Notes: notes}
repo := repository.NewNoteRepository(store) repo := repository.NewNoteRepository(store)
testcases := []struct { testcases := []struct {
name string name string
id int id int
title string
content string content string
expectedNote models.Note expectedNote models.Note
expectedError bool expectedError bool
}{ }{
{ {
name: "Update existing note", name: "Update existing note content only",
id: 1, id: 1,
title: "",
content: "Updated first note", content: "Updated first note",
expectedNote: models.Note{ expectedNote: models.Note{
ID: 1, ID: 1,
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: fixedTestDate,
Content: "Updated first note", Content: "Updated first note",
Title: "Joe",
}, },
expectedError: false, expectedError: false,
}, },
{ {
name: "Update non-existing note", name: "Update non-existing note",
id: 999, id: 999,
title: "Does it matter?",
content: "This note does not exist", content: "This note does not exist",
expectedNote: models.Note{}, expectedNote: models.Note{},
expectedError: true, expectedError: true,
}, },
{ {
name: "Update with empty content", name: "Update with empty content and title",
id: 2, id: 2,
content: "", 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{}, expectedNote: models.Note{},
expectedError: true, expectedError: true,
}, },
@@ -242,7 +341,10 @@ func TestUpdateNote(t *testing.T) {
for _, tc := range testcases { for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
synctest.Test(t, 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 tc.expectedError {
if err == nil { if err == nil {
t.Errorf("expected an error but got none") t.Errorf("expected an error but got none")
@@ -255,6 +357,9 @@ func TestUpdateNote(t *testing.T) {
if note.Content != tc.expectedNote.Content { if note.Content != tc.expectedNote.Content {
t.Errorf("expected content %q but got %q", tc.expectedNote.Content, note.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 { if note.ID != tc.expectedNote.ID {
t.Errorf("expected ID %d but got %d", tc.expectedNote.ID, note.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", name: "Delete existing note",
id: 1, id: 1,
setupNotes: []models.Note{ setupNotes: []models.Note{
{ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, {ID: 1, LastUpdate: fixedTestDate, Content: "First 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"},
}, },
expectedNotes: []models.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, expectedError: false,
}, },
@@ -291,12 +396,12 @@ func TestDeleteNote(t *testing.T) {
name: "Delete non-existing note", name: "Delete non-existing note",
id: 999, id: 999,
setupNotes: []models.Note{ setupNotes: []models.Note{
{ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, {ID: 1, LastUpdate: fixedTestDate, Content: "First 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"},
}, },
expectedNotes: []models.Note{ expectedNotes: []models.Note{
{ID: 1, LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), Content: "First note"}, {ID: 1, LastUpdate: fixedTestDate, Content: "First 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: true, expectedError: true,
}, },

View File

@@ -85,12 +85,13 @@ func (s *Server) getNotes(w http.ResponseWriter, r *http.Request) {
func (s *Server) createNote(w http.ResponseWriter, r *http.Request) { func (s *Server) createNote(w http.ResponseWriter, r *http.Request) {
note := struct { note := struct {
Content string `json:"content"` Content string `json:"content"`
Title string `json:"title"`
}{} }{}
if err := json.NewDecoder(r.Body).Decode(&note); err != nil { if err := json.NewDecoder(r.Body).Decode(&note); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest) http.Error(w, "invalid request body", http.StatusBadRequest)
return return
} }
createdNote, err := s.noteRepo.CreateNote(r.Context(), note.Content) createdNote, err := s.noteRepo.CreateNote(r.Context(), note.Content, note.Title)
if err != nil { if err != nil {
s.getErrorResponse(w, err) s.getErrorResponse(w, err)
return return
@@ -122,12 +123,13 @@ func (s *Server) updateNoteByID(w http.ResponseWriter, r *http.Request) {
} }
note := struct { note := struct {
Content string `json:"content"` Content string `json:"content"`
Title string `json:"title"`
}{} }{}
if err := json.NewDecoder(r.Body).Decode(&note); err != nil { if err := json.NewDecoder(r.Body).Decode(&note); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest) http.Error(w, "invalid request body", http.StatusBadRequest)
return 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 { if err != nil {
s.getErrorResponse(w, err) s.getErrorResponse(w, err)
return return