Compare commits

..

21 Commits

Author SHA1 Message Date
f9ef9bfba2 docs: add aspirational targets
Some checks failed
test / go-test (push) Failing after 1m1s
2026-03-22 20:25:27 +00:00
6b505a5da8 refactor(go): enforce notestore interface 2026-03-22 20:25:00 +00:00
0e48b345eb docs(changelog): add notes to changelog
Some checks failed
test / go-test (push) Failing after 1m6s
2026-03-22 19:38:31 +00:00
649134422e docs(changelog): create changelog 2026-03-22 19:37:41 +00:00
5587583ed3 docs: adding readme.md 2026-03-22 19:35:12 +00:00
77c60de4f8 Correcting project layout 2026-03-22 19:33:22 +00:00
e591c5a4ae Need the target dir to exist!
Some checks failed
test / go-test (push) Failing after 6m59s
2026-03-19 01:37:48 +00:00
6428dd2157 Cache me outside?
Some checks failed
test / go-test (push) Has been cancelled
2026-03-19 01:36:09 +00:00
5cf0f637f8 -v not --verbose
Some checks failed
test / go-test (push) Failing after 6m2s
2026-03-19 01:23:42 +00:00
6b47951ff8 Decouple workflow from just
Some checks failed
test / go-test (push) Failing after 5m13s
2026-03-19 01:16:13 +00:00
9527311a12 Adding testing workflow
Some checks failed
test / go-test (push) Failing after 7m23s
2026-03-19 01:03:21 +00:00
52405411d3 Rename to match repo name 2026-03-19 00:51:28 +00:00
4e5b535f09 Validation is cheap so do it more 2026-03-19 00:49:35 +00:00
de6c9d6ae5 Refactoring validation to belong to model 2026-03-19 00:44:37 +00:00
a33cedf09e Adding new errors and centralising handling 2026-03-19 00:44:13 +00:00
8749ff3c03 Adding structure and testing around title support 2026-03-18 22:41:00 +00:00
0935788b69 Separating server and client for reasons ... 2026-03-18 21:41:58 +00:00
45c394cded Adding recommended extensions 2026-03-18 21:40:18 +00:00
Micheal Wilkinson
d67ce0d00b get is a mode 2026-03-18 00:47:17 +00:00
Micheal Wilkinson
19fbe8191d Making the output better 2026-03-18 00:44:37 +00:00
Micheal Wilkinson
981142eb60 I want my notes sorted 2026-03-18 00:36:38 +00:00
20 changed files with 642 additions and 104 deletions

70
.gitea/workflows/test.yml Normal file
View File

@@ -0,0 +1,70 @@
name: test
on:
push:
pull_request:
env:
build_path: _build
jobs:
go-test:
env:
RUNNER_TOOL_CACHE: /cache/tools
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.26.x"
- name: Get go-hashfiles
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: hash-go
with:
patterns: |-
go.mod
go.sum
- name: Echo hash
run: echo ${{ steps.hash-go.outputs.hash }}
- name: Cache go
id: cache-go
uses: https://github.com/actions/cache@v3 # Action cache
with: # specify with your GOMODCACHE and GOCACHE
path: |-
/root/go/pkg/mod
/root/.cache/go-build
key: go_cache-${{ steps.hash-go.outputs.hash }}
restore-keys: |-
go_cache-${{ steps.hash-go.outputs.hash }}
- name: Check Formatting
run: |
gofmt -l .
exit $(gofmt -l . | wc -l)
- name: Run tests
run: go test -v ./...
- name: Generate coverage
run: |
mkdir -p ${build_path}
go test -v -coverprofile=${build_path}/coverage.out ./...
- name: Code Coverage Report
uses: irongut/CodeCoverageSummary@v1.3.0
with:
filename: _build/coverage.out
badge: true
fail_below_min: true
format: markdown
hide_branch_rate: true
hide_complexity: false
indicators: true
output: both
thresholds: '60 80'

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"golang.go",
"skellock.just"
]
}

