mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
564 lines
13 KiB
Go
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")
|
|
}
|
|
}
|