Skip to content

Commit 774d83e

Browse files
committed
refactor readToken() and tests to use error values
1 parent 330d9a8 commit 774d83e

4 files changed

Lines changed: 83 additions & 64 deletions

File tree

credentials/jwt/jwt_file_reader.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ package jwt
2121
import (
2222
"encoding/base64"
2323
"encoding/json"
24+
"errors"
2425
"fmt"
2526
"os"
2627
"strings"
2728
"time"
2829
)
2930

31+
var (
32+
errTokenFileAccess = errors.New("token file access error")
33+
errJWTFormat = errors.New("invalid JWT format")
34+
errJWTValidation = errors.New("JWT validation failure")
35+
)
36+
3037
// jwtClaims represents the JWT claims structure for extracting expiration time.
3138
type jwtClaims struct {
3239
Exp int64 `json:"exp"`
@@ -42,17 +49,17 @@ type jWTFileReader struct {
4249
func (r *jWTFileReader) readToken() (string, time.Time, error) {
4350
tokenBytes, err := os.ReadFile(r.tokenFilePath)
4451
if err != nil {
45-
return "", time.Time{}, fmt.Errorf("failed to read token file %q: %v", r.tokenFilePath, err)
52+
return "", time.Time{}, fmt.Errorf("%w: failed to read token file %q: %v", errTokenFileAccess, r.tokenFilePath, err)
4653
}
4754

4855
token := strings.TrimSpace(string(tokenBytes))
4956
if token == "" {
50-
return "", time.Time{}, fmt.Errorf("token file %q is empty", r.tokenFilePath)
57+
return "", time.Time{}, fmt.Errorf("%w: token file %q is empty", errJWTFormat, r.tokenFilePath)
5158
}
5259

5360
exp, err := r.extractExpiration(token)
5461
if err != nil {
55-
return "", time.Time{}, fmt.Errorf("failed to parse JWT from token file %q: %v", r.tokenFilePath, err)
62+
return "", time.Time{}, fmt.Errorf("%q: %w", r.tokenFilePath, err)
5663
}
5764

5865
return token, exp, nil
@@ -62,7 +69,7 @@ func (r *jWTFileReader) readToken() (string, time.Time, error) {
6269
func (r *jWTFileReader) extractExpiration(token string) (time.Time, error) {
6370
parts := strings.Split(token, ".")
6471
if len(parts) != 3 {
65-
return time.Time{}, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
72+
return time.Time{}, fmt.Errorf("%w: expected 3 parts, got %d", errJWTFormat, len(parts))
6673
}
6774

6875
payload := parts[1]
@@ -73,23 +80,23 @@ func (r *jWTFileReader) extractExpiration(token string) (time.Time, error) {
7380

7481
payloadBytes, err := base64.URLEncoding.DecodeString(payload)
7582
if err != nil {
76-
return time.Time{}, fmt.Errorf("failed to decode JWT payload: %v", err)
83+
return time.Time{}, fmt.Errorf("%w: failed to decode JWT payload: %v", errJWTFormat, err)
7784
}
7885

7986
var claims jwtClaims
8087
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
81-
return time.Time{}, fmt.Errorf("failed to unmarshal JWT claims: %v", err)
88+
return time.Time{}, fmt.Errorf("%w: failed to unmarshal JWT claims: %v", errJWTFormat, err)
8289
}
8390

8491
if claims.Exp == 0 {
85-
return time.Time{}, fmt.Errorf("JWT token has no expiration claim")
92+
return time.Time{}, fmt.Errorf("%w: JWT token has no expiration claim", errJWTValidation)
8693
}
8794

8895
expTime := time.Unix(claims.Exp, 0)
8996

9097
// Check if token is already expired.
9198
if expTime.Before(time.Now()) {
92-
return time.Time{}, fmt.Errorf("JWT token is expired")
99+
return time.Time{}, fmt.Errorf("%w: JWT token is expired", errJWTValidation)
93100
}
94101

95102
return expTime, nil

credentials/jwt/jwt_file_reader_test.go

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package jwt
2121
import (
2222
"encoding/base64"
2323
"encoding/json"
24+
"errors"
2425
"fmt"
2526
"strings"
2627
"testing"
@@ -35,28 +36,28 @@ func TestJWTFileReader(t *testing.T) {
3536

3637
func (s) TestJWTFileReader_ReadToken_FileErrors(t *testing.T) {
3738
tests := []struct {
38-
name string
39-
create bool
40-
contents string
41-
wantErrContains string
39+
name string
40+
create bool
41+
contents string
42+
wantErr error
4243
}{
4344
{
44-
name: "nonexistent file",
45-
create: false,
46-
contents: "",
47-
wantErrContains: "failed to read token file",
45+
name: "nonexistent file",
46+
create: false,
47+
contents: "",
48+
wantErr: errTokenFileAccess,
4849
},
4950
{
50-
name: "empty file",
51-
create: true,
52-
contents: "",
53-
wantErrContains: "token file",
51+
name: "empty file",
52+
create: true,
53+
contents: "",
54+
wantErr: errJWTFormat,
5455
},
5556
{
56-
name: "file with whitespace only",
57-
create: true,
58-
contents: " \n\t ",
59-
wantErrContains: "token file",
57+
name: "file with whitespace only",
58+
create: true,
59+
contents: " \n\t ",
60+
wantErr: errJWTFormat,
6061
},
6162
}
6263

@@ -75,8 +76,8 @@ func (s) TestJWTFileReader_ReadToken_FileErrors(t *testing.T) {
7576
t.Fatal("ReadToken() expected error, got nil")
7677
}
7778

78-
if !strings.Contains(err.Error(), tt.wantErrContains) {
79-
t.Fatalf("ReadToken() error = %v, want error containing %q", err, tt.wantErrContains)
79+
if !errors.Is(err, tt.wantErr) {
80+
t.Fatalf("ReadToken() error = %v, want error %v", err, tt.wantErr)
8081
}
8182
})
8283
}
@@ -85,34 +86,34 @@ func (s) TestJWTFileReader_ReadToken_FileErrors(t *testing.T) {
8586
func (s) TestJWTFileReader_ReadToken_InvalidJWT(t *testing.T) {
8687
now := time.Now().Truncate(time.Second)
8788
tests := []struct {
88-
name string
89-
tokenContent string
90-
wantErrContains string
89+
name string
90+
tokenContent string
91+
wantErr error
9192
}{
9293
{
93-
name: "valid token without expiration",
94-
tokenContent: createTestJWT(t, time.Time{}),
95-
wantErrContains: "JWT token has no expiration claim",
94+
name: "valid token without expiration",
95+
tokenContent: createTestJWT(t, time.Time{}),
96+
wantErr: errJWTValidation,
9697
},
9798
{
98-
name: "expired token",
99-
tokenContent: createTestJWT(t, now.Add(-time.Hour)),
100-
wantErrContains: "JWT token is expired",
99+
name: "expired token",
100+
tokenContent: createTestJWT(t, now.Add(-time.Hour)),
101+
wantErr: errJWTValidation,
101102
},
102103
{
103-
name: "malformed JWT - not enough parts",
104-
tokenContent: "invalid.jwt",
105-
wantErrContains: "invalid JWT format: expected 3 parts, got 2",
104+
name: "malformed JWT - not enough parts",
105+
tokenContent: "invalid.jwt",
106+
wantErr: errJWTFormat,
106107
},
107108
{
108-
name: "malformed JWT - invalid base64",
109-
tokenContent: "header.invalid_base64!@#.signature",
110-
wantErrContains: "failed to decode JWT payload",
109+
name: "malformed JWT - invalid base64",
110+
tokenContent: "header.invalid_base64!@#.signature",
111+
wantErr: errJWTFormat,
111112
},
112113
{
113-
name: "malformed JWT - invalid JSON",
114-
tokenContent: createInvalidJSONJWT(t),
115-
wantErrContains: "failed to unmarshal JWT claims",
114+
name: "malformed JWT - invalid JSON",
115+
tokenContent: createInvalidJSONJWT(t),
116+
wantErr: errJWTFormat,
116117
},
117118
}
118119

@@ -123,8 +124,8 @@ func (s) TestJWTFileReader_ReadToken_InvalidJWT(t *testing.T) {
123124
reader := jWTFileReader{tokenFilePath: tokenFile}
124125
if _, _, err := reader.readToken(); err == nil {
125126
t.Fatal("ReadToken() expected error, got nil")
126-
} else if !strings.Contains(err.Error(), tt.wantErrContains) {
127-
t.Fatalf("ReadToken() error = %v, want error containing %q", err, tt.wantErrContains)
127+
} else if !errors.Is(err, tt.wantErr) {
128+
t.Fatalf("ReadToken() error = %v, want error %v", err, tt.wantErr)
128129
}
129130
})
130131
}

credentials/jwt/jwt_token_file_call_creds.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ package jwt
2121

2222
import (
2323
"context"
24+
"errors"
2425
"fmt"
25-
"strings"
2626
"sync"
2727
"time"
2828

@@ -154,10 +154,13 @@ func (c *jwtTokenFileCallCreds) refreshToken() {
154154
func (c *jwtTokenFileCallCreds) updateCacheLocked(token string, expiry time.Time, err error) {
155155
if err != nil {
156156
// Convert to gRPC status codes
157-
if strings.Contains(err.Error(), "failed to read token file") || strings.Contains(err.Error(), "token file") && strings.Contains(err.Error(), "is empty") {
158-
c.cachedError = status.Errorf(codes.Unavailable, "%v", err)
157+
if errors.Is(err, errTokenFileAccess) {
158+
c.cachedError = status.Error(codes.Unavailable, err.Error())
159+
} else if errors.Is(err, errJWTFormat) || errors.Is(err, errJWTValidation) {
160+
c.cachedError = status.Error(codes.Unauthenticated, err.Error())
159161
} else {
160-
c.cachedError = status.Errorf(codes.Unauthenticated, "%v", err)
162+
// Should not happen. Treat unknown errors as UNAUTHENTICATED.
163+
c.cachedError = status.Error(codes.Unauthenticated, err.Error())
161164
}
162165
c.retryAttempt++
163166
backoffDelay := c.backoffStrategy.Backoff(c.retryAttempt - 1)

credentials/jwt/jwt_token_file_call_creds_test.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ func (s) TestNewTokenFileCallCredentialsMissingFilepath(t *testing.T) {
6161
if err == nil {
6262
t.Fatalf("NewTokenFileCallCredentials() expected error, got nil")
6363
}
64-
expectedErr := "tokenFilePath cannot be empty"
65-
if !strings.Contains(err.Error(), expectedErr) {
66-
t.Fatalf("NewTokenFileCallCredentials() error = %v, want error containing %q", err, expectedErr)
67-
}
6864
}
6965

7066
func (s) TestTokenFileCallCreds_RequireTransportSecurity(t *testing.T) {
@@ -81,11 +77,12 @@ func (s) TestTokenFileCallCreds_RequireTransportSecurity(t *testing.T) {
8177
func (s) TestTokenFileCallCreds_GetRequestMetadata(t *testing.T) {
8278
now := time.Now().Truncate(time.Second)
8379
tests := []struct {
84-
name string
85-
tokenContent string
86-
authInfo credentials.AuthInfo
87-
grpcCode codes.Code
88-
wantMetadata map[string]string
80+
name string
81+
invalidTokenPath bool
82+
tokenContent string
83+
authInfo credentials.AuthInfo
84+
grpcCode codes.Code
85+
wantMetadata map[string]string
8986
}{
9087
{
9188
name: "valid token with future expiration",
@@ -98,13 +95,20 @@ func (s) TestTokenFileCallCreds_GetRequestMetadata(t *testing.T) {
9895
name: "insufficient security level",
9996
tokenContent: createTestJWT(t, now.Add(time.Hour)),
10097
authInfo: &testAuthInfo{secLevel: credentials.NoSecurity},
101-
grpcCode: codes.Unknown,
98+
grpcCode: codes.Unknown, // http2Client.getCallAuthData actually transforms such errors into into Unauthenticated
99+
},
100+
{
101+
name: "unreachable token file",
102+
invalidTokenPath: true,
103+
tokenContent: "",
104+
authInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
105+
grpcCode: codes.Unavailable,
102106
},
103107
{
104-
name: "unreachable token file",
108+
name: "empty file",
105109
tokenContent: "",
106110
authInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
107-
grpcCode: codes.Unavailable,
111+
grpcCode: codes.Unauthenticated,
108112
},
109113
{
110114
name: "malformed JWT token",
@@ -116,8 +120,12 @@ func (s) TestTokenFileCallCreds_GetRequestMetadata(t *testing.T) {
116120

117121
for _, tt := range tests {
118122
t.Run(tt.name, func(t *testing.T) {
119-
tokenFile := writeTempFile(t, "token", tt.tokenContent)
120-
123+
var tokenFile string
124+
if tt.invalidTokenPath {
125+
tokenFile = "/does-not-exist"
126+
} else {
127+
tokenFile = writeTempFile(t, "token", tt.tokenContent)
128+
}
121129
creds, err := NewTokenFileCallCredentials(tokenFile)
122130
if err != nil {
123131
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)

0 commit comments

Comments
 (0)