27
CHANGELOG.md Normal file
View File

@@ -0,0 +1,27 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
A `### Breaking` section is used in addition to Keep a Changelog's standard sections to explicitly document changes that are backwards-incompatible but would otherwise appear under `### Changed`. Entries under `### Breaking` trigger a major version bump in automated release recommendation logic.
## [Unreleased]
### Breaking
### Added
- SQLite based server
- Simple CLI for interacting with server
### Changed
### Removed
### Fixed
[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/894c2fb...main

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# Gotes
A simple golang based notes application
## Current state
Server application runs independantly & binds to port 8080 storing data in an sqlite table
Client CLI supports crud operations for notes with arguments consumed from flags
## TODO
- [ ] Implement `title` support in cli
- [ ] Refactor cli interactions to use a repository/notes interface wrapped implementation
- [ ] General project structure tidy up
- [ ] Update simplecli to use positional arguments comined with flags
- [ ] Create unified cmd entry point starting server in go routine closing after command completed
- [ ] Create config module for controlling server startup config
- [ ] Implement filesystem based notestore
- [ ] Implement webdav based notestore
- [ ] Implement git based notestore
- [ ] Create TUI based on charmbracelet eco system
### Target structure
|- config
|- cmd
| |- CLI (contains unified entrypoint with positional args for starting server / client ops)
| |- UnifiedCLI (CLI with auto background server)
| |- TUI
|- internal
| |- models
| |- repository
| | |- notes
| | | |- sqlite
| | | |- server
| | | |- filesystem ... etc
| |- service
| | | |- notes
| |- config
| |- handlers (HTTP server handlers)

29
cmd/server/main.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"context"
"flag"
"log"
"os"
"path/filepath"
"git.hrafn.xyz/aether/gotes/internal/repository"
"git.hrafn.xyz/aether/gotes/internal/store/sqlite"
"git.hrafn.xyz/aether/gotes/internal/cli/server"
)
func main() {
databasefile := flag.String("db", filepath.Join(os.Getenv("HOME"), "notes.db"), "Path to the SQLite database file")
dbPath := *databasefile
sqliteStore, err := sqlite.NewSQLiteStore(context.Background(), dbPath)
if err != nil {
panic(err)
}
defer sqliteStore.Close()
repo := repository.NewNoteRepository(sqliteStore)
s := server.GetServer(repo, log.Default())
err = s.Start(":8080")
if err != nil {
panic(err)
}
}

View File

