test: add unit tests for errors package
This commit is contained in:
parent
ae876b4a10
commit
159a1cb3a3
1 changed files with 563 additions and 0 deletions
563
internal/errors/errors_test.go
Normal file
563
internal/errors/errors_test.go
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue