fetch_ml/tests/unit/api/helpers/response_helpers_test.go
Jeremie Fraeys 7305e2bc21
test: add comprehensive test coverage and command improvements
- Add logs and debug end-to-end tests
- Add test helper utilities
- Improve test fixtures and templates
- Update API server and config lint commands
- Add multi-user database initialization
2026-02-16 20:38:15 -05:00

345 lines
8.4 KiB
Go

package helpers
import (
"testing"
"github.com/jfraeys/fetch_ml/internal/api/helpers"
"github.com/jfraeys/fetch_ml/internal/queue"
)
func TestNewTaskErrorMapper(t *testing.T) {
mapper := helpers.NewTaskErrorMapper()
if mapper == nil {
t.Error("NewTaskErrorMapper() returned nil")
}
}
func TestTaskErrorMapper_MapJupyterError(t *testing.T) {
mapper := helpers.NewTaskErrorMapper()
tests := []struct {
name string
task *queue.Task
want helpers.ErrorCode
}{
{
name: "nil task",
task: nil,
want: 0x00, // ErrorCodeUnknownError
},
{
name: "cancelled task",
task: &queue.Task{Status: "cancelled"},
want: 0x24, // ErrorCodeJobCancelled
},
{
name: "oom error",
task: &queue.Task{Status: "failed", Error: "out of memory"},
want: 0x30, // ErrorCodeOutOfMemory
},
{
name: "oom shorthand",
task: &queue.Task{Status: "failed", Error: "OOM killed"},
want: 0x30, // ErrorCodeOutOfMemory
},
{
name: "disk full",
task: &queue.Task{Status: "failed", Error: "no space left on device"},
want: 0x31, // ErrorCodeDiskFull
},
{
name: "disk full alt",
task: &queue.Task{Status: "failed", Error: "disk full"},
want: 0x31, // ErrorCodeDiskFull
},
{
name: "rate limit",
task: &queue.Task{Status: "failed", Error: "rate limit exceeded"},
want: 0x33, // ErrorCodeServiceUnavailable
},
{
name: "throttle",
task: &queue.Task{Status: "failed", Error: "request throttled"},
want: 0x33, // ErrorCodeServiceUnavailable
},
{
name: "timeout",
task: &queue.Task{Status: "failed", Error: "timed out waiting"},
want: 0x14, // ErrorCodeTimeout
},
{
name: "deadline",
task: &queue.Task{Status: "failed", Error: "context deadline exceeded"},
want: 0x14, // ErrorCodeTimeout
},
{
name: "connection refused",
task: &queue.Task{Status: "failed", Error: "connection refused"},
want: 0x12, // ErrorCodeNetworkError
},
{
name: "connection reset",
task: &queue.Task{Status: "failed", Error: "connection reset by peer"},
want: 0x12, // ErrorCodeNetworkError
},
{
name: "network unreachable",
task: &queue.Task{Status: "failed", Error: "network unreachable"},
want: 0x12, // ErrorCodeNetworkError
},
{
name: "queue not configured",
task: &queue.Task{Status: "failed", Error: "queue not configured"},
want: 0x32, // ErrorCodeInvalidConfiguration
},
{
name: "generic failed",
task: &queue.Task{Status: "failed", Error: "something went wrong"},
want: 0x23, // ErrorCodeJobExecutionFailed
},
{
name: "unknown status",
task: &queue.Task{Status: "unknown", Error: "unknown error"},
want: 0x00, // ErrorCodeUnknownError
},
{
name: "case insensitive - cancelled",
task: &queue.Task{Status: "CANCELLED"},
want: 0x24, // ErrorCodeJobCancelled
},
{
name: "case insensitive - oom",
task: &queue.Task{Status: "FAILED", Error: "OUT OF MEMORY"},
want: 0x30, // ErrorCodeOutOfMemory
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := mapper.MapJupyterError(tt.task)
if got != tt.want {
t.Errorf("MapJupyterError() = 0x%02X, want 0x%02X", got, tt.want)
}
})
}
}
func TestTaskErrorMapper_MapError(t *testing.T) {
mapper := helpers.NewTaskErrorMapper()
tests := []struct {
name string
task *queue.Task
defaultCode helpers.ErrorCode
want helpers.ErrorCode
}{
{
name: "nil task returns default",
task: nil,
defaultCode: 0x11, // ErrorCodeDatabaseError
want: 0x11,
},
{
name: "cancelled",
task: &queue.Task{Status: "cancelled"},
defaultCode: 0x00,
want: 0x24, // ErrorCodeJobCancelled
},
{
name: "oom",
task: &queue.Task{Status: "failed", Error: "oom"},
defaultCode: 0x00,
want: 0x30, // ErrorCodeOutOfMemory
},
{
name: "timeout",
task: &queue.Task{Status: "failed", Error: "timeout"},
defaultCode: 0x00,
want: 0x14, // ErrorCodeTimeout
},
{
name: "generic failed",
task: &queue.Task{Status: "failed", Error: "generic error"},
defaultCode: 0x00,
want: 0x23, // ErrorCodeJobExecutionFailed
},
{
name: "unknown status returns default",
task: &queue.Task{Status: "weird", Error: "unknown"},
defaultCode: 0x01,
want: 0x01,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := mapper.MapError(tt.task, tt.defaultCode)
if got != tt.want {
t.Errorf("MapError() = 0x%02X, want 0x%02X", got, tt.want)
}
})
}
}
func TestParseResourceRequest(t *testing.T) {
tests := []struct {
name string
payload []byte
want *helpers.ResourceRequest
wantErr bool
}{
{
name: "empty payload",
payload: []byte{},
want: nil,
wantErr: false,
},
{
name: "nil payload",
payload: nil,
want: nil,
wantErr: false,
},
{
name: "valid minimal",
payload: []byte{4, 8, 1, 0},
want: &helpers.ResourceRequest{CPU: 4, MemoryGB: 8, GPU: 1, GPUMemory: ""},
wantErr: false,
},
{
name: "valid with gpu memory",
payload: []byte{8, 16, 2, 4, '8', 'G', 'B', '!'},
want: &helpers.ResourceRequest{CPU: 8, MemoryGB: 16, GPU: 2, GPUMemory: "8GB!"},
wantErr: false,
},
{
name: "too short",
payload: []byte{1, 2},
want: nil,
wantErr: true,
},
{
name: "invalid gpu mem length",
payload: []byte{1, 2, 1, 10, 'a'},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := helpers.ParseResourceRequest(tt.payload)
if (err != nil) != tt.wantErr {
t.Errorf("ParseResourceRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.want == nil {
if got != nil {
t.Errorf("ParseResourceRequest() = %v, want nil", got)
}
} else if got == nil {
t.Errorf("ParseResourceRequest() = nil, want %v", tt.want)
} else {
if got.CPU != tt.want.CPU {
t.Errorf("CPU = %d, want %d", got.CPU, tt.want.CPU)
}
if got.MemoryGB != tt.want.MemoryGB {
t.Errorf("MemoryGB = %d, want %d", got.MemoryGB, tt.want.MemoryGB)
}
if got.GPU != tt.want.GPU {
t.Errorf("GPU = %d, want %d", got.GPU, tt.want.GPU)
}
if got.GPUMemory != tt.want.GPUMemory {
t.Errorf("GPUMemory = %q, want %q", got.GPUMemory, tt.want.GPUMemory)
}
}
})
}
}
func TestMarshalJSONOrEmpty(t *testing.T) {
tests := []struct {
name string
data interface{}
want []byte
}{
{
name: "simple map",
data: map[string]string{"key": "value"},
want: []byte(`{"key":"value"}`),
},
{
name: "string slice",
data: []string{"a", "b", "c"},
want: []byte(`["a","b","c"]`),
},
{
name: "empty slice",
data: []int{},
want: []byte(`[]`),
},
{
name: "nil",
data: nil,
want: []byte(`null`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := helpers.MarshalJSONOrEmpty(tt.data)
// For valid JSON, compare strings since JSON formatting might vary
gotStr := string(got)
wantStr := string(tt.want)
if gotStr != wantStr {
t.Errorf("MarshalJSONOrEmpty() = %q, want %q", gotStr, wantStr)
}
})
}
}
func TestMarshalJSONOrEmpty_ErrorCase(t *testing.T) {
// Test with a value that can't be marshaled (function)
got := helpers.MarshalJSONOrEmpty(func() {})
want := []byte("[]")
if string(got) != string(want) {
t.Errorf("MarshalJSONOrEmpty() with invalid data = %q, want %q", string(got), string(want))
}
}
func TestMarshalJSONBytes(t *testing.T) {
data := map[string]int{"count": 42}
got, err := helpers.MarshalJSONBytes(data)
if err != nil {
t.Errorf("MarshalJSONBytes() unexpected error: %v", err)
}
want := `{"count":42}`
if string(got) != want {
t.Errorf("MarshalJSONBytes() = %q, want %q", string(got), want)
}
}
func TestIsEmptyJSON(t *testing.T) {
tests := []struct {
name string
data []byte
want bool
}{
{"empty", []byte{}, true},
{"null", []byte("null"), true},
{"empty array", []byte("[]"), true},
{"empty object", []byte("{}"), true},
{"whitespace", []byte(" "), true},
{"data", []byte(`{"key":"value"}`), false},
{"non-empty array", []byte("[1,2,3]"), false},
{"null with spaces", []byte(" null "), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := helpers.IsEmptyJSON(tt.data); got != tt.want {
t.Errorf("IsEmptyJSON() = %v, want %v", got, tt.want)
}
})
}
}