@@ -1,37 +1,30 @@
package main package main
import ( import (
"context"
"flag" "flag"
"log" "log"
"os" "os"
"path/filepath"
"git.hrafn.xyz/aether/notes/client" "git.hrafn.xyz/aether/gotes/internal/cli/client"
"git.hrafn.xyz/aether/notes/internal/repository"
"git.hrafn.xyz/aether/notes/internal/store/sqlite"
"git.hrafn.xyz/aether/notes/server"
) )
func main() { func main() {
serverMode := flag.Bool("server", false, "Start the application in server mode")
list := flag.Bool("list", false, "List all notes in the database") list := flag.Bool("list", false, "List all notes in the database")
create := flag.Bool("create", false, "Create a new note") create := flag.Bool("create", false, "Create a new note")
update := flag.Bool("update", false, "Update an existing note") update := flag.Bool("update", false, "Update an existing note")
delete := flag.Bool("delete", false, "Delete a note from the database") delete := flag.Bool("delete", false, "Delete a note from the database")
get := flag.Bool("get", false, "Get a note by ID") get := flag.Bool("get", false, "Get a note by ID")
databasefile := flag.String("db", filepath.Join(os.Getenv("HOME"), "notes.db"), "Path to the SQLite database file (only used in server mode)")
id := flag.Int("id", 0, "ID of the note to get, update, or delete (only used with -get, -update, or -delete)") id := flag.Int("id", 0, "ID of the note to get, update, or delete (only used with -get, -update, or -delete)")
content := flag.String("content", "", "Content of the note to create or update (only used with -create or -update)") content := flag.String("content", "", "Content of the note to create or update (only used with -create or -update)")
flag.Parse() flag.Parse()
modes := map[string]bool{ modes := map[string]bool{
"server": *serverMode,
"list": *list, "list": *list,
"create": *create, "create": *create,
"update": *update, "update": *update,
"delete": *delete, "delete": *delete,
"get": *get,
} }
enabledCount := 0 enabledCount := 0
@@ -41,7 +34,7 @@ func main() {
} }
} }
if enabledCount == 0 { if enabledCount == 0 {
log.Fatal("No mode specified. Use -server, -list, -create, -update, -delete, or -get.") log.Fatal("No mode specified. Use -list, -create, -update, -delete, or -get.")
os.Exit(1) os.Exit(1)
} }
if enabledCount > 1 { if enabledCount > 1 {
@@ -49,21 +42,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if *serverMode {
dbPath := *databasefile
sqliteStore, err := sqlite.NewSQLiteStore(context.Background(), dbPath)
if err != nil {
panic(err)
}
defer sqliteStore.Close()
repo := repository.NewNoteRepository(sqliteStore)
s := server.GetServer(repo, log.Default())
err = s.Start(":8080")
if err != nil {
panic(err)
}
}
if *list { if *list {
client.ListNotes() client.ListNotes()
} }

2
go.mod
View File

@@ -1,4 +1,4 @@
module git.hrafn.xyz/aether/notes module git.hrafn.xyz/aether/gotes
go 1.26.1 go 1.26.1

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"os"
"strings" "strings"
"time" "time"
) )
@@ -20,14 +21,16 @@ func doRequest(method, url string, body any) []byte {
if body != nil { if body != nil {
data, err := json.Marshal(body) data, err := json.Marshal(body)
if err != nil { if err != nil {
log.Fatalf("Failed to marshal request body: %v", err) fmt.Println("Failed to marshal request body")
os.Exit(1)
} }
reqBody = bytes.NewReader(data) reqBody = bytes.NewReader(data)
} }
req, err := http.NewRequest(method, url, reqBody) req, err := http.NewRequest(method, url, reqBody)
if err != nil { if err != nil {
log.Fatalf("Failed to create request: %v", err) fmt.Println("Failed to create request")
os.Exit(1)
} }
if body != nil { if body != nil {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -35,7 +38,9 @@ func doRequest(method, url string, body any) []byte {
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
log.Fatalf("Failed to send request: %v", err) fmt.Println("Failed to send request is the server running?")
os.Exit(1)
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -79,12 +84,13 @@ func ListNotes() {
fmt.Printf("| %-*s | %-*s | %-*s |\n", idW, id, updatedW, updated, contentW, content) fmt.Printf("| %-*s | %-*s | %-*s |\n", idW, id, updatedW, updated, contentW, content)
} }
sep := fmt.Sprintf("|-%s-|-%s-|-%s-|", strings.Repeat("-", idW), strings.Repeat("-", updatedW), strings.Repeat("-", contentW)) sep := fmt.Sprintf("|-%s-|-%s-|-%s-|", strings.Repeat("-", idW), strings.Repeat("-", updatedW), strings.Repeat("-", contentW))
fmt.Println(strings.Repeat("-", idW+updatedW+contentW+10))
row("ID", "Updated", "Content") row("ID", "Updated", "Content")
fmt.Println(sep) fmt.Println(sep)
for _, n := range notes { for _, n := range notes {
row(fmt.Sprintf("%d", n.ID), n.LastUpdate.Format("2006-01-02 15:04:05"), n.Content) row(fmt.Sprintf("%d", n.ID), n.LastUpdate.Format("2006-01-02 15:04:05"), n.Content)
} }
fmt.Println(strings.Repeat("-", idW+updatedW+contentW+10))
} }
func CreateNote(content string) { func CreateNote(content string) {

View File

@@ -8,7 +8,7 @@ import (
"strconv" "strconv"
"time" "time"
"git.hrafn.xyz/aether/notes/internal/repository" "git.hrafn.xyz/aether/gotes/internal/repository"
) )
type Server struct { type Server struct {
@@ -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

View File

@@ -4,12 +4,37 @@ 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.
Content string `json:"content"` 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
}

View File

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

View File

@@ -0,0 +1,146 @@
package models_test
import (
"errors"
"fmt"
"math/rand/v2"
"reflect"
"strings"
"testing"
"time"
"git.hrafn.xyz/aether/gotes/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)
})
}
}

