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) }