Skip to content

Commit 8558f4f

Browse files
devondragonclaude
andcommitted
feat: Add comprehensive unit tests for OAuth2/OIDC services and security utilities
- Created OAuth2UserTestDataBuilder and OidcUserTestDataBuilder test fixtures - Provides realistic test data for Google, Facebook OAuth2 providers - Supports Keycloak OIDC testing with proper claims and tokens - Follows builder pattern for flexible test data creation - Added DSOAuth2UserServiceTest with 15 tests - Tests Google and Facebook OAuth2 authentication flows - Covers user creation, updates, provider conflicts - Validates error handling for unsupported providers - Tests real business logic, not just mock interactions - Added DSOidcUserServiceTest with 14 tests - Tests Keycloak OIDC authentication flows - Validates OIDC-specific user extraction methods - Tests DSUserDetails integration with OIDC tokens - Covers provider conflict scenarios - Added UserUtilsTest with 29 tests - Comprehensive IP extraction from proxy headers - Tests security-critical header priority ordering - Validates URL building with various configurations - Tests edge cases and security considerations Increased test count from 193 to 251 (58 new tests) All tests passing with 100% success rate 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9137119 commit 8558f4f

5 files changed

Lines changed: 1587 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.digitalsanctuary.spring.user.fixtures;
2+
3+
import java.util.Collection;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import java.util.Set;
7+
import org.springframework.security.core.GrantedAuthority;
8+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
9+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
10+
import org.springframework.security.oauth2.core.user.OAuth2User;
11+
12+
/**
13+
* Test data builder for creating OAuth2User instances with realistic provider-specific attributes.
14+
* Supports Google and Facebook OAuth2 providers with accurate attribute mappings.
15+
*/
16+
public class OAuth2UserTestDataBuilder {
17+
18+
private Map<String, Object> attributes = new HashMap<>();
19+
private Set<GrantedAuthority> authorities = Set.of(new SimpleGrantedAuthority("ROLE_USER"));
20+
private String nameAttributeKey = "sub";
21+
22+
private OAuth2UserTestDataBuilder() {
23+
// Private constructor to enforce builder pattern
24+
}
25+
26+
/**
27+
* Creates a builder for Google OAuth2 user with standard Google attributes.
28+
*/
29+
public static OAuth2UserTestDataBuilder google() {
30+
OAuth2UserTestDataBuilder builder = new OAuth2UserTestDataBuilder();
31+
builder.nameAttributeKey = "sub";
32+
// Set default Google attributes
33+
builder.attributes.put("sub", "123456789");
34+
builder.attributes.put("email", "test.user@gmail.com");
35+
builder.attributes.put("email_verified", true);
36+
builder.attributes.put("given_name", "Test");
37+
builder.attributes.put("family_name", "User");
38+
builder.attributes.put("name", "Test User");
39+
builder.attributes.put("picture", "https://lh3.googleusercontent.com/a/test-picture");
40+
builder.attributes.put("locale", "en");
41+
return builder;
42+
}
43+
44+
/**
45+
* Creates a builder for Facebook OAuth2 user with standard Facebook attributes.
46+
*/
47+
public static OAuth2UserTestDataBuilder facebook() {
48+
OAuth2UserTestDataBuilder builder = new OAuth2UserTestDataBuilder();
49+
builder.nameAttributeKey = "id";
50+
// Set default Facebook attributes
51+
builder.attributes.put("id", "987654321");
52+
builder.attributes.put("email", "test.user@facebook.com");
53+
builder.attributes.put("name", "Test User");
54+
builder.attributes.put("first_name", "Test");
55+
builder.attributes.put("last_name", "User");
56+
return builder;
57+
}
58+
59+
/**
60+
* Creates a builder for an unsupported OAuth2 provider.
61+
*/
62+
public static OAuth2UserTestDataBuilder unsupported() {
63+
OAuth2UserTestDataBuilder builder = new OAuth2UserTestDataBuilder();
64+
builder.nameAttributeKey = "sub";
65+
builder.attributes.put("sub", "unknown-provider");
66+
builder.attributes.put("email", "test@unknown.com");
67+
return builder;
68+
}
69+
70+
/**
71+
* Creates a builder with minimal attributes (missing required fields).
72+
*/
73+
public static OAuth2UserTestDataBuilder minimal() {
74+
OAuth2UserTestDataBuilder builder = new OAuth2UserTestDataBuilder();
75+
builder.nameAttributeKey = "sub";
76+
builder.attributes.put("sub", "minimal-user");
77+
return builder;
78+
}
79+
80+
public OAuth2UserTestDataBuilder withEmail(String email) {
81+
this.attributes.put("email", email);
82+
return this;
83+
}
84+
85+
public OAuth2UserTestDataBuilder withFirstName(String firstName) {
86+
if (attributes.containsKey("given_name")) {
87+
// Google format
88+
this.attributes.put("given_name", firstName);
89+
} else {
90+
// Facebook format
91+
this.attributes.put("first_name", firstName);
92+
}
93+
return this;
94+
}
95+
96+
public OAuth2UserTestDataBuilder withLastName(String lastName) {
97+
if (attributes.containsKey("family_name")) {
98+
// Google format
99+
this.attributes.put("family_name", lastName);
100+
} else {
101+
// Facebook format
102+
this.attributes.put("last_name", lastName);
103+
}
104+
return this;
105+
}
106+
107+
public OAuth2UserTestDataBuilder withFullName(String fullName) {
108+
this.attributes.put("name", fullName);
109+
return this;
110+
}
111+
112+
public OAuth2UserTestDataBuilder withAttribute(String key, Object value) {
113+
this.attributes.put(key, value);
114+
return this;
115+
}
116+
117+
public OAuth2UserTestDataBuilder withoutAttribute(String key) {
118+
this.attributes.remove(key);
119+
return this;
120+
}
121+
122+
public OAuth2UserTestDataBuilder withAuthorities(Collection<? extends GrantedAuthority> authorities) {
123+
this.authorities = Set.copyOf(authorities);
124+
return this;
125+
}
126+
127+
/**
128+
* Builds the OAuth2User with the configured attributes.
129+
*/
130+
public OAuth2User build() {
131+
return new DefaultOAuth2User(authorities, attributes, nameAttributeKey);
132+
}
133+
134+
/**
135+
* Builds the OAuth2User and also returns the attributes map for verification.
136+
*/
137+
public OAuth2UserWithAttributes buildWithAttributes() {
138+
OAuth2User user = build();
139+
return new OAuth2UserWithAttributes(user, new HashMap<>(attributes));
140+
}
141+
142+
/**
143+
* Helper class to return both OAuth2User and its attributes for testing.
144+
*/
145+
public static class OAuth2UserWithAttributes {
146+
public final OAuth2User user;
147+
public final Map<String, Object> attributes;
148+
149+
public OAuth2UserWithAttributes(OAuth2User user, Map<String, Object> attributes) {
150+
this.user = user;
151+
this.attributes = attributes;
152+
}
153+
}
154+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package com.digitalsanctuary.spring.user.fixtures;
2+
3+
import java.time.Instant;
4+
import java.util.Collection;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.Set;
8+
import org.springframework.security.core.GrantedAuthority;
9+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
10+
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
11+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
12+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
13+
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
14+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
15+
16+
/**
17+
* Test data builder for creating OidcUser instances with realistic Keycloak-specific attributes.
18+
*/
19+
public class OidcUserTestDataBuilder {
20+
21+
private Map<String, Object> idTokenClaims = new HashMap<>();
22+
private Map<String, Object> userInfoClaims = new HashMap<>();
23+
private Set<GrantedAuthority> authorities = Set.of(new SimpleGrantedAuthority("ROLE_USER"));
24+
25+
private OidcUserTestDataBuilder() {
26+
// Private constructor to enforce builder pattern
27+
}
28+
29+
/**
30+
* Creates a builder for Keycloak OIDC user with standard Keycloak claims.
31+
*/
32+
public static OidcUserTestDataBuilder keycloak() {
33+
OidcUserTestDataBuilder builder = new OidcUserTestDataBuilder();
34+
35+
// Standard OIDC ID token claims
36+
builder.idTokenClaims.put(IdTokenClaimNames.SUB, "f:12345678-1234-1234-1234-123456789abc:testuser");
37+
builder.idTokenClaims.put(IdTokenClaimNames.ISS, "https://keycloak.example.com/realms/test-realm");
38+
builder.idTokenClaims.put(IdTokenClaimNames.AUD, "test-client");
39+
builder.idTokenClaims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600));
40+
builder.idTokenClaims.put(IdTokenClaimNames.IAT, Instant.now());
41+
builder.idTokenClaims.put(IdTokenClaimNames.AUTH_TIME, Instant.now().minusSeconds(60));
42+
builder.idTokenClaims.put(IdTokenClaimNames.NONCE, "test-nonce");
43+
builder.idTokenClaims.put("azp", "test-client");
44+
builder.idTokenClaims.put("session_state", "test-session-state");
45+
46+
// Standard UserInfo claims
47+
builder.userInfoClaims.put("sub", "f:12345678-1234-1234-1234-123456789abc:testuser");
48+
builder.userInfoClaims.put("email", "test.user@keycloak.com");
49+
builder.userInfoClaims.put("email_verified", true);
50+
builder.userInfoClaims.put("given_name", "Test");
51+
builder.userInfoClaims.put("family_name", "User");
52+
builder.userInfoClaims.put("name", "Test User");
53+
builder.userInfoClaims.put("preferred_username", "testuser");
54+
55+
return builder;
56+
}
57+
58+
/**
59+
* Creates a builder for an unsupported OIDC provider.
60+
*/
61+
public static OidcUserTestDataBuilder unsupported() {
62+
OidcUserTestDataBuilder builder = new OidcUserTestDataBuilder();
63+
64+
builder.idTokenClaims.put(IdTokenClaimNames.SUB, "unknown-provider-user");
65+
builder.idTokenClaims.put(IdTokenClaimNames.ISS, "https://unknown.provider.com");
66+
builder.idTokenClaims.put(IdTokenClaimNames.AUD, "unknown-client");
67+
builder.idTokenClaims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600));
68+
builder.idTokenClaims.put(IdTokenClaimNames.IAT, Instant.now());
69+
70+
builder.userInfoClaims.put("sub", "unknown-provider-user");
71+
builder.userInfoClaims.put("email", "test@unknown.com");
72+
73+
return builder;
74+
}
75+
76+
/**
77+
* Creates a builder with minimal claims (missing some standard fields).
78+
*/
79+
public static OidcUserTestDataBuilder minimal() {
80+
OidcUserTestDataBuilder builder = new OidcUserTestDataBuilder();
81+
82+
builder.idTokenClaims.put(IdTokenClaimNames.SUB, "minimal-user");
83+
builder.idTokenClaims.put(IdTokenClaimNames.ISS, "https://minimal.provider.com");
84+
builder.idTokenClaims.put(IdTokenClaimNames.AUD, "minimal-client");
85+
builder.idTokenClaims.put(IdTokenClaimNames.EXP, Instant.now().plusSeconds(3600));
86+
builder.idTokenClaims.put(IdTokenClaimNames.IAT, Instant.now());
87+
88+
// Minimal userInfo - missing names
89+
builder.userInfoClaims.put("sub", "minimal-user");
90+
91+
return builder;
92+
}
93+
94+
public OidcUserTestDataBuilder withEmail(String email) {
95+
this.userInfoClaims.put("email", email);
96+
return this;
97+
}
98+
99+
public OidcUserTestDataBuilder withGivenName(String givenName) {
100+
this.userInfoClaims.put("given_name", givenName);
101+
return this;
102+
}
103+
104+
public OidcUserTestDataBuilder withFamilyName(String familyName) {
105+
this.userInfoClaims.put("family_name", familyName);
106+
return this;
107+
}
108+
109+
public OidcUserTestDataBuilder withFullName(String fullName) {
110+
this.userInfoClaims.put("name", fullName);
111+
return this;
112+
}
113+
114+
public OidcUserTestDataBuilder withPreferredUsername(String username) {
115+
this.userInfoClaims.put("preferred_username", username);
116+
return this;
117+
}
118+
119+
public OidcUserTestDataBuilder withIdTokenClaim(String key, Object value) {
120+
this.idTokenClaims.put(key, value);
121+
return this;
122+
}
123+
124+
public OidcUserTestDataBuilder withUserInfoClaim(String key, Object value) {
125+
this.userInfoClaims.put(key, value);
126+
return this;
127+
}
128+
129+
public OidcUserTestDataBuilder withoutUserInfoClaim(String key) {
130+
this.userInfoClaims.remove(key);
131+
return this;
132+
}
133+
134+
public OidcUserTestDataBuilder withAuthorities(Collection<? extends GrantedAuthority> authorities) {
135+
this.authorities = Set.copyOf(authorities);
136+
return this;
137+
}
138+
139+
/**
140+
* Builds the OidcUser with the configured claims.
141+
*/
142+
public OidcUser build() {
143+
OidcIdToken idToken = new OidcIdToken("test-token-value",
144+
(Instant) idTokenClaims.get(IdTokenClaimNames.IAT),
145+
(Instant) idTokenClaims.get(IdTokenClaimNames.EXP),
146+
idTokenClaims);
147+
148+
OidcUserInfo userInfo = userInfoClaims.isEmpty() ? null : new OidcUserInfo(userInfoClaims);
149+
150+
return new DefaultOidcUser(authorities, idToken, userInfo, IdTokenClaimNames.SUB);
151+
}
152+
153+
/**
154+
* Builds the OidcUser and also returns the claims for verification.
155+
*/
156+
public OidcUserWithClaims buildWithClaims() {
157+
OidcUser user = build();
158+
return new OidcUserWithClaims(user, new HashMap<>(idTokenClaims), new HashMap<>(userInfoClaims));
159+
}
160+
161+
/**
162+
* Helper class to return OidcUser with its claims for testing.
163+
*/
164+
public static class OidcUserWithClaims {
165+
public final OidcUser user;
166+
public final Map<String, Object> idTokenClaims;
167+
public final Map<String, Object> userInfoClaims;
168+
169+
public OidcUserWithClaims(OidcUser user, Map<String, Object> idTokenClaims, Map<String, Object> userInfoClaims) {
170+
this.user = user;
171+
this.idTokenClaims = idTokenClaims;
172+
this.userInfoClaims = userInfoClaims;
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)