View File

@@ -2,10 +2,9 @@ package repository
import ( import (
"context" "context"
"fmt"
"time" "time"
"git.hrafn.xyz/aether/notes/internal/models" "git.hrafn.xyz/aether/gotes/internal/models"
) )
// NoteRepository manages operations on notes using a data store. // NoteRepository manages operations on notes using a data store.
@@ -13,20 +12,26 @@ 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 == "" {
return models.Note{}, fmt.Errorf("content cannot be empty")
}
note := models.Note{ note := models.Note{
Title: title,
Content: content, Content: content,
LastUpdate: time.Now(), LastUpdate: time.Now(),
} }
if err := note.Validate(); err != nil {
return models.Note{}, err
}
return r.store.SaveNote(ctx, note) return r.store.SaveNote(ctx, note)
} }
@@ -41,15 +46,30 @@ func (r *NoteRepository) ListNotes(ctx context.Context) ([]models.Note, error) {
} }
// UpdateNote updates the content of an existing note. // UpdateNote updates the content of an existing note.
func (r *NoteRepository) UpdateNote(ctx context.Context, id int, content string) (models.Note, error) { func (r *NoteRepository) UpdateNote(ctx context.Context, id int, update NoteUpdate) (models.Note, error) {
if content == "" { if update.Content == "" && update.Title == "" {
return models.Note{}, fmt.Errorf("content cannot be empty") return models.Note{}, &ErrEmptyUpdate{}
} }
note, err := r.store.GetNoteByID(ctx, id) note, err := r.store.GetNoteByID(ctx, id)
if err != nil { if err != nil {
return models.Note{}, err return models.Note{}, err
} }
note.Content = content
if update.Content == note.Content && update.Title == note.Title {
return models.Note{}, &ErrNoOP{}
}
if update.Content != "" {
note.Content = update.Content
}
if update.Title != "" {
note.Title = update.Title
}
if err := note.Validate(); err != nil {
return models.Note{}, err
}
note.LastUpdate = time.Now() note.LastUpdate = time.Now()
return r.store.SaveNote(ctx, note) return r.store.SaveNote(ctx, note)
} }

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/gotes/internal/models"
"git.hrafn.xyz/aether/notes/internal/repository" "git.hrafn.xyz/aether/gotes/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

@@ -2,7 +2,8 @@ package repository
import ( import (
"context" "context"
"git.hrafn.xyz/aether/notes/internal/models"
"git.hrafn.xyz/aether/gotes/internal/models"
) )
// NoteStore defines the interface for persisting and retrieving notes. // NoteStore defines the interface for persisting and retrieving notes.

View File

