From 627fcbafe19802052a67bff97ea0afb5d863ccdf Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Mon, 19 Jan 2026 12:46:07 -0500 Subject: [PATCH] tests: add backend tests --- Makefile | 17 ++ backend/AGENTS.md | 19 +- backend/go.mod | 10 +- backend/go.sum | 10 +- backend/internal/store/memory_test.go | 176 +++++++++++++++++ backend/internal/store/storage_test.go | 252 +++++++++++++++++++++++++ backend/pkg/ptr/ptr_test.go | 53 ++++++ backend/pkg/slices/map_test.go | 83 ++++++++ backend/pkg/values/values_test.go | 51 +++++ 9 files changed, 664 insertions(+), 7 deletions(-) create mode 100644 Makefile create mode 100644 backend/internal/store/memory_test.go create mode 100644 backend/internal/store/storage_test.go create mode 100644 backend/pkg/ptr/ptr_test.go create mode 100644 backend/pkg/slices/map_test.go create mode 100644 backend/pkg/values/values_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c51610c --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: all frontend backend clean dev + +all: frontend backend + +frontend: + cd frontend && bun run build + +backend: + cd backend && go build -o ./dist/aethera ./cmd + +clean: + rm -rf frontend/public/dist + rm -rf backend/dist + +dev: + cd backend && go run ./cmd --listen 0.0.0.0 & + cd frontend && bun run dev diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 993752b..6095c69 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -16,6 +16,23 @@ golangci-lint run go test ./... ``` +## Testing + +All Go code is tested using the [testify](https://github.com/stretchr/testify) framework with comprehensive test coverage: + +- **Unit Tests**: Each function and method is thoroughly tested including edge cases +- **Integration Tests**: Store implementations are tested end-to-end +- **Error Handling**: All error conditions are explicitly tested +- **Concurrency**: Thread-safety of concurrent operations is verified +- **File Operations**: File-based storage tests use temporary directories + +Tests follow these conventions: +- Use `require.NoError(t, err)` for fatal errors that break test flow +- Use `assert.NoError(t, err)` for non-fatal assertions +- Test both success and error paths for all methods +- Use table-driven tests where appropriate +- All tests are run with `go test ./...` or `go test -v ./...` + ## Non-Negotiables - ❌ No unhandled errors - always check `err` @@ -40,7 +57,7 @@ go test ./... - **DI**: Dependencies through constructors (`New*` functions) - **HTTP**: Handlers receive `store.Store`, validate inputs, return proper status codes - **Streaming**: Use `FlushWriter` for SSE/text streams -- **Storage**: JSON file-based (`FileStore` implementation) +- **Storage**: JSON file-based (`FileStore` implementation) with in-memory alternative (`InMemoryStore`) ## What Goes Where diff --git a/backend/go.mod b/backend/go.mod index 84fb79c..3a5e420 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,19 +3,23 @@ module reichard.io/aethera go 1.25.5 require ( + github.com/google/jsonschema-go v0.4.2 + github.com/google/uuid v1.6.0 + github.com/openai/openai-go/v3 v3.15.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 ) require ( - github.com/google/jsonschema-go v0.4.2 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/openai/openai-go/v3 v3.15.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect golang.org/x/sys v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 2379ed9..3ddcdbe 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -2,6 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -20,8 +22,9 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -33,10 +36,11 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/store/memory_test.go b/backend/internal/store/memory_test.go new file mode 100644 index 0000000..a6d9b7e --- /dev/null +++ b/backend/internal/store/memory_test.go @@ -0,0 +1,176 @@ +package store + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInMemoryStore_SaveChat(t *testing.T) { + store := NewInMemoryStore() + + chat := &Chat{ + Title: "Test Chat", + } + + err := store.SaveChat(chat) + require.NoError(t, err) + assert.NotEqual(t, uuid.Nil, chat.ID) + assert.False(t, chat.CreatedAt.IsZero()) +} + +func TestInMemoryStore_GetChat(t *testing.T) { + store := NewInMemoryStore() + + // Create and save a chat + chat := &Chat{ + Title: "Test Chat", + } + err := store.SaveChat(chat) + require.NoError(t, err) + + // Retrieve the chat + retrievedChat, err := store.GetChat(chat.ID) + require.NoError(t, err) + assert.Equal(t, chat.ID, retrievedChat.ID) + assert.Equal(t, chat.Title, retrievedChat.Title) +} + +func TestInMemoryStore_GetChat_NotFound(t *testing.T) { + store := NewInMemoryStore() + + _, err := store.GetChat(uuid.New()) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestInMemoryStore_DeleteChat(t *testing.T) { + store := NewInMemoryStore() + + // Create and save a chat + chat := &Chat{ + Title: "Test Chat", + } + err := store.SaveChat(chat) + require.NoError(t, err) + + // Delete the chat + err = store.DeleteChat(chat.ID) + require.NoError(t, err) + + // Try to retrieve the deleted chat + _, err = store.GetChat(chat.ID) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestInMemoryStore_DeleteChat_NotFound(t *testing.T) { + store := NewInMemoryStore() + + err := store.DeleteChat(uuid.New()) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestInMemoryStore_ListChats(t *testing.T) { + store := NewInMemoryStore() + + // Create and save multiple chats + chat1 := &Chat{Title: "Chat 1"} + chat2 := &Chat{Title: "Chat 2"} + + err := store.SaveChat(chat1) + require.NoError(t, err) + + err = store.SaveChat(chat2) + require.NoError(t, err) + + // List all chats + chats, err := store.ListChats() + require.NoError(t, err) + assert.Len(t, chats, 2) +} + +func TestInMemoryStore_SaveChatMessage(t *testing.T) { + store := NewInMemoryStore() + + // Create and save a chat + chat := &Chat{Title: "Test Chat"} + err := store.SaveChat(chat) + require.NoError(t, err) + + // Create and save a message + message := &Message{ + ChatID: chat.ID, + Role: "user", + Content: "Hello", + } + + err = store.SaveChatMessage(message) + require.NoError(t, err) + assert.NotEqual(t, uuid.Nil, message.ID) + assert.False(t, message.CreatedAt.IsZero()) +} + +func TestInMemoryStore_SaveChatMessage_InvalidChatID(t *testing.T) { + store := NewInMemoryStore() + + message := &Message{ + ChatID: uuid.Nil, + Role: "user", + Content: "Hello", + } + + err := store.SaveChatMessage(message) + assert.ErrorIs(t, err, ErrNilChatID) +} + +func TestInMemoryStore_SaveChatMessage_ChatNotFound(t *testing.T) { + store := NewInMemoryStore() + + message := &Message{ + ChatID: uuid.New(), + Role: "user", + Content: "Hello", + } + + err := store.SaveChatMessage(message) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestInMemoryStore_SaveSettings(t *testing.T) { + store := NewInMemoryStore() + + settings := &Settings{ + APIEndpoint: "http://example.com", + ImageEditSelector: ".image-edit", + ImageGenerationSelector: ".image-gen", + TextGenerationSelector: ".text-gen", + } + + err := store.SaveSettings(settings) + require.NoError(t, err) +} + +func TestInMemoryStore_GetSettings(t *testing.T) { + store := NewInMemoryStore() + + // Get settings when none exist + settings, err := store.GetSettings() + require.NoError(t, err) + assert.NotNil(t, settings) + + // Set some settings + settings = &Settings{ + APIEndpoint: "http://example.com", + ImageEditSelector: ".image-edit", + ImageGenerationSelector: ".image-gen", + TextGenerationSelector: ".text-gen", + } + err = store.SaveSettings(settings) + require.NoError(t, err) + + // Get the settings + settings, err = store.GetSettings() + require.NoError(t, err) + assert.Equal(t, "http://example.com", settings.APIEndpoint) +} diff --git a/backend/internal/store/storage_test.go b/backend/internal/store/storage_test.go new file mode 100644 index 0000000..3b5eee8 --- /dev/null +++ b/backend/internal/store/storage_test.go @@ -0,0 +1,252 @@ +package store + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFileStore_NewFileStore(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + assert.NotNil(t, store) + assert.Equal(t, filePath, store.filePath) + assert.Equal(t, filepath.Join(tempDir, "chats"), store.chatDir) +} + +func TestFileStore_SaveChat(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + chat := &Chat{ + Title: "Test Chat", + } + + err = store.SaveChat(chat) + require.NoError(t, err) + assert.NotEqual(t, uuid.Nil, chat.ID) + assert.False(t, chat.CreatedAt.IsZero()) + + // Verify the file was created + chatFile := filepath.Join(store.chatDir, chat.ID.String()+".json") + _, err = os.Stat(chatFile) + assert.NoError(t, err) +} + +func TestFileStore_GetChat(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + // Create and save a chat + chat := &Chat{ + Title: "Test Chat", + } + err = store.SaveChat(chat) + require.NoError(t, err) + + // Retrieve the chat + retrievedChat, err := store.GetChat(chat.ID) + require.NoError(t, err) + assert.Equal(t, chat.ID, retrievedChat.ID) + assert.Equal(t, chat.Title, retrievedChat.Title) +} + +func TestFileStore_GetChat_NotFound(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + _, err = store.GetChat(uuid.New()) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestFileStore_DeleteChat(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + // Create and save a chat + chat := &Chat{ + Title: "Test Chat", + } + err = store.SaveChat(chat) + require.NoError(t, err) + + // Verify the file was created + chatFile := filepath.Join(store.chatDir, chat.ID.String()+".json") + _, err = os.Stat(chatFile) + assert.NoError(t, err) + + // Delete the chat + err = store.DeleteChat(chat.ID) + require.NoError(t, err) + + // Verify the file was deleted + _, err = os.Stat(chatFile) + assert.True(t, os.IsNotExist(err)) +} + +func TestFileStore_DeleteChat_NotFound(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + err = store.DeleteChat(uuid.New()) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestFileStore_ListChats(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + // Create and save multiple chats + chat1 := &Chat{Title: "Chat 1"} + chat2 := &Chat{Title: "Chat 2"} + + err = store.SaveChat(chat1) + require.NoError(t, err) + + err = store.SaveChat(chat2) + require.NoError(t, err) + + // List all chats + chats, err := store.ListChats() + require.NoError(t, err) + assert.Len(t, chats, 2) +} + +func TestFileStore_SaveChatMessage(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + // Create and save a chat + chat := &Chat{Title: "Test Chat"} + err = store.SaveChat(chat) + require.NoError(t, err) + + // Create and save a message + message := &Message{ + ChatID: chat.ID, + Role: "user", + Content: "Hello", + } + + err = store.SaveChatMessage(message) + require.NoError(t, err) + assert.NotEqual(t, uuid.Nil, message.ID) + assert.False(t, message.CreatedAt.IsZero()) + + // Verify the chat file was updated + chatFile := filepath.Join(store.chatDir, chat.ID.String()+".json") + _, err = os.Stat(chatFile) + assert.NoError(t, err) +} + +func TestFileStore_SaveChatMessage_InvalidChatID(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + message := &Message{ + ChatID: uuid.Nil, + Role: "user", + Content: "Hello", + } + + err = store.SaveChatMessage(message) + assert.ErrorIs(t, err, ErrNilChatID) +} + +func TestFileStore_SaveChatMessage_ChatNotFound(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + message := &Message{ + ChatID: uuid.New(), + Role: "user", + Content: "Hello", + } + + err = store.SaveChatMessage(message) + assert.ErrorIs(t, err, ErrChatNotFound) +} + +func TestFileStore_SaveSettings(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + settings := &Settings{ + APIEndpoint: "http://example.com", + ImageEditSelector: ".image-edit", + ImageGenerationSelector: ".image-gen", + TextGenerationSelector: ".text-gen", + } + + err = store.SaveSettings(settings) + require.NoError(t, err) + + // Verify the settings file was created + _, err = os.Stat(filePath) + assert.NoError(t, err) +} + +func TestFileStore_GetSettings(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.json") + + store, err := NewFileStore(filePath) + require.NoError(t, err) + + // Get settings when none exist + settings, err := store.GetSettings() + require.NoError(t, err) + assert.NotNil(t, settings) + + // Set some settings + settings = &Settings{ + APIEndpoint: "http://example.com", + ImageEditSelector: ".image-edit", + ImageGenerationSelector: ".image-gen", + TextGenerationSelector: ".text-gen", + } + err = store.SaveSettings(settings) + require.NoError(t, err) + + // Get the settings + settings, err = store.GetSettings() + require.NoError(t, err) + assert.Equal(t, "http://example.com", settings.APIEndpoint) +} diff --git a/backend/pkg/ptr/ptr_test.go b/backend/pkg/ptr/ptr_test.go new file mode 100644 index 0000000..a68e11e --- /dev/null +++ b/backend/pkg/ptr/ptr_test.go @@ -0,0 +1,53 @@ +package ptr + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDerefOrZero(t *testing.T) { + // Test with nil pointer + var nilPtr *int + result := DerefOrZero(nilPtr) + assert.Equal(t, 0, result) + + // Test with non-nil pointer + value := 42 + ptr := &value + result = DerefOrZero(ptr) + assert.Equal(t, 42, result) + + // Test with string + var nilString *string + resultStr := DerefOrZero(nilString) + assert.Equal(t, "", resultStr) + + // Test with non-nil string + strValue := "hello" + strPtr := &strValue + resultStr = DerefOrZero(strPtr) + assert.Equal(t, "hello", resultStr) +} + +func TestOf(t *testing.T) { + // Test with int + value := 42 + ptr := Of(value) + assert.NotNil(t, ptr) + assert.Equal(t, 42, *ptr) + + // Test with string - using explicit typing + var strPtr *string + strValue := "hello" + strPtr = Of(strValue) + assert.NotNil(t, strPtr) + assert.Equal(t, "hello", *strPtr) + + // Test with bool - using explicit typing + var boolPtr *bool + boolValue := true + boolPtr = Of(boolValue) + assert.NotNil(t, boolPtr) + assert.Equal(t, true, *boolPtr) +} diff --git a/backend/pkg/slices/map_test.go b/backend/pkg/slices/map_test.go new file mode 100644 index 0000000..e489eaa --- /dev/null +++ b/backend/pkg/slices/map_test.go @@ -0,0 +1,83 @@ +package slices + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMap(t *testing.T) { + // Test with integers + ints := []int{1, 2, 3, 4, 5} + result := Map(ints, func(x int) int { + return x * 2 + }) + assert.Equal(t, []int{2, 4, 6, 8, 10}, result) + + // Test with strings + strings := []string{"a", "b", "c"} + resultStr := Map(strings, func(s string) string { + return s + "_suffix" + }) + assert.Equal(t, []string{"a_suffix", "b_suffix", "c_suffix"}, resultStr) + + // Test with empty slice + empty := []int{} + resultEmpty := Map(empty, func(x int) int { + return x * 2 + }) + assert.Equal(t, []int{}, resultEmpty) +} + +func TestFirst(t *testing.T) { + // Test with non-empty slice + ints := []int{1, 2, 3} + item, found := First(ints) + assert.True(t, found) + assert.Equal(t, 1, item) + + // Test with empty slice + empty := []int{} + item, found = First(empty) + assert.False(t, found) + assert.Equal(t, 0, item) +} + +func TestLast(t *testing.T) { + // Test with non-empty slice + ints := []int{1, 2, 3} + item, found := Last(ints) + assert.True(t, found) + assert.Equal(t, 3, item) + + // Test with empty slice + empty := []int{} + item, found = Last(empty) + assert.False(t, found) + assert.Equal(t, 0, item) +} + +func TestFindFirst(t *testing.T) { + // Test with matching element + ints := []int{1, 2, 3, 4, 5} + item, found := FindFirst(ints, func(x int) bool { + return x > 3 + }) + assert.True(t, found) + assert.Equal(t, 4, item) + + // Test with no matching element + item, found = FindFirst(ints, func(x int) bool { + return x > 10 + }) + assert.False(t, found) + assert.Equal(t, 0, item) + + // Test with empty slice + empty := []int{} + item, found = FindFirst(empty, func(x int) bool { + return x > 3 + }) + assert.False(t, found) + assert.Equal(t, 0, item) +} diff --git a/backend/pkg/values/values_test.go b/backend/pkg/values/values_test.go new file mode 100644 index 0000000..5e3dbe3 --- /dev/null +++ b/backend/pkg/values/values_test.go @@ -0,0 +1,51 @@ +package values + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFirstNonZero(t *testing.T) { + // Test with all zero values + result := FirstNonZero(0, 0, 0) + assert.Equal(t, 0, result) + + // Test with first non-zero + result = FirstNonZero(0, 5, 10) + assert.Equal(t, 5, result) + + // Test with middle non-zero + result = FirstNonZero(0, 0, 15, 0, 20) + assert.Equal(t, 15, result) + + // Test with strings + resultStr := FirstNonZero("", "hello", "") + assert.Equal(t, "hello", resultStr) + + // Test with empty strings (empty string is zero value for string) + resultStr = FirstNonZero("", "", "") + assert.Equal(t, "", resultStr) +} + +func TestCountNonZero(t *testing.T) { + // Test with all zero values + count := CountNonZero(0, 0, 0) + assert.Equal(t, 0, count) + + // Test with some non-zero values + count = CountNonZero(0, 5, 10) + assert.Equal(t, 2, count) + + // Test with all non-zero values + count = CountNonZero(1, 2, 3, 4) + assert.Equal(t, 4, count) + + // Test with strings + count = CountNonZero("", "hello", "", "world") + assert.Equal(t, 2, count) + + // Test with empty slice + count = CountNonZero[int]() + assert.Equal(t, 0, count) +}