-
-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: standardize Go error handling with error chain support #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| package auth | ||
|
|
||
| import "errors" | ||
| import pkgerrors "github-project-status-viewer-server/pkg/errors" | ||
|
|
||
| var ( | ||
| ErrInvalidAuthHeader = errors.New("authorization header must be 'Bearer <token>'") | ||
| ErrInvalidAuthHeader = pkgerrors.ErrInvalidAuthHeader | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package errors | ||
|
|
||
| import "errors" | ||
|
|
||
| // Configuration errors | ||
| var ( | ||
| ErrJWTSecretMissing = errors.New("JWT_SECRET not configured") | ||
| ErrOAuthConfigMissing = errors.New("OAuth configuration missing") | ||
| ErrRedisConfigMissing = errors.New("upstash redis configuration missing") | ||
| ErrInvalidAuthHeader = errors.New("authorization header must be 'Bearer <token>'") | ||
| ErrMissingAuthCode = errors.New("authorization code is required") | ||
| ErrMissingStateParam = errors.New("state parameter is required for CSRF protection") | ||
| ErrBearerTokenRequired = errors.New("bearer token required") | ||
| ) | ||
|
|
||
| // Token errors | ||
| var ( | ||
| ErrInvalidTokenFormat = errors.New("invalid token format") | ||
| ErrTokenExpired = errors.New("token expired") | ||
| ErrInvalidSigningMethod = errors.New("unexpected signing method") | ||
| ErrInvalidAccessTokenClaims = errors.New("invalid access token claims type") | ||
| ErrInvalidRefreshTokenClaims = errors.New("invalid refresh token claims type") | ||
| ErrSessionNotFound = errors.New("session not found") | ||
| ErrSessionExpired = errors.New("session expired or invalid") | ||
| ErrSessionMismatch = errors.New("session mismatch detected") | ||
| ErrRefreshTokenRevoked = errors.New("refresh token has been revoked or expired") | ||
| ) | ||
|
|
||
| // OAuth errors | ||
| var ( | ||
| ErrOAuthExchangeFailed = errors.New("failed to exchange authorization code") | ||
| ErrOAuthRequestFailed = errors.New("OAuth request failed") | ||
| ErrAuthenticationFailed = errors.New("authentication failed") | ||
| ) | ||
|
|
||
| // Redis errors | ||
| var ( | ||
| ErrKeyNotFound = errors.New("key not found") | ||
| ErrRedisRequestFailed = errors.New("redis request failed") | ||
| ErrUnexpectedResponse = errors.New("unexpected response type") | ||
| ) | ||
|
|
||
| // Crypto errors | ||
| var ( | ||
| ErrRandomGeneration = errors.New("failed to generate random bytes") | ||
| ) | ||
|
|
||
| // HTTP errors | ||
| var ( | ||
| ErrMethodNotAllowed = errors.New("method not allowed") | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| package errors | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestErrorWrapping(t *testing.T) { | ||
| tests := []struct { | ||
| baseErr error | ||
| name string | ||
| wrapContext string | ||
| }{ | ||
| { | ||
| name: "wrap JWT secret error", | ||
| baseErr: ErrJWTSecretMissing, | ||
| wrapContext: "manager initialization", | ||
| }, | ||
| { | ||
| name: "wrap OAuth config error", | ||
| baseErr: ErrOAuthConfigMissing, | ||
| wrapContext: "client initialization", | ||
| }, | ||
| { | ||
| name: "wrap session not found error", | ||
| baseErr: ErrSessionNotFound, | ||
| wrapContext: "verify access token", | ||
| }, | ||
| { | ||
| name: "wrap token expired error", | ||
| baseErr: ErrTokenExpired, | ||
| wrapContext: "validate refresh token", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| wrappedErr := fmt.Errorf("%s: %w", tt.wrapContext, tt.baseErr) | ||
|
|
||
| if !errors.Is(wrappedErr, tt.baseErr) { | ||
| t.Errorf("errors.Is() failed: wrapped error does not match base error") | ||
| } | ||
|
|
||
| if wrappedErr.Error() == tt.baseErr.Error() { | ||
| t.Errorf("wrapped error message should include context, got %q", wrappedErr.Error()) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestErrorChains(t *testing.T) { | ||
| tests := []struct { | ||
| buildChain func() error | ||
| name string | ||
| targetError error | ||
| }{ | ||
| { | ||
| name: "multi-level JWT error chain", | ||
| targetError: ErrJWTSecretMissing, | ||
| buildChain: func() error { | ||
| err := ErrJWTSecretMissing | ||
| err = fmt.Errorf("NewManager failed: %w", err) | ||
| err = fmt.Errorf("GetManager failed: %w", err) | ||
| return err | ||
| }, | ||
| }, | ||
| { | ||
| name: "multi-level session error chain", | ||
| targetError: ErrSessionNotFound, | ||
| buildChain: func() error { | ||
| err := ErrSessionNotFound | ||
| err = fmt.Errorf("redis.Get failed: %w", err) | ||
| err = fmt.Errorf("verify handler failed: %w", err) | ||
| return err | ||
| }, | ||
| }, | ||
| { | ||
| name: "OAuth error chain", | ||
| targetError: ErrOAuthExchangeFailed, | ||
| buildChain: func() error { | ||
| err := ErrOAuthExchangeFailed | ||
| err = fmt.Errorf("requestToken failed: %w", err) | ||
| err = fmt.Errorf("callback handler failed: %w", err) | ||
| return err | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| chainedErr := tt.buildChain() | ||
|
|
||
| if !errors.Is(chainedErr, tt.targetError) { | ||
| t.Errorf("errors.Is() failed: error chain does not contain target error %v", tt.targetError) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestErrorUnwrap(t *testing.T) { | ||
| baseErr := ErrSessionNotFound | ||
| wrappedErr := fmt.Errorf("context: %w", baseErr) | ||
|
|
||
| unwrapped := errors.Unwrap(wrappedErr) | ||
| if unwrapped != baseErr { | ||
| t.Errorf("errors.Unwrap() = %v, want %v", unwrapped, baseErr) | ||
| } | ||
| } | ||
|
|
||
| func TestErrorEquality(t *testing.T) { | ||
| tests := []struct { | ||
| err1 error | ||
| err2 error | ||
| name string | ||
| wantEq bool | ||
| }{ | ||
| { | ||
| name: "same error instance", | ||
| err1: ErrSessionNotFound, | ||
| err2: ErrSessionNotFound, | ||
| wantEq: true, | ||
| }, | ||
| { | ||
| name: "different error instances", | ||
| err1: ErrSessionNotFound, | ||
| err2: ErrSessionExpired, | ||
| wantEq: false, | ||
| }, | ||
| { | ||
| name: "wrapped vs unwrapped same error", | ||
| err1: fmt.Errorf("context: %w", ErrSessionNotFound), | ||
| err2: ErrSessionNotFound, | ||
| wantEq: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| equal := (tt.err1 == tt.err2) | ||
| if equal != tt.wantEq { | ||
| t.Errorf("error equality = %v, want %v", equal, tt.wantEq) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestErrorIsComparison(t *testing.T) { | ||
| tests := []struct { | ||
| err error | ||
| name string | ||
| target error | ||
| want bool | ||
| }{ | ||
| { | ||
| name: "exact match", | ||
| err: ErrSessionNotFound, | ||
| target: ErrSessionNotFound, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "wrapped error matches", | ||
| err: fmt.Errorf("context: %w", ErrSessionNotFound), | ||
| target: ErrSessionNotFound, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "different errors", | ||
| err: ErrSessionNotFound, | ||
| target: ErrSessionExpired, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "double wrapped error matches", | ||
| err: fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", ErrSessionNotFound)), | ||
| target: ErrSessionNotFound, | ||
| want: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := errors.Is(tt.err, tt.target) | ||
| if got != tt.want { | ||
| t.Errorf("errors.Is() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestDefinedErrors(t *testing.T) { | ||
| errorGroups := []struct { | ||
| name string | ||
| errors []error | ||
| }{ | ||
| { | ||
| name: "Configuration Errors", | ||
| errors: []error{ | ||
| ErrJWTSecretMissing, | ||
| ErrOAuthConfigMissing, | ||
| ErrRedisConfigMissing, | ||
| }, | ||
| }, | ||
| { | ||
| name: "Token Errors", | ||
| errors: []error{ | ||
| ErrInvalidTokenFormat, | ||
| ErrTokenExpired, | ||
| ErrInvalidSigningMethod, | ||
| ErrInvalidAccessTokenClaims, | ||
| ErrInvalidRefreshTokenClaims, | ||
| ErrSessionNotFound, | ||
| ErrSessionExpired, | ||
| ErrSessionMismatch, | ||
| ErrRefreshTokenRevoked, | ||
| }, | ||
| }, | ||
| { | ||
| name: "Redis Errors", | ||
| errors: []error{ | ||
| ErrKeyNotFound, | ||
| ErrRedisRequestFailed, | ||
| ErrUnexpectedResponse, | ||
| }, | ||
| }, | ||
| { | ||
| name: "OAuth Errors", | ||
| errors: []error{ | ||
| ErrOAuthExchangeFailed, | ||
| ErrOAuthRequestFailed, | ||
| ErrAuthenticationFailed, | ||
| }, | ||
| }, | ||
| { | ||
| name: "Crypto Errors", | ||
| errors: []error{ | ||
| ErrRandomGeneration, | ||
| }, | ||
| }, | ||
| { | ||
| name: "HTTP Errors", | ||
| errors: []error{ | ||
| ErrInvalidAuthHeader, | ||
| ErrMissingAuthCode, | ||
| ErrMissingStateParam, | ||
| ErrBearerTokenRequired, | ||
| }, | ||
|
kubrickcode marked this conversation as resolved.
|
||
| }, | ||
| } | ||
|
|
||
| for _, group := range errorGroups { | ||
| t.Run(group.name, func(t *testing.T) { | ||
| for _, err := range group.errors { | ||
| if err == nil { | ||
| t.Errorf("error in group %q should not be nil", group.name) | ||
| } | ||
| if err.Error() == "" { | ||
| t.Errorf("error message for %v in group %q should not be empty", err, group.name) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.