From f070674cdbde30da7744c12795dce439e2874803 Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Thu, 2 Apr 2026 17:24:18 +0200 Subject: [PATCH 1/4] feat(uow): transaction share by retun tx --- core/handlers/create_note_handler.go | 8 +--- persistence/repositories/note_repository.go | 9 +++-- persistence/repositories/tag_repository.go | 11 ++--- persistence/repositories/uow.go | 45 ++++++++++----------- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/core/handlers/create_note_handler.go b/core/handlers/create_note_handler.go index 1fe121d..81a443d 100644 --- a/core/handlers/create_note_handler.go +++ b/core/handlers/create_note_handler.go @@ -17,13 +17,9 @@ type createNoteCommand struct { Vars map[string]string `json:"vars"` } -type CreateNoteHandler struct { - uow *repositories.UnitOfWork -} +type CreateNoteHandler struct {} -func NewCreateNoteHandler(uow *repositories.UnitOfWork) *CreateNoteHandler { - return &CreateNoteHandler{uow: uow} -} +func NewCreateNoteHandler(uow *repositories.UnitOfWork) *CreateNoteHandler func (h CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) { var cmd createNoteCommand diff --git a/persistence/repositories/note_repository.go b/persistence/repositories/note_repository.go index 0c858b7..3ad8b06 100644 --- a/persistence/repositories/note_repository.go +++ b/persistence/repositories/note_repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "database/sql" ) type NoteRepository interface { @@ -9,11 +10,11 @@ type NoteRepository interface { } type noteRepository struct { - dbCtx DBContext + Transaction *sql.Tx } -func NewNoteRepository(dbContext DBContext) NoteRepository { - return ¬eRepository{dbCtx: dbContext} +func NewNoteRepository(tx *sql.Tx) NoteRepository { + return ¬eRepository{Transaction: tx} } func (r *noteRepository) Upsert(ctx context.Context, title string, path string, slug string) error { @@ -24,6 +25,6 @@ func (r *noteRepository) Upsert(ctx context.Context, title string, path string, SET title = EXCLUDED.title, path = EXCLUDED.path; ` - _, err := r.dbCtx.ExecContext(ctx, query, title, path, slug) + _, err := r.Transaction.ExecContext(ctx, query, title, path, slug) return err } diff --git a/persistence/repositories/tag_repository.go b/persistence/repositories/tag_repository.go index 3f8b950..f1446cf 100644 --- a/persistence/repositories/tag_repository.go +++ b/persistence/repositories/tag_repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "database/sql" "strings" ) @@ -10,11 +11,11 @@ type TagRepository interface { } type tagRepository struct { - dbContext DBContext + Transaction *sql.Tx } -func NewTagRepository(dbContext DBContext) TagRepository { - return &tagRepository{dbContext: dbContext} +func NewTagRepository(tx *sql.Tx) TagRepository { +return &tagRepository{Transaction: tx} } func (r *tagRepository) Upsert(ctx context.Context, names []string) error { @@ -36,7 +37,7 @@ func (r *tagRepository) Upsert(ctx context.Context, names []string) error { "SELECT name FROM input " + "ON CONFLICT(name) DO NOTHING;" - _, err := r.dbContext.ExecContext(ctx, query, args) + _, err := r.Transaction.ExecContext(ctx, query, args) if err != nil { return err } @@ -64,7 +65,7 @@ func (r *tagRepository) UpsertNoteTags(noteID int64, tagIDs []int64) error { "ON CONFLICT(note_id, tag_id) DO NOTHING" + "SELECT note_id, tag_id FROM input;" - _, err := r.dbContext.ExecContext(context.Background(), query, args) + _, err := r.Transaction.ExecContext(context.Background(), query, args) if err != nil { return err } diff --git a/persistence/repositories/uow.go b/persistence/repositories/uow.go index 4213763..cbec578 100644 --- a/persistence/repositories/uow.go +++ b/persistence/repositories/uow.go @@ -1,40 +1,39 @@ + package repositories import ( - "context" "database/sql" ) -type DBContext interface { - ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) - QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row -} - type UnitOfWork struct { db *sql.DB + Transaction *sql.Tx } func NewUnitOfWork(db *sql.DB) *UnitOfWork { return &UnitOfWork{db: db} } -func (u *UnitOfWork) Execute(ctx context.Context, fn func(tx *sql.Tx) error) (err error) { - transcation, err := u.db.BeginTx(ctx, nil) +func (u *UnitOfWork) Begin() (tx *sql.Tx, err error) { + tx, err = u.db.Begin() if err != nil { - return err + return nil, err } - - defer func() { - if err != nil { - _ = transcation.Rollback() - } - }() - - if err = fn(transcation); err != nil { - return err - } - - err = transcation.Commit() - return err + u.Transaction = tx + return tx, nil } + +func (u *UnitOfWork) Commit() error { + if u.Transaction == nil { + return nil + } + return u.Transaction.Commit() +} + +func (u *UnitOfWork) Rollback() error { + if u.Transaction == nil { + return nil + } + return u.Transaction.Rollback() +} + From 884e9cd207f026d96526c04fa1195d4b50966389 Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Thu, 2 Apr 2026 17:56:35 +0200 Subject: [PATCH 2/4] feat(uow): added filestore for cross boundary tx --- core/files/doc.go | 2 - core/files/file_handler.go | 19 --------- core/handlers/create_note_handler.go | 64 ++++++++++++++-------------- persistence/repositories/uow.go | 12 +++--- persistence/store/doc.go | 2 + persistence/store/file_store.go | 46 ++++++++++++++++++++ 6 files changed, 87 insertions(+), 58 deletions(-) delete mode 100644 core/files/doc.go delete mode 100644 core/files/file_handler.go create mode 100644 persistence/store/doc.go create mode 100644 persistence/store/file_store.go diff --git a/core/files/doc.go b/core/files/doc.go deleted file mode 100644 index a4727ba..0000000 --- a/core/files/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package files provides utilities for working with files and directories. -package files diff --git a/core/files/file_handler.go b/core/files/file_handler.go deleted file mode 100644 index 3ba059b..0000000 --- a/core/files/file_handler.go +++ /dev/null @@ -1,19 +0,0 @@ -package files - -import "os" - -func WriteToFile(path string, data []byte) (filePath string, err error) { - if checkIfFileExists(path) { - return path, nil - } - err = os.WriteFile(path, data, 0o644) - if err != nil { - return "", err - } - return path, nil -} - -func checkIfFileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} diff --git a/core/handlers/create_note_handler.go b/core/handlers/create_note_handler.go index 81a443d..4225430 100644 --- a/core/handlers/create_note_handler.go +++ b/core/handlers/create_note_handler.go @@ -2,11 +2,10 @@ package handlers import ( "context" - "database/sql" "encoding/json" - "os" "github.com/KristianJBorgwarth/dendrite.daemon/core/frontmatter" + "github.com/KristianJBorgwarth/dendrite.daemon/core/template" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" ) @@ -17,55 +16,56 @@ type createNoteCommand struct { Vars map[string]string `json:"vars"` } -type CreateNoteHandler struct {} +type CreateNoteHandler struct { + uow *repositories.UnitOfWork +} -func NewCreateNoteHandler(uow *repositories.UnitOfWork) *CreateNoteHandler +func NewCreateNoteHandler(uow *repositories.UnitOfWork) *CreateNoteHandler { + return &CreateNoteHandler{uow: uow} +} -func (h CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) { +func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) { var cmd createNoteCommand - if err := json.Unmarshal(raw, &cmd); err != nil { return nil, err } - data, err := h.getTemplate(cmd.TemplatePath) + data, err := template.GenerateTemplate(cmd.TemplatePath) if err != nil { return nil, err } + if data == nil { + data = []byte("---\ntitle: " + cmd.Title + "\ntags: []\n---\n") + } + tags, err := frontmatter.ParseTags(data) if err != nil { return nil, err } - err = h.uow.Execute(ctx, func(tx *sql.Tx) error { - tagRepo := repositories.NewTagRepository(tx) - noteRepo := repositories.NewNoteRepository(tx) - - if err = tagRepo.Upsert(ctx, tags); err != nil { - return err - } - - if err = noteRepo.Upsert(ctx, cmd.Title, cmd.Path, frontmatter.Slugify(cmd.Title)); err != nil { - return err - } - - return nil - }) + tx, err := h.uow.Begin() if err != nil { return nil, err } + defer h.uow.Rollback() + + tagRepo := repositories.NewTagRepository(tx) + noteRepo := repositories.NewNoteRepository(tx) + + if err = tagRepo.Upsert(ctx, tags); err != nil { + return nil, err + } + + if err = noteRepo.Upsert(ctx, cmd.Title, cmd.Path, frontmatter.Slugify(cmd.Title)); err != nil { + return nil, err + } + + h.uow.FileStore.Stage(cmd.Path, data) + + if err = h.uow.Commit(); err != nil { + return nil, err + } return cmd.Path, nil } - -func (h *CreateNoteHandler) getTemplate(templatePath string) ([]byte, error) { - if templatePath == "" { - return nil, nil - } - data, err := os.ReadFile(templatePath) - if err != nil { - return nil, err - } - return data, nil -} diff --git a/persistence/repositories/uow.go b/persistence/repositories/uow.go index cbec578..ff98c6b 100644 --- a/persistence/repositories/uow.go +++ b/persistence/repositories/uow.go @@ -1,13 +1,15 @@ - package repositories import ( "database/sql" + + "github.com/KristianJBorgwarth/dendrite.daemon/persistence/store" ) type UnitOfWork struct { db *sql.DB Transaction *sql.Tx + FileStore *store.FileStore } func NewUnitOfWork(db *sql.DB) *UnitOfWork { @@ -30,10 +32,10 @@ func (u *UnitOfWork) Commit() error { return u.Transaction.Commit() } -func (u *UnitOfWork) Rollback() error { +func (u *UnitOfWork) Rollback() { if u.Transaction == nil { - return nil + return } - return u.Transaction.Rollback() + u.Transaction.Rollback() + u.FileStore.Rollback() } - diff --git a/persistence/store/doc.go b/persistence/store/doc.go new file mode 100644 index 0000000..a60b605 --- /dev/null +++ b/persistence/store/doc.go @@ -0,0 +1,2 @@ +// Package store provides access to any in memory stores related to files and index +package store diff --git a/persistence/store/file_store.go b/persistence/store/file_store.go new file mode 100644 index 0000000..9f77bc3 --- /dev/null +++ b/persistence/store/file_store.go @@ -0,0 +1,46 @@ +package store + +import "os" + +type FileStore struct { + staged []stagedFile + commited []string +} + +type stagedFile struct { + path string + data []byte +} + +func (fs *FileStore) Stage(path string, data []byte) { + fs.staged = append(fs.staged, stagedFile{path: path, data: data}) +} + +func (fs *FileStore) Flush() error { + for _, file := range fs.staged { + if fs.fileExists(file.path) { + continue + } + if err := os.WriteFile(file.path, file.data, 0o644); err != nil { + return err + } + fs.commited = append(fs.commited, file.path) + } + fs.staged = nil + return nil +} + +func (fs *FileStore) Rollback() error { + for _, path := range fs.commited { + if err := os.Remove(path); err != nil { + return err + } + } + fs.commited = nil + return nil +} + +func (fs *FileStore) fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} From d9cc1c6ec7d1c251d38e650e6e0f8400cc42e919 Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Thu, 2 Apr 2026 18:34:37 +0200 Subject: [PATCH 3/4] test(create_note): integration test for create note --- persistence/repositories/note_repository.go | 4 +- persistence/repositories/tag_repository.go | 18 +-- persistence/repositories/uow.go | 20 ++- persistence/store/file_store.go | 12 +- .../create_note_handler_test.go | 136 ++++++++++++++---- 5 files changed, 134 insertions(+), 56 deletions(-) diff --git a/persistence/repositories/note_repository.go b/persistence/repositories/note_repository.go index 3ad8b06..e9d9444 100644 --- a/persistence/repositories/note_repository.go +++ b/persistence/repositories/note_repository.go @@ -19,8 +19,8 @@ func NewNoteRepository(tx *sql.Tx) NoteRepository { func (r *noteRepository) Upsert(ctx context.Context, title string, path string, slug string) error { query := ` - INSERT INTO notes (title, path, slug) - VALUES ($1, $2, $3) + INSERT INTO notes (title, path, slug, created_at, updated_at) + VALUES (?, ?, ?, datetime('now'), datetime('now')) ON CONFLICT (slug) DO UPDATE SET title = EXCLUDED.title, path = EXCLUDED.path; diff --git a/persistence/repositories/tag_repository.go b/persistence/repositories/tag_repository.go index f1446cf..a49257e 100644 --- a/persistence/repositories/tag_repository.go +++ b/persistence/repositories/tag_repository.go @@ -15,7 +15,7 @@ type tagRepository struct { } func NewTagRepository(tx *sql.Tx) TagRepository { -return &tagRepository{Transaction: tx} + return &tagRepository{Transaction: tx} } func (r *tagRepository) Upsert(ctx context.Context, names []string) error { @@ -31,18 +31,10 @@ func (r *tagRepository) Upsert(ctx context.Context, names []string) error { args = append(args, name) } - query := "WITH input(name) AS (VALUES " + - strings.Join(placeholders, ",") + - ") INSERT INTO tags(name) " + - "SELECT name FROM input " + - "ON CONFLICT(name) DO NOTHING;" + query := "INSERT OR IGNORE INTO tags(name) VALUES " + strings.Join(placeholders, ",") - _, err := r.Transaction.ExecContext(ctx, query, args) - if err != nil { - return err - } - - return nil + _, err := r.Transaction.ExecContext(ctx, query, args...) + return err } func (r *tagRepository) UpsertNoteTags(noteID int64, tagIDs []int64) error { @@ -65,7 +57,7 @@ func (r *tagRepository) UpsertNoteTags(noteID int64, tagIDs []int64) error { "ON CONFLICT(note_id, tag_id) DO NOTHING" + "SELECT note_id, tag_id FROM input;" - _, err := r.Transaction.ExecContext(context.Background(), query, args) + _, err := r.Transaction.ExecContext(context.Background(), query, args...) if err != nil { return err } diff --git a/persistence/repositories/uow.go b/persistence/repositories/uow.go index ff98c6b..49db413 100644 --- a/persistence/repositories/uow.go +++ b/persistence/repositories/uow.go @@ -7,13 +7,13 @@ import ( ) type UnitOfWork struct { - db *sql.DB + db *sql.DB Transaction *sql.Tx - FileStore *store.FileStore + FileStore *store.FileStore } func NewUnitOfWork(db *sql.DB) *UnitOfWork { - return &UnitOfWork{db: db} + return &UnitOfWork{db: db, FileStore: store.NewFileStore()} } func (u *UnitOfWork) Begin() (tx *sql.Tx, err error) { @@ -26,10 +26,18 @@ func (u *UnitOfWork) Begin() (tx *sql.Tx, err error) { } func (u *UnitOfWork) Commit() error { - if u.Transaction == nil { - return nil + if err := u.FileStore.Flush(); err != nil { + u.Transaction.Rollback() + u.Transaction = nil + return err } - return u.Transaction.Commit() + if err := u.Transaction.Commit(); err != nil { + u.FileStore.Rollback() + u.Transaction = nil + return err + } + u.Transaction = nil + return nil } func (u *UnitOfWork) Rollback() { diff --git a/persistence/store/file_store.go b/persistence/store/file_store.go index 9f77bc3..6229823 100644 --- a/persistence/store/file_store.go +++ b/persistence/store/file_store.go @@ -4,7 +4,7 @@ import "os" type FileStore struct { staged []stagedFile - commited []string + committed []string } type stagedFile struct { @@ -12,6 +12,10 @@ type stagedFile struct { data []byte } +func NewFileStore() *FileStore { + return &FileStore{} +} + func (fs *FileStore) Stage(path string, data []byte) { fs.staged = append(fs.staged, stagedFile{path: path, data: data}) } @@ -24,19 +28,19 @@ func (fs *FileStore) Flush() error { if err := os.WriteFile(file.path, file.data, 0o644); err != nil { return err } - fs.commited = append(fs.commited, file.path) + fs.committed = append(fs.committed, file.path) } fs.staged = nil return nil } func (fs *FileStore) Rollback() error { - for _, path := range fs.commited { + for _, path := range fs.committed { if err := os.Remove(path); err != nil { return err } } - fs.commited = nil + fs.committed = nil return nil } diff --git a/test/test_integration/create_note_handler_test.go b/test/test_integration/create_note_handler_test.go index e210a42..653084f 100644 --- a/test/test_integration/create_note_handler_test.go +++ b/test/test_integration/create_note_handler_test.go @@ -2,42 +2,116 @@ package integration_test import ( "encoding/json" + "os" + "path/filepath" "testing" "github.com/KristianJBorgwarth/dendrite.daemon/core/handlers" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestCreateNoteHandlerOnSucess(t *testing.T) { - // Arrange +func newCreateNoteHandler() *handlers.CreateNoteHandler { uow := repositories.NewUnitOfWork(Fixture.DB) - - handler := handlers.NewCreateNoteHandler(uow) - - requestParams := struct { - Title string `json:"title"` - Content string `json:"content"` - }{ - Title: "Test Note", - Content: "This is a test note.", - } - - requestParamsBytes, err := json.Marshal(requestParams) - if err != nil { - t.Fatalf("failed to marshal request params: %v", err) - } - - request := CreateTestRequest("createNote", 1, requestParamsBytes) - - // Act - response, err := handler.Handle(Fixture.TestContext, request.Params) - - // Assert - if err != nil { - t.Fatalf("handler returned an error: %v", err) - } - - if response != nil { - t.Fatalf("expected response to be nil, got: %v", response) - } + return handlers.NewCreateNoteHandler(uow) +} + +func TestCreateNoteHandler_NoTemplate_CreatesNoteFileAndReturnsPath(t *testing.T) { + handler := newCreateNoteHandler() + notePath := filepath.Join(t.TempDir(), "my-note.md") + + params, _ := json.Marshal(map[string]any{ + "title": "My Note", + "path": notePath, + }) + + result, err := handler.Handle(Fixture.TestContext, params) + + require.NoError(t, err) + assert.Equal(t, notePath, result) + + var count int + require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM notes WHERE slug = ?`, "my-note").Scan(&count)) + assert.Equal(t, 1, count) + + _, statErr := os.Stat(notePath) + assert.NoError(t, statErr) +} + +func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t *testing.T) { + handler := newCreateNoteHandler() + dir := t.TempDir() + + templatePath := filepath.Join(dir, "template.md") + require.NoError(t, os.WriteFile(templatePath, []byte("---\ntitle: Template\ntags: [go, testing]\n---\n"), 0o644)) + + notePath := filepath.Join(dir, "templated-note.md") + params, _ := json.Marshal(map[string]any{ + "title": "Templated Note", + "path": notePath, + "templatePath": templatePath, + }) + + result, err := handler.Handle(Fixture.TestContext, params) + + require.NoError(t, err) + assert.Equal(t, notePath, result) + + var noteCount int + require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM notes WHERE slug = ?`, "templated-note").Scan(¬eCount)) + assert.Equal(t, 1, noteCount) + + rows, err := Fixture.DB.Query(`SELECT name FROM tags WHERE name IN ('go', 'testing')`) + require.NoError(t, err) + defer rows.Close() + + var tags []string + for rows.Next() { + var name string + require.NoError(t, rows.Scan(&name)) + tags = append(tags, name) + } + assert.Len(t, tags, 2) + + _, statErr := os.Stat(notePath) + assert.NoError(t, statErr) +} + +func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { + dir := t.TempDir() + + path1 := filepath.Join(dir, "dup-note.md") + params1, _ := json.Marshal(map[string]any{"title": "Dup Note", "path": path1}) + _, err := newCreateNoteHandler().Handle(Fixture.TestContext, params1) + require.NoError(t, err) + + path2 := filepath.Join(dir, "dup-note-moved.md") + params2, _ := json.Marshal(map[string]any{"title": "Dup Note", "path": path2}) + _, err = newCreateNoteHandler().Handle(Fixture.TestContext, params2) + require.NoError(t, err) + + var count int + require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM notes WHERE slug = ?`, "dup-note").Scan(&count)) + assert.Equal(t, 1, count) + + var path string + require.NoError(t, Fixture.DB.QueryRow(`SELECT path FROM notes WHERE slug = ?`, "dup-note").Scan(&path)) + assert.Equal(t, path2, path) +} + +func TestCreateNoteHandler_InvalidJSON_ReturnsError(t *testing.T) { + _, err := newCreateNoteHandler().Handle(Fixture.TestContext, json.RawMessage(`{invalid json}`)) + assert.Error(t, err) +} + +func TestCreateNoteHandler_NonExistentTemplatePath_ReturnsError(t *testing.T) { + params, _ := json.Marshal(map[string]any{ + "title": "Ghost Note", + "path": filepath.Join(t.TempDir(), "ghost.md"), + "templatePath": "/non/existent/template.md", + }) + + _, err := newCreateNoteHandler().Handle(Fixture.TestContext, params) + assert.Error(t, err) } From 89dcc7206c62c07708686d9baea22e8d67207663 Mon Sep 17 00:00:00 2001 From: Kristian Borgwarth <10348902@pm.me> Date: Thu, 2 Apr 2026 18:45:07 +0200 Subject: [PATCH 4/4] sm(test): aaa --- .../create_note_handler_test.go | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/test/test_integration/create_note_handler_test.go b/test/test_integration/create_note_handler_test.go index 653084f..093539b 100644 --- a/test/test_integration/create_note_handler_test.go +++ b/test/test_integration/create_note_handler_test.go @@ -18,16 +18,18 @@ func newCreateNoteHandler() *handlers.CreateNoteHandler { } func TestCreateNoteHandler_NoTemplate_CreatesNoteFileAndReturnsPath(t *testing.T) { + // Arrange handler := newCreateNoteHandler() notePath := filepath.Join(t.TempDir(), "my-note.md") - params, _ := json.Marshal(map[string]any{ "title": "My Note", "path": notePath, }) + // Act result, err := handler.Handle(Fixture.TestContext, params) + // Assert require.NoError(t, err) assert.Equal(t, notePath, result) @@ -40,6 +42,7 @@ func TestCreateNoteHandler_NoTemplate_CreatesNoteFileAndReturnsPath(t *testing.T } func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t *testing.T) { + // Arrange handler := newCreateNoteHandler() dir := t.TempDir() @@ -53,8 +56,10 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t "templatePath": templatePath, }) + // Act result, err := handler.Handle(Fixture.TestContext, params) + // Assert require.NoError(t, err) assert.Equal(t, notePath, result) @@ -79,6 +84,7 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t } func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { + // Arrange dir := t.TempDir() path1 := filepath.Join(dir, "dup-note.md") @@ -88,7 +94,11 @@ func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { path2 := filepath.Join(dir, "dup-note-moved.md") params2, _ := json.Marshal(map[string]any{"title": "Dup Note", "path": path2}) + + // Act _, err = newCreateNoteHandler().Handle(Fixture.TestContext, params2) + + // Assert require.NoError(t, err) var count int @@ -101,17 +111,28 @@ func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { } func TestCreateNoteHandler_InvalidJSON_ReturnsError(t *testing.T) { - _, err := newCreateNoteHandler().Handle(Fixture.TestContext, json.RawMessage(`{invalid json}`)) + // Arrange + handler := newCreateNoteHandler() + + // Act + _, err := handler.Handle(Fixture.TestContext, json.RawMessage(`{invalid json}`)) + + // Assert assert.Error(t, err) } func TestCreateNoteHandler_NonExistentTemplatePath_ReturnsError(t *testing.T) { + // Arrange + handler := newCreateNoteHandler() params, _ := json.Marshal(map[string]any{ "title": "Ghost Note", "path": filepath.Join(t.TempDir(), "ghost.md"), "templatePath": "/non/existent/template.md", }) - _, err := newCreateNoteHandler().Handle(Fixture.TestContext, params) + // Act + _, err := handler.Handle(Fixture.TestContext, params) + + // Assert assert.Error(t, err) }