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") } }