package api import ( "errors" "net/http" "testing" ) func TestHTTPError_Error(t *testing.T) { tests := []struct { name string err *HTTPError expected string }{ { name: "simple error", err: &HTTPError{Status: 404, Code: "NOT_FOUND", Message: "user not found"}, expected: "user not found", }, { name: "error with cause", err: &HTTPError{ Status: 500, Code: "INTERNAL_ERROR", Message: "database error", cause: errors.New("connection refused"), }, expected: "database error: connection refused", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.err.Error(); got != tt.expected { t.Errorf("HTTPError.Error() = %q, want %q", got, tt.expected) } }) } } func TestHTTPError_Is(t *testing.T) { tests := []struct { name string err error target error expected bool }{ { name: "matches ErrNotFound", err: NotFound("user not found"), target: ErrNotFound, expected: true, }, { name: "matches ErrBadRequest", err: BadRequest("invalid input"), target: ErrBadRequest, expected: true, }, { name: "does not match different status", err: NotFound("not found"), target: ErrBadRequest, expected: false, }, { name: "wrapped error matches", err: WrapError(ErrNotFound, errors.New("db error")), target: ErrNotFound, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := errors.Is(tt.err, tt.target); got != tt.expected { t.Errorf("errors.Is() = %v, want %v", got, tt.expected) } }) } } func TestHTTPError_Unwrap(t *testing.T) { cause := errors.New("underlying error") err := WrapError(ErrInternal, cause) unwrapped := errors.Unwrap(err) if unwrapped != cause { t.Errorf("Unwrap() = %v, want %v", unwrapped, cause) } } func TestFactoryFunctions(t *testing.T) { tests := []struct { name string err error wantStatus int wantCode string wantContains string }{ { name: "BadRequest", err: BadRequest("invalid input"), wantStatus: http.StatusBadRequest, wantCode: "BAD_REQUEST", wantContains: "invalid input", }, { name: "BadRequestf", err: BadRequestf("field %s is invalid", "email"), wantStatus: http.StatusBadRequest, wantCode: "BAD_REQUEST", wantContains: "field email is invalid", }, { name: "Unauthorized", err: Unauthorized("token expired"), wantStatus: http.StatusUnauthorized, wantCode: "UNAUTHORIZED", wantContains: "token expired", }, { name: "Forbidden", err: Forbidden("access denied"), wantStatus: http.StatusForbidden, wantCode: "FORBIDDEN", wantContains: "access denied", }, { name: "NotFound", err: NotFound("user not found"), wantStatus: http.StatusNotFound, wantCode: "NOT_FOUND", wantContains: "user not found", }, { name: "NotFoundf", err: NotFoundf("user %s not found", "123"), wantStatus: http.StatusNotFound, wantCode: "NOT_FOUND", wantContains: "user 123 not found", }, { name: "Conflict", err: Conflict("already exists"), wantStatus: http.StatusConflict, wantCode: "CONFLICT", wantContains: "already exists", }, { name: "Internal", err: Internal("something went wrong"), wantStatus: http.StatusInternalServerError, wantCode: "INTERNAL_ERROR", wantContains: "something went wrong", }, { name: "Validation", err: Validation("validation failed"), wantStatus: http.StatusBadRequest, wantCode: "VALIDATION_ERROR", wantContains: "validation failed", }, { name: "UnprocessableEntity", err: UnprocessableEntity("semantic error"), wantStatus: http.StatusUnprocessableEntity, wantCode: "UNPROCESSABLE_ENTITY", wantContains: "semantic error", }, { name: "TooManyRequests", err: TooManyRequests("rate limited"), wantStatus: http.StatusTooManyRequests, wantCode: "TOO_MANY_REQUESTS", wantContains: "rate limited", }, { name: "ServiceUnavailable", err: ServiceUnavailable("maintenance"), wantStatus: http.StatusServiceUnavailable, wantCode: "SERVICE_UNAVAILABLE", wantContains: "maintenance", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { httpErr := AsHTTPError(tt.err) if httpErr == nil { t.Fatal("expected HTTPError") } if httpErr.Status != tt.wantStatus { t.Errorf("Status = %d, want %d", httpErr.Status, tt.wantStatus) } if httpErr.Code != tt.wantCode { t.Errorf("Code = %q, want %q", httpErr.Code, tt.wantCode) } if httpErr.Message != tt.wantContains { t.Errorf("Message = %q, want %q", httpErr.Message, tt.wantContains) } }) } } func TestWithDetails(t *testing.T) { details := map[string]string{"field": "email", "error": "invalid format"} err := WithDetails(BadRequest("validation failed"), details) httpErr := AsHTTPError(err) if httpErr == nil { t.Fatal("expected HTTPError") } if httpErr.Details == nil { t.Fatal("expected Details to be set") } detailsMap, ok := httpErr.Details.(map[string]string) if !ok { t.Fatalf("expected map[string]string, got %T", httpErr.Details) } if detailsMap["field"] != "email" { t.Errorf("Details[field] = %q, want %q", detailsMap["field"], "email") } } func TestWithCode(t *testing.T) { err := WithCode(Forbidden("access denied"), "KEY_REVOKED") httpErr := AsHTTPError(err) if httpErr == nil { t.Fatal("expected HTTPError") } if httpErr.Code != "KEY_REVOKED" { t.Errorf("Code = %q, want %q", httpErr.Code, "KEY_REVOKED") } if httpErr.Status != http.StatusForbidden { t.Errorf("Status = %d, want %d", httpErr.Status, http.StatusForbidden) } } func TestStatusCode(t *testing.T) { tests := []struct { name string err error wantStatus int }{ { name: "HTTPError", err: NotFound("not found"), wantStatus: http.StatusNotFound, }, { name: "regular error", err: errors.New("some error"), wantStatus: http.StatusInternalServerError, }, { name: "nil error returns 200 OK", err: nil, wantStatus: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := StatusCode(tt.err); got != tt.wantStatus { t.Errorf("StatusCode() = %d, want %d", got, tt.wantStatus) } }) } } func TestIsHTTPError(t *testing.T) { tests := []struct { name string err error expected bool }{ { name: "HTTPError", err: NotFound("not found"), expected: true, }, { name: "regular error", err: errors.New("some error"), expected: false, }, { name: "nil", err: nil, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsHTTPError(tt.err); got != tt.expected { t.Errorf("IsHTTPError() = %v, want %v", got, tt.expected) } }) } }