network/pkg/errors/errors_test.go
2026-01-20 10:03:55 +02:00

406 lines
10 KiB
Go

package errors
import (
"errors"
"fmt"
"strings"
"testing"
)
func TestValidationError(t *testing.T) {
tests := []struct {
name string
field string
message string
value interface{}
expectedError string
}{
{
name: "with field",
field: "email",
message: "invalid email format",
value: "not-an-email",
expectedError: "validation error: email: invalid email format",
},
{
name: "without field",
field: "",
message: "invalid input",
value: nil,
expectedError: "validation error: invalid input",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewValidationError(tt.field, tt.message, tt.value)
if err.Error() != tt.expectedError {
t.Errorf("Expected error %q, got %q", tt.expectedError, err.Error())
}
if err.Code() != CodeValidation {
t.Errorf("Expected code %q, got %q", CodeValidation, err.Code())
}
if err.Field != tt.field {
t.Errorf("Expected field %q, got %q", tt.field, err.Field)
}
})
}
}
func TestNotFoundError(t *testing.T) {
tests := []struct {
name string
resource string
id string
expectedError string
}{
{
name: "with ID",
resource: "user",
id: "123",
expectedError: "user with ID '123' not found",
},
{
name: "without ID",
resource: "user",
id: "",
expectedError: "user not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewNotFoundError(tt.resource, tt.id)
if err.Error() != tt.expectedError {
t.Errorf("Expected error %q, got %q", tt.expectedError, err.Error())
}
if err.Code() != CodeNotFound {
t.Errorf("Expected code %q, got %q", CodeNotFound, err.Code())
}
if err.Resource != tt.resource {
t.Errorf("Expected resource %q, got %q", tt.resource, err.Resource)
}
})
}
}
func TestUnauthorizedError(t *testing.T) {
t.Run("default message", func(t *testing.T) {
err := NewUnauthorizedError("")
if err.Message() != "authentication required" {
t.Errorf("Expected message 'authentication required', got %q", err.Message())
}
if err.Code() != CodeUnauthorized {
t.Errorf("Expected code %q, got %q", CodeUnauthorized, err.Code())
}
})
t.Run("custom message", func(t *testing.T) {
err := NewUnauthorizedError("invalid token")
if err.Message() != "invalid token" {
t.Errorf("Expected message 'invalid token', got %q", err.Message())
}
})
t.Run("with realm", func(t *testing.T) {
err := NewUnauthorizedError("").WithRealm("api")
if err.Realm != "api" {
t.Errorf("Expected realm 'api', got %q", err.Realm)
}
})
}
func TestForbiddenError(t *testing.T) {
tests := []struct {
name string
resource string
action string
expectedMsg string
}{
{
name: "with resource and action",
resource: "function",
action: "delete",
expectedMsg: "forbidden: cannot delete function",
},
{
name: "without details",
resource: "",
action: "",
expectedMsg: "forbidden",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewForbiddenError(tt.resource, tt.action)
if err.Message() != tt.expectedMsg {
t.Errorf("Expected message %q, got %q", tt.expectedMsg, err.Message())
}
if err.Code() != CodeForbidden {
t.Errorf("Expected code %q, got %q", CodeForbidden, err.Code())
}
})
}
}
func TestConflictError(t *testing.T) {
tests := []struct {
name string
resource string
field string
value string
expectedMsg string
}{
{
name: "with field",
resource: "user",
field: "email",
value: "test@example.com",
expectedMsg: "user with email='test@example.com' already exists",
},
{
name: "without field",
resource: "user",
field: "",
value: "",
expectedMsg: "user already exists",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewConflictError(tt.resource, tt.field, tt.value)
if err.Message() != tt.expectedMsg {
t.Errorf("Expected message %q, got %q", tt.expectedMsg, err.Message())
}
if err.Code() != CodeConflict {
t.Errorf("Expected code %q, got %q", CodeConflict, err.Code())
}
})
}
}
func TestInternalError(t *testing.T) {
t.Run("with cause", func(t *testing.T) {
cause := errors.New("database connection failed")
err := NewInternalError("failed to save user", cause)
if err.Message() != "failed to save user" {
t.Errorf("Expected message 'failed to save user', got %q", err.Message())
}
if err.Unwrap() != cause {
t.Errorf("Expected cause to be preserved")
}
if !strings.Contains(err.Error(), "database connection failed") {
t.Errorf("Expected error to contain cause: %q", err.Error())
}
})
t.Run("with operation", func(t *testing.T) {
err := NewInternalError("operation failed", nil).WithOperation("saveUser")
if err.Operation != "saveUser" {
t.Errorf("Expected operation 'saveUser', got %q", err.Operation)
}
})
}
func TestServiceError(t *testing.T) {
cause := errors.New("connection refused")
err := NewServiceError("rqlite", "database unavailable", 503, cause)
if err.Service != "rqlite" {
t.Errorf("Expected service 'rqlite', got %q", err.Service)
}
if err.StatusCode != 503 {
t.Errorf("Expected status code 503, got %d", err.StatusCode)
}
if err.Unwrap() != cause {
t.Errorf("Expected cause to be preserved")
}
}
func TestTimeoutError(t *testing.T) {
err := NewTimeoutError("function execution", "30s")
if err.Operation != "function execution" {
t.Errorf("Expected operation 'function execution', got %q", err.Operation)
}
if err.Duration != "30s" {
t.Errorf("Expected duration '30s', got %q", err.Duration)
}
if !strings.Contains(err.Message(), "timeout") {
t.Errorf("Expected message to contain 'timeout': %q", err.Message())
}
}
func TestRateLimitError(t *testing.T) {
err := NewRateLimitError(100, 60)
if err.Limit != 100 {
t.Errorf("Expected limit 100, got %d", err.Limit)
}
if err.RetryAfter != 60 {
t.Errorf("Expected retry after 60, got %d", err.RetryAfter)
}
if err.Code() != CodeRateLimit {
t.Errorf("Expected code %q, got %q", CodeRateLimit, err.Code())
}
}
func TestWrap(t *testing.T) {
t.Run("wrap standard error", func(t *testing.T) {
original := errors.New("original error")
wrapped := Wrap(original, "additional context")
if !strings.Contains(wrapped.Error(), "additional context") {
t.Errorf("Expected wrapped error to contain context: %q", wrapped.Error())
}
if !errors.Is(wrapped, original) {
t.Errorf("Expected wrapped error to preserve original error")
}
})
t.Run("wrap custom error", func(t *testing.T) {
original := NewNotFoundError("user", "123")
wrapped := Wrap(original, "failed to fetch user")
if !strings.Contains(wrapped.Error(), "failed to fetch user") {
t.Errorf("Expected wrapped error to contain new context: %q", wrapped.Error())
}
if errors.Unwrap(wrapped) != original {
t.Errorf("Expected wrapped error to preserve original error")
}
})
t.Run("wrap nil error", func(t *testing.T) {
wrapped := Wrap(nil, "context")
if wrapped != nil {
t.Errorf("Expected Wrap(nil) to return nil, got %v", wrapped)
}
})
}
func TestWrapf(t *testing.T) {
original := errors.New("connection failed")
wrapped := Wrapf(original, "failed to connect to %s:%d", "localhost", 5432)
expected := "failed to connect to localhost:5432"
if !strings.Contains(wrapped.Error(), expected) {
t.Errorf("Expected wrapped error to contain %q, got %q", expected, wrapped.Error())
}
}
func TestErrorChaining(t *testing.T) {
// Create a chain of errors
root := errors.New("root cause")
level1 := Wrap(root, "level 1")
level2 := Wrap(level1, "level 2")
level3 := Wrap(level2, "level 3")
// Test unwrapping
if !errors.Is(level3, root) {
t.Errorf("Expected error chain to preserve root cause")
}
// Test that we can unwrap multiple levels
unwrapped := errors.Unwrap(level3)
if unwrapped != level2 {
t.Errorf("Expected first unwrap to return level2")
}
unwrapped = errors.Unwrap(unwrapped)
if unwrapped != level1 {
t.Errorf("Expected second unwrap to return level1")
}
}
func TestStackTrace(t *testing.T) {
err := NewInternalError("test error", nil)
if len(err.Stack()) == 0 {
t.Errorf("Expected stack trace to be captured")
}
trace := err.StackTrace()
if trace == "" {
t.Errorf("Expected stack trace string to be non-empty")
}
// Stack trace should contain this test function
if !strings.Contains(trace, "TestStackTrace") {
t.Errorf("Expected stack trace to contain test function name: %s", trace)
}
}
func TestNew(t *testing.T) {
err := New("test error")
if err.Error() != "test error" {
t.Errorf("Expected error message 'test error', got %q", err.Error())
}
// Check that it implements our Error interface
var customErr Error
if !errors.As(err, &customErr) {
t.Errorf("Expected New() to return an Error interface")
}
}
func TestNewf(t *testing.T) {
err := Newf("error code: %d, message: %s", 404, "not found")
expected := "error code: 404, message: not found"
if err.Error() != expected {
t.Errorf("Expected error message %q, got %q", expected, err.Error())
}
}
func TestSentinelErrors(t *testing.T) {
tests := []struct {
name string
err error
}{
{"ErrNotFound", ErrNotFound},
{"ErrUnauthorized", ErrUnauthorized},
{"ErrForbidden", ErrForbidden},
{"ErrConflict", ErrConflict},
{"ErrInvalidInput", ErrInvalidInput},
{"ErrTimeout", ErrTimeout},
{"ErrServiceUnavailable", ErrServiceUnavailable},
{"ErrInternal", ErrInternal},
{"ErrTooManyRequests", ErrTooManyRequests},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wrapped := fmt.Errorf("wrapped: %w", tt.err)
if !errors.Is(wrapped, tt.err) {
t.Errorf("Expected errors.Is to work with sentinel error")
}
})
}
}
func BenchmarkNewValidationError(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewValidationError("field", "message", "value")
}
}
func BenchmarkWrap(b *testing.B) {
err := errors.New("original error")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Wrap(err, "wrapped")
}
}
func BenchmarkStackTrace(b *testing.B) {
err := NewInternalError("test", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = err.StackTrace()
}
}