Files
Pulse/internal/errors/errors_test.go
2025-11-26 14:09:14 +00:00

564 lines
13 KiB
Go

package errors
import (
"errors"
"fmt"
"testing"
)
func TestMonitorError_Error(t *testing.T) {
baseErr := fmt.Errorf("base error")
tests := []struct {
name string
err *MonitorError
expected string
}{
{
name: "with node",
err: &MonitorError{
Op: "poll_nodes",
Instance: "pve1",
Node: "node1",
Err: baseErr,
},
expected: "poll_nodes failed on pve1/node1: base error",
},
{
name: "with instance only",
err: &MonitorError{
Op: "get_vms",
Instance: "pve1",
Err: baseErr,
},
expected: "get_vms failed on pve1: base error",
},
{
name: "operation only",
err: &MonitorError{
Op: "sync",
Err: baseErr,
},
expected: "sync failed: base error",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.err.Error()
if result != tc.expected {
t.Errorf("Error() = %q, want %q", result, tc.expected)
}
})
}
}
func TestMonitorError_Unwrap(t *testing.T) {
baseErr := fmt.Errorf("wrapped error")
monErr := &MonitorError{
Op: "test",
Instance: "instance",
Err: baseErr,
}
unwrapped := monErr.Unwrap()
if unwrapped != baseErr {
t.Errorf("Unwrap() = %v, want %v", unwrapped, baseErr)
}
}
func TestMonitorError_Is(t *testing.T) {
tests := []struct {
name string
err *MonitorError
target error
expected bool
}{
{
name: "not found type matches ErrNotFound",
err: &MonitorError{Type: ErrorTypeNotFound},
target: ErrNotFound,
expected: true,
},
{
name: "auth type matches ErrUnauthorized",
err: &MonitorError{Type: ErrorTypeAuth},
target: ErrUnauthorized,
expected: true,
},
{
name: "auth type matches ErrForbidden",
err: &MonitorError{Type: ErrorTypeAuth},
target: ErrForbidden,
expected: true,
},
{
name: "timeout type matches ErrTimeout",
err: &MonitorError{Type: ErrorTypeTimeout},
target: ErrTimeout,
expected: true,
},
{
name: "connection type matches ErrConnectionFailed",
err: &MonitorError{Type: ErrorTypeConnection},
target: ErrConnectionFailed,
expected: true,
},
{
name: "wrapped error matches",
err: &MonitorError{Type: ErrorTypeInternal, Err: ErrInvalidInput},
target: ErrInvalidInput,
expected: true,
},
{
name: "type mismatch",
err: &MonitorError{Type: ErrorTypeInternal},
target: ErrNotFound,
expected: false,
},
{
name: "nil target",
err: &MonitorError{Type: ErrorTypeNotFound},
target: nil,
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.err.Is(tc.target)
if result != tc.expected {
t.Errorf("Is(%v) = %v, want %v", tc.target, result, tc.expected)
}
})
}
}
func TestNewMonitorError(t *testing.T) {
baseErr := fmt.Errorf("test error")
err := NewMonitorError(ErrorTypeConnection, "poll", "instance1", baseErr)
if err.Type != ErrorTypeConnection {
t.Errorf("Type = %v, want %v", err.Type, ErrorTypeConnection)
}
if err.Op != "poll" {
t.Errorf("Op = %v, want %v", err.Op, "poll")
}
if err.Instance != "instance1" {
t.Errorf("Instance = %v, want %v", err.Instance, "instance1")
}
if err.Err != baseErr {
t.Errorf("Err = %v, want %v", err.Err, baseErr)
}
if err.Timestamp.IsZero() {
t.Error("Timestamp should be set")
}
}
func TestMonitorError_WithNode(t *testing.T) {
err := NewMonitorError(ErrorTypeConnection, "poll", "instance1", nil)
result := err.WithNode("node1")
if result.Node != "node1" {
t.Errorf("Node = %v, want %v", result.Node, "node1")
}
// Should return same instance for chaining
if result != err {
t.Error("WithNode() should return same instance")
}
}
func TestMonitorError_WithStatusCode(t *testing.T) {
tests := []struct {
name string
statusCode int
wantRetryable bool
}{
{
name: "500 error is retryable",
statusCode: 500,
wantRetryable: true,
},
{
name: "502 error is retryable",
statusCode: 502,
wantRetryable: true,
},
{
name: "429 rate limit is retryable",
statusCode: 429,
wantRetryable: true,
},
{
name: "408 timeout is retryable",
statusCode: 408,
wantRetryable: true,
},
{
name: "400 bad request not retryable",
statusCode: 400,
wantRetryable: false,
},
{
name: "401 unauthorized not retryable",
statusCode: 401,
wantRetryable: false,
},
{
name: "403 forbidden not retryable",
statusCode: 403,
wantRetryable: false,
},
{
name: "404 not found not retryable",
statusCode: 404,
wantRetryable: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := NewMonitorError(ErrorTypeAPI, "request", "instance", nil)
result := err.WithStatusCode(tc.statusCode)
if result.StatusCode != tc.statusCode {
t.Errorf("StatusCode = %v, want %v", result.StatusCode, tc.statusCode)
}
if result.Retryable != tc.wantRetryable {
t.Errorf("Retryable = %v, want %v", result.Retryable, tc.wantRetryable)
}
})
}
}
func TestIsRetryable(t *testing.T) {
tests := []struct {
name string
errorType ErrorType
err error
wantRetryable bool
}{
{
name: "connection error is retryable",
errorType: ErrorTypeConnection,
err: nil,
wantRetryable: true,
},
{
name: "timeout error is retryable",
errorType: ErrorTypeTimeout,
err: nil,
wantRetryable: true,
},
{
name: "auth error not retryable",
errorType: ErrorTypeAuth,
err: nil,
wantRetryable: false,
},
{
name: "validation error not retryable",
errorType: ErrorTypeValidation,
err: nil,
wantRetryable: false,
},
{
name: "not found error not retryable",
errorType: ErrorTypeNotFound,
err: nil,
wantRetryable: false,
},
{
name: "internal error retryable by default",
errorType: ErrorTypeInternal,
err: nil,
wantRetryable: true,
},
{
name: "internal with invalid input not retryable",
errorType: ErrorTypeInternal,
err: ErrInvalidInput,
wantRetryable: false,
},
{
name: "internal with forbidden not retryable",
errorType: ErrorTypeInternal,
err: ErrForbidden,
wantRetryable: false,
},
{
name: "API error retryable by default",
errorType: ErrorTypeAPI,
err: nil,
wantRetryable: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := isRetryable(tc.errorType, tc.err)
if result != tc.wantRetryable {
t.Errorf("isRetryable(%v, %v) = %v, want %v", tc.errorType, tc.err, result, tc.wantRetryable)
}
})
}
}
func TestWrapConnectionError(t *testing.T) {
baseErr := fmt.Errorf("connection refused")
err := WrapConnectionError("connect", "pve1", baseErr)
var monErr *MonitorError
if !errors.As(err, &monErr) {
t.Fatal("WrapConnectionError() did not return MonitorError")
}
if monErr.Type != ErrorTypeConnection {
t.Errorf("Type = %v, want %v", monErr.Type, ErrorTypeConnection)
}
if monErr.Op != "connect" {
t.Errorf("Op = %v, want %v", monErr.Op, "connect")
}
if monErr.Instance != "pve1" {
t.Errorf("Instance = %v, want %v", monErr.Instance, "pve1")
}
}
func TestWrapAuthError(t *testing.T) {
baseErr := fmt.Errorf("invalid credentials")
err := WrapAuthError("login", "pve1", baseErr)
var monErr *MonitorError
if !errors.As(err, &monErr) {
t.Fatal("WrapAuthError() did not return MonitorError")
}
if monErr.Type != ErrorTypeAuth {
t.Errorf("Type = %v, want %v", monErr.Type, ErrorTypeAuth)
}
}
func TestWrapAPIError(t *testing.T) {
baseErr := fmt.Errorf("server error")
err := WrapAPIError("request", "pve1", baseErr, 500)
var monErr *MonitorError
if !errors.As(err, &monErr) {
t.Fatal("WrapAPIError() did not return MonitorError")
}
if monErr.Type != ErrorTypeAPI {
t.Errorf("Type = %v, want %v", monErr.Type, ErrorTypeAPI)
}
if monErr.StatusCode != 500 {
t.Errorf("StatusCode = %v, want %v", monErr.StatusCode, 500)
}
}
func TestIsRetryableError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "MonitorError retryable",
err: NewMonitorError(ErrorTypeConnection, "test", "instance", nil),
expected: true,
},
{
name: "MonitorError not retryable",
err: NewMonitorError(ErrorTypeAuth, "test", "instance", nil),
expected: false,
},
{
name: "ErrTimeout is retryable",
err: ErrTimeout,
expected: true,
},
{
name: "ErrConnectionFailed is retryable",
err: ErrConnectionFailed,
expected: true,
},
{
name: "wrapped ErrTimeout is retryable",
err: fmt.Errorf("wrapped: %w", ErrTimeout),
expected: true,
},
{
name: "regular error not retryable",
err: fmt.Errorf("regular error"),
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := IsRetryableError(tc.err)
if result != tc.expected {
t.Errorf("IsRetryableError(%v) = %v, want %v", tc.err, result, tc.expected)
}
})
}
}
func TestIsAuthError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "nil error",
err: nil,
expected: false,
},
{
name: "MonitorError auth type",
err: NewMonitorError(ErrorTypeAuth, "login", "instance", nil),
expected: true,
},
{
name: "MonitorError with 401 status",
err: NewMonitorError(ErrorTypeAPI, "request", "instance", nil).WithStatusCode(401),
expected: true,
},
{
name: "MonitorError with 403 status",
err: NewMonitorError(ErrorTypeAPI, "request", "instance", nil).WithStatusCode(403),
expected: true,
},
{
name: "ErrUnauthorized",
err: ErrUnauthorized,
expected: true,
},
{
name: "ErrForbidden",
err: ErrForbidden,
expected: true,
},
{
name: "wrapped ErrUnauthorized",
err: fmt.Errorf("wrapped: %w", ErrUnauthorized),
expected: true,
},
{
name: "error with 'unauthorized' in message",
err: fmt.Errorf("request unauthorized"),
expected: true,
},
{
name: "error with 'forbidden' in message",
err: fmt.Errorf("access forbidden"),
expected: true,
},
{
name: "error with 'authentication failed' in message",
err: fmt.Errorf("authentication failed"),
expected: true,
},
{
name: "error with 'authentication error' in message",
err: fmt.Errorf("authentication error occurred"),
expected: true,
},
{
name: "error with '401' in message",
err: fmt.Errorf("HTTP 401 response"),
expected: true,
},
{
name: "error with '403' in message",
err: fmt.Errorf("HTTP 403 response"),
expected: true,
},
{
name: "regular error",
err: fmt.Errorf("some other error"),
expected: false,
},
{
name: "MonitorError connection type not auth",
err: NewMonitorError(ErrorTypeConnection, "connect", "instance", nil),
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := IsAuthError(tc.err)
if result != tc.expected {
t.Errorf("IsAuthError(%v) = %v, want %v", tc.err, result, tc.expected)
}
})
}
}
func TestErrorTypes(t *testing.T) {
// Verify error types are defined correctly
types := []ErrorType{
ErrorTypeConnection,
ErrorTypeAuth,
ErrorTypeValidation,
ErrorTypeNotFound,
ErrorTypeInternal,
ErrorTypeAPI,
ErrorTypeTimeout,
}
for _, errorType := range types {
if errorType == "" {
t.Error("ErrorType should not be empty")
}
}
}
func TestBaseErrors(t *testing.T) {
// Verify base errors are defined
baseErrors := []error{
ErrNotFound,
ErrUnauthorized,
ErrForbidden,
ErrTimeout,
ErrInvalidInput,
ErrConnectionFailed,
ErrInternalError,
}
for _, err := range baseErrors {
if err == nil {
t.Error("Base error should not be nil")
}
if err.Error() == "" {
t.Error("Base error should have non-empty message")
}
}
}
func TestMonitorError_ErrorsIs(t *testing.T) {
// Test using errors.Is with MonitorError
monErr := NewMonitorError(ErrorTypeNotFound, "get", "instance", nil)
if !errors.Is(monErr, ErrNotFound) {
t.Error("errors.Is() should return true for matching error type")
}
}
func TestMonitorError_ErrorsAs(t *testing.T) {
// Test using errors.As with MonitorError
originalErr := NewMonitorError(ErrorTypeConnection, "connect", "pve1", fmt.Errorf("refused"))
wrappedErr := fmt.Errorf("outer: %w", originalErr)
var monErr *MonitorError
if !errors.As(wrappedErr, &monErr) {
t.Error("errors.As() should extract MonitorError from wrapped error")
}
if monErr.Op != "connect" {
t.Errorf("Op = %v, want %v", monErr.Op, "connect")
}
}