Skip to content

Commit 1a60b89

Browse files
committed
Expanded unit tests
1 parent 7792cb3 commit 1a60b89

11 files changed

+1849
-0
lines changed

auth/jwt/jwt.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ func (s *Service) WithRemoteValidator(config RemoteConfig) *Service {
7171
return s
7272
}
7373

74+
// SetRemoteValidatorForTesting sets the remote validator for testing purposes.
75+
// This method should only be used in tests.
76+
func (s *Service) SetRemoteValidatorForTesting(validator TokenValidator) {
77+
s.remoteValidator = validator
78+
}
79+
7480
// Claims represents the JWT claims contained in a token.
7581
type Claims struct {
7682
// UserID is the unique identifier of the user (stored in the 'sub' claim)

auth/jwt/mock_validator_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2025 A Bit of Help, Inc.
2+
3+
package jwt_test
4+
5+
import (
6+
"context"
7+
"errors"
8+
9+
autherrors "github.com/abitofhelp/servicelib/auth/errors"
10+
"github.com/abitofhelp/servicelib/auth/jwt"
11+
)
12+
13+
// MockValidator is a mock implementation of the TokenValidator interface for testing.
14+
type MockValidator struct {
15+
// ShouldSucceed determines whether the validation should succeed or fail
16+
ShouldSucceed bool
17+
18+
// ErrorToReturn is the error to return when validation fails
19+
ErrorToReturn error
20+
21+
// ClaimsToReturn is the claims to return when validation succeeds
22+
ClaimsToReturn *jwt.Claims
23+
24+
// Called tracks whether ValidateToken was called
25+
Called bool
26+
}
27+
28+
// ValidateToken implements the TokenValidator interface for testing.
29+
func (m *MockValidator) ValidateToken(ctx context.Context, tokenString string) (*jwt.Claims, error) {
30+
m.Called = true
31+
32+
if tokenString == "" {
33+
return nil, autherrors.ErrMissingToken
34+
}
35+
36+
if m.ShouldSucceed {
37+
return m.ClaimsToReturn, nil
38+
}
39+
40+
if m.ErrorToReturn != nil {
41+
return nil, m.ErrorToReturn
42+
}
43+
44+
return nil, errors.New("mock validation failed")
45+
}
46+
47+
// NewSuccessfulMockValidator creates a new MockValidator that succeeds with the given claims.
48+
func NewSuccessfulMockValidator(claims *jwt.Claims) *MockValidator {
49+
return &MockValidator{
50+
ShouldSucceed: true,
51+
ClaimsToReturn: claims,
52+
}
53+
}
54+
55+
// NewFailingMockValidator creates a new MockValidator that fails with the given error.
56+
func NewFailingMockValidator(err error) *MockValidator {
57+
return &MockValidator{
58+
ShouldSucceed: false,
59+
ErrorToReturn: err,
60+
}
61+
}

auth/jwt/remote_validation_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (c) 2025 A Bit of Help, Inc.
2+
3+
package jwt_test
4+
5+
import (
6+
"context"
7+
"testing"
8+
"time"
9+
10+
autherrors "github.com/abitofhelp/servicelib/auth/errors"
11+
"github.com/abitofhelp/servicelib/auth/jwt"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"go.uber.org/zap"
15+
)
16+
17+
// TestRemoteValidationSuccess tests the case where remote validation succeeds
18+
func TestRemoteValidationSuccess(t *testing.T) {
19+
logger := zap.NewNop()
20+
config := jwt.Config{
21+
SecretKey: "test-secret",
22+
TokenDuration: 1 * time.Hour,
23+
Issuer: "test-issuer",
24+
}
25+
service := jwt.NewService(config, logger)
26+
ctx := context.Background()
27+
28+
// Create expected claims
29+
expectedClaims := &jwt.Claims{
30+
UserID: "remote-user-123",
31+
Roles: []string{"admin", "remote-user"},
32+
Scopes: []string{"read", "write"},
33+
Resources: []string{"resource1", "resource2"},
34+
}
35+
36+
// Set up a mock validator that succeeds
37+
mockValidator := NewSuccessfulMockValidator(expectedClaims)
38+
39+
// Set the mock validator as the remote validator
40+
service.WithRemoteValidator(jwt.RemoteConfig{})
41+
// Access the private field using reflection
42+
setRemoteValidator(t, service, mockValidator)
43+
44+
// Test with any token (the mock will ignore the actual token)
45+
claims, err := service.ValidateToken(ctx, "any-token")
46+
47+
// Verify the mock was called
48+
assert.True(t, mockValidator.Called)
49+
50+
// Verify the result
51+
assert.NoError(t, err)
52+
assert.NotNil(t, claims)
53+
assert.Equal(t, expectedClaims.UserID, claims.UserID)
54+
assert.Equal(t, expectedClaims.Roles, claims.Roles)
55+
assert.Equal(t, expectedClaims.Scopes, claims.Scopes)
56+
assert.Equal(t, expectedClaims.Resources, claims.Resources)
57+
}
58+
59+
// TestRemoteValidationFailure tests the case where remote validation fails with a non-NotImplemented error
60+
func TestRemoteValidationFailure(t *testing.T) {
61+
logger := zap.NewNop()
62+
config := jwt.Config{
63+
SecretKey: "test-secret",
64+
TokenDuration: 1 * time.Hour,
65+
Issuer: "test-issuer",
66+
}
67+
service := jwt.NewService(config, logger)
68+
ctx := context.Background()
69+
70+
// Generate a valid token for local validation
71+
userID := "user123"
72+
roles := []string{"admin", "user"}
73+
token, err := service.GenerateToken(ctx, userID, roles, []string{}, []string{})
74+
require.NoError(t, err)
75+
require.NotEmpty(t, token)
76+
77+
// Set up a mock validator that fails with a custom error
78+
customError := autherrors.WithMessage(autherrors.ErrInvalidToken, "remote validation failed")
79+
mockValidator := NewFailingMockValidator(customError)
80+
81+
// Set the mock validator as the remote validator
82+
service.WithRemoteValidator(jwt.RemoteConfig{})
83+
// Access the private field using reflection
84+
setRemoteValidator(t, service, mockValidator)
85+
86+
// Test validation - should fall back to local validation and succeed
87+
claims, err := service.ValidateToken(ctx, token)
88+
89+
// Verify the mock was called
90+
assert.True(t, mockValidator.Called)
91+
92+
// Verify the result - should succeed with local validation
93+
assert.NoError(t, err)
94+
assert.NotNil(t, claims)
95+
assert.Equal(t, userID, claims.UserID)
96+
assert.Equal(t, roles, claims.Roles)
97+
}
98+
99+
// TestRemoteValidationNotImplemented tests the case where remote validation returns ErrNotImplemented
100+
func TestRemoteValidationNotImplemented(t *testing.T) {
101+
logger := zap.NewNop()
102+
config := jwt.Config{
103+
SecretKey: "test-secret",
104+
TokenDuration: 1 * time.Hour,
105+
Issuer: "test-issuer",
106+
}
107+
service := jwt.NewService(config, logger)
108+
ctx := context.Background()
109+
110+
// Generate a valid token for local validation
111+
userID := "user123"
112+
roles := []string{"admin", "user"}
113+
token, err := service.GenerateToken(ctx, userID, roles, []string{}, []string{})
114+
require.NoError(t, err)
115+
require.NotEmpty(t, token)
116+
117+
// Set up a mock validator that fails with ErrNotImplemented
118+
mockValidator := NewFailingMockValidator(autherrors.ErrNotImplemented)
119+
120+
// Set the mock validator as the remote validator
121+
service.WithRemoteValidator(jwt.RemoteConfig{})
122+
// Access the private field using reflection
123+
setRemoteValidator(t, service, mockValidator)
124+
125+
// Test validation - should fall back to local validation and succeed
126+
claims, err := service.ValidateToken(ctx, token)
127+
128+
// Verify the mock was called
129+
assert.True(t, mockValidator.Called)
130+
131+
// Verify the result - should succeed with local validation
132+
assert.NoError(t, err)
133+
assert.NotNil(t, claims)
134+
assert.Equal(t, userID, claims.UserID)
135+
assert.Equal(t, roles, claims.Roles)
136+
}
137+
138+
// Helper function to set the remote validator for testing
139+
func setRemoteValidator(t *testing.T, service *jwt.Service, validator jwt.TokenValidator) {
140+
// Use the exported method to set the remote validator for testing
141+
service.SetRemoteValidatorForTesting(validator)
142+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) 2025 A Bit of Help, Inc.
2+
3+
package middleware
4+
5+
import (
6+
"testing"
7+
8+
"github.com/abitofhelp/servicelib/auth/jwt"
9+
"github.com/abitofhelp/servicelib/auth/oidc"
10+
"github.com/stretchr/testify/assert"
11+
"go.uber.org/zap"
12+
)
13+
14+
// TestNewMiddleware tests the NewMiddleware function
15+
func TestNewMiddleware(t *testing.T) {
16+
// Create a mock JWT service
17+
jwtService := &jwt.Service{}
18+
19+
// Create a config
20+
config := Config{
21+
SkipPaths: []string{"/health", "/metrics"},
22+
RequireAuth: true,
23+
}
24+
25+
// Test with logger
26+
logger := zap.NewNop()
27+
middleware := NewMiddleware(jwtService, config, logger)
28+
29+
// Verify the middleware was created correctly
30+
assert.NotNil(t, middleware)
31+
assert.Equal(t, config, middleware.config)
32+
assert.Equal(t, jwtService, middleware.jwtService)
33+
assert.Nil(t, middleware.oidcService)
34+
35+
// Test with nil logger (should use NopLogger)
36+
middleware = NewMiddleware(jwtService, config, nil)
37+
assert.NotNil(t, middleware)
38+
}
39+
40+
// TestNewMiddlewareWithOIDC tests the NewMiddlewareWithOIDC function
41+
func TestNewMiddlewareWithOIDC(t *testing.T) {
42+
// Create mock services
43+
jwtService := &jwt.Service{}
44+
oidcService := &oidc.Service{}
45+
46+
// Create a config
47+
config := Config{
48+
SkipPaths: []string{"/health", "/metrics"},
49+
RequireAuth: true,
50+
}
51+
52+
// Test with logger
53+
logger := zap.NewNop()
54+
middleware := NewMiddlewareWithOIDC(jwtService, oidcService, config, logger)
55+
56+
// Verify the middleware was created correctly
57+
assert.NotNil(t, middleware)
58+
assert.Equal(t, config, middleware.config)
59+
assert.Equal(t, jwtService, middleware.jwtService)
60+
assert.Equal(t, oidcService, middleware.oidcService)
61+
62+
// Test with nil logger (should use NopLogger)
63+
middleware = NewMiddlewareWithOIDC(jwtService, oidcService, config, nil)
64+
assert.NotNil(t, middleware)
65+
}

0 commit comments

Comments
 (0)