@@ -5,7 +5,7 @@ import (
"context" "context"
"fmt" "fmt"
"git.hrafn.xyz/aether/notes/internal/models" "git.hrafn.xyz/aether/gotes/internal/models"
) )
type mockNoteStore struct { type mockNoteStore struct {

View File

@@ -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"
}

View File

@@ -9,7 +9,8 @@ import (
"modernc.org/sqlite" "modernc.org/sqlite"
"git.hrafn.xyz/aether/notes/internal/models" "git.hrafn.xyz/aether/gotes/internal/models"
"git.hrafn.xyz/aether/gotes/internal/repository"
) )
const sqlSet = ` const sqlSet = `
@@ -17,6 +18,8 @@ const sqlSet = `
PRAGMA busy_timeout = 7000; PRAGMA busy_timeout = 7000;
` `
var _ repository.NoteStore = &SQLiteStore{}
type SQLiteStore struct { type SQLiteStore struct {
read *sql.DB read *sql.DB
write *sql.DB write *sql.DB
@@ -85,10 +88,14 @@ func (s *SQLiteStore) SaveNote(ctx context.Context, note models.Note) (models.No
} }
defer tx.Rollback() defer tx.Rollback()
if err := note.Validate(); err != nil {
return models.Note{}, err
}
if note.ID == 0 { if note.ID == 0 {
result, err := tx.ExecContext(ctx, ` result, err := tx.ExecContext(ctx, `
INSERT INTO notes (content, last_update) VALUES (?, ?); INSERT INTO notes (title, content, last_update) VALUES (?, ?, ?);
`, note.Content, note.LastUpdate) `, note.Title, note.Content, note.LastUpdate)
if err != nil { if err != nil {
return models.Note{}, fmt.Errorf("failed to insert note: %w", err) return models.Note{}, fmt.Errorf("failed to insert note: %w", err)
} }
@@ -99,14 +106,14 @@ func (s *SQLiteStore) SaveNote(ctx context.Context, note models.Note) (models.No
note.ID = int(id) note.ID = int(id)
} else { } else {
_, err := s.scanNote(tx.QueryRowContext(ctx, ` _, err := s.scanNote(tx.QueryRowContext(ctx, `
SELECT id, content, last_update FROM notes WHERE id = ?; SELECT id, title, content, last_update FROM notes WHERE id = ?;
`, note.ID)) `, note.ID))
if err != nil { if err != nil {
return models.Note{}, fmt.Errorf("cannot update note not found: %w", err) return models.Note{}, fmt.Errorf("cannot update note not found: %w", err)
} }
_, err = tx.ExecContext(ctx, ` _, err = tx.ExecContext(ctx, `
UPDATE notes SET content = ?, last_update = ? WHERE id = ?; UPDATE notes SET title = ?, content = ?, last_update = ? WHERE id = ?;
`, note.Content, note.LastUpdate, note.ID) `, note.Title, note.Content, note.LastUpdate, note.ID)
if err != nil { if err != nil {
return models.Note{}, fmt.Errorf("failed to update note: %w", err) return models.Note{}, fmt.Errorf("failed to update note: %w", err)
} }
@@ -120,7 +127,7 @@ func (s *SQLiteStore) SaveNote(ctx context.Context, note models.Note) (models.No
func (s *SQLiteStore) GetNoteByID(ctx context.Context, id int) (models.Note, error) { func (s *SQLiteStore) GetNoteByID(ctx context.Context, id int) (models.Note, error) {
row := s.read.QueryRowContext(ctx, ` row := s.read.QueryRowContext(ctx, `
SELECT id, content, last_update FROM notes WHERE id = ?; SELECT id, title, content, last_update FROM notes WHERE id = ?;
`, id) `, id)
note, err := s.scanNote(row) note, err := s.scanNote(row)
if err != nil { if err != nil {
@@ -134,7 +141,7 @@ func (s *SQLiteStore) GetNoteByID(ctx context.Context, id int) (models.Note, err
func (s *SQLiteStore) GetAllNotes(ctx context.Context) ([]models.Note, error) { func (s *SQLiteStore) GetAllNotes(ctx context.Context) ([]models.Note, error) {
rows, err := s.read.QueryContext(ctx, ` rows, err := s.read.QueryContext(ctx, `
SELECT id, content, last_update FROM notes ORDER BY last_update DESC; SELECT id, title, content, last_update FROM notes ORDER BY id ASC;
`) `)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query all notes: %w", err) return nil, fmt.Errorf("failed to query all notes: %w", err)
@@ -177,8 +184,11 @@ func (s *SQLiteStore) validateSchema(ctx context.Context) error {
CREATE TABLE IF NOT EXISTS notes ( CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
last_update TIMESTAMP NOT NULL, last_update TIMESTAMP NOT NULL,
content TEXT NOT NULL title TEXT NOT NULL,
content BLOB NOT NULL
); );
CREATE UNIQUE INDEX idx_id ON notes(id);
CREATE INDEX idx_title ON notes(title);
`, nil) `, nil)
return err return err
} }
@@ -193,7 +203,7 @@ func (s *SQLiteStore) getWriteTransaction(ctx context.Context) (*sql.Tx, error)
func (s *SQLiteStore) scanNote(results scannable) (models.Note, error) { func (s *SQLiteStore) scanNote(results scannable) (models.Note, error) {
var note models.Note var note models.Note
err := results.Scan(&note.ID, &note.Content, &note.LastUpdate) err := results.Scan(&note.ID, &note.Title, &note.Content, &note.LastUpdate)
if err != nil { if err != nil {
return models.Note{}, fmt.Errorf("failed to scan note: %w", err) return models.Note{}, fmt.Errorf("failed to scan note: %w", err)
} }

View File

@@ -9,8 +9,8 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"git.hrafn.xyz/aether/notes/internal/models" "git.hrafn.xyz/aether/gotes/internal/models"
"git.hrafn.xyz/aether/notes/internal/store/sqlite" "git.hrafn.xyz/aether/gotes/internal/store/sqlite"
) )
func TestCreateSQLiteStore(t *testing.T) { func TestCreateSQLiteStore(t *testing.T) {
@@ -86,11 +86,13 @@ func TestSaveNote(t *testing.T) {
name: "new note", name: "new note",
note: models.Note{ note: models.Note{
Content: "Test note", Content: "Test note",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedNote: models.Note{ expectedNote: models.Note{
ID: 1, ID: 1,
Content: "Test note", Content: "Test note",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedError: false, expectedError: false,
@@ -98,11 +100,13 @@ func TestSaveNote(t *testing.T) {
name: "new 2 note", name: "new 2 note",
note: models.Note{ note: models.Note{
Content: "Test note 2!", Content: "Test note 2!",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedNote: models.Note{ expectedNote: models.Note{
ID: 2, ID: 2,
Content: "Test note 2!", Content: "Test note 2!",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedError: false, expectedError: false,
@@ -112,11 +116,13 @@ func TestSaveNote(t *testing.T) {
note: models.Note{ note: models.Note{
ID: 1, ID: 1,
Content: "Updated note", Content: "Updated note",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedNote: models.Note{ expectedNote: models.Note{
ID: 1, ID: 1,
Content: "Updated note", Content: "Updated note",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedError: false, expectedError: false,
@@ -126,6 +132,7 @@ func TestSaveNote(t *testing.T) {
note: models.Note{ note: models.Note{
ID: 999, ID: 999,
Content: "This note does not exist", Content: "This note does not exist",
Title: "No Problem",
LastUpdate: time.Now(), LastUpdate: time.Now(),
}, },
expectedNote: models.Note{}, expectedNote: models.Note{},
@@ -168,6 +175,7 @@ func TestGetNoteByID(t *testing.T) {
}() }()
store.SaveNote(context.Background(), models.Note{ store.SaveNote(context.Background(), models.Note{
Content: "Test note", Content: "Test note",
Title: "Saved No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}) })
@@ -183,6 +191,7 @@ func TestGetNoteByID(t *testing.T) {
expectedNote: models.Note{ expectedNote: models.Note{
ID: 1, ID: 1,
Content: "Test note", Content: "Test note",
Title: "Saved No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}, },
expectedError: false, expectedError: false,
@@ -259,10 +268,12 @@ func TestGetAllNotes(t *testing.T) {
store.SaveNote(context.Background(), models.Note{ store.SaveNote(context.Background(), models.Note{
Content: "Test note 1", Content: "Test note 1",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}) })
store.SaveNote(context.Background(), models.Note{ store.SaveNote(context.Background(), models.Note{
Content: "Test note 2", Content: "Test note 2",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC),
}) })
@@ -277,11 +288,13 @@ func TestGetAllNotes(t *testing.T) {
{ {
ID: 1, ID: 1,
Content: "Test note 1", Content: "Test note 1",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}, },
{ {
ID: 2, ID: 2,
Content: "Test note 2", Content: "Test note 2",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC),
}, },
}, },
@@ -324,14 +337,17 @@ func TestDeleteNoteByID(t *testing.T) {
store.SaveNote(context.Background(), models.Note{ store.SaveNote(context.Background(), models.Note{
Content: "Test note 1", Content: "Test note 1",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}) })
store.SaveNote(context.Background(), models.Note{ store.SaveNote(context.Background(), models.Note{
Content: "Test note 2", Content: "Test note 2",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}) })
store.SaveNote(context.Background(), models.Note{ store.SaveNote(context.Background(), models.Note{
Content: "Test note 3", Content: "Test note 3",
Title: "No Problem",
LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), LastUpdate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
}) })

View File

@@ -1,5 +1,7 @@
# Use 'just <command>' to run tasks # Use 'just <command>' to run tasks
build_path := "_build"
server_exec_name := "server"
simplecli_exec_name := "simplecli"
# Default recipe (runs when you just type 'just') # Default recipe (runs when you just type 'just')
default: default:
@@ -7,9 +9,20 @@ default:
build: clean build: clean
@echo "Building notes..." @echo "Building notes..."
@mkdir -p _build @mkdir -p {{ build_path }}
go build -o _build/notes -ldflags="-s -w" . @just build-server
@echo "Build complete: ./_build/notes" @just build-simplecli
build-server:
@echo "Building the isolated server..."
@rm -f {{ build_path }}/{{ server_exec_name }}
go build -o {{ build_path }}/{{ server_exec_name }} -ldflags="-s -w" cmd/server/main.go
build-simplecli:
@echo "Building the simple cli..."
@rm -f {{ build_path }}/{{ simplecli_exec_name }}
go build -o {{ build_path }}/{{ simplecli_exec_name }} -ldflags="-s -w" cmd/simplecli/main.go
# Run tests with short output # Run tests with short output
test: test:
@@ -20,12 +33,12 @@ test:
# Run tests with coverage # Run tests with coverage
test-coverage: test-coverage:
@echo "Running tests with coverage..." @echo "Running tests with coverage..."
@mkdir -p _build @mkdir -p {{ build_path }}
@rm -f _build/coverage.out @rm -f {{ build_path }}/coverage.out
go test -v -coverprofile=_build/coverage.out ./... go test -v -coverprofile={{ build_path }}/coverage.out ./...
@echo "Generate HTML report" @echo "Generate HTML report"
go tool cover -html=_build/coverage.out -o _build/coverage.html go tool cover -html={{ build_path }}/coverage.out -o {{ build_path }}/coverage.html
@echo "Coverage report generated: _build/coverage.html" @echo "Coverage report generated: {{ build_path }}/coverage.html"
# Watch for changes and run tests automatically # Watch for changes and run tests automatically
watch: watch:
@@ -33,17 +46,8 @@ watch:
@command -v watchexec >/dev/null 2>&1 || (echo "Error: watchexec not installed. Install with: brew install watchexec" && exit 1) @command -v watchexec >/dev/null 2>&1 || (echo "Error: watchexec not installed. Install with: brew install watchexec" && exit 1)
@watchexec --clear --exts go -- sh -c 'echo "Changes detected, running tests..." && just test' @watchexec --clear --exts go -- sh -c 'echo "Changes detected, running tests..." && just test'
# Run the server
run:
@echo "Starting notes server..."
go run . --server
# Run the built binary
run-binary: build
./_build/notes --server
# Clean build artifacts # Clean build artifacts
clean: clean:
@echo "Cleaning build artifacts..." @echo "Cleaning build artifacts..."
rm -rf _build rm -rf {{ build_path }}
@echo "Clean complete" @echo "Clean complete"