Skip to content

Commit b466ac9

Browse files
committed
Improve brittle tests relying on implementation details
They require the implementation to call a specific method on the RestOperations interface. Change to use a MockWebServer to allow refactoring without the tests breaking. Signed-off-by: Andreas Svanberg <[email protected]>
1 parent 446c7a0 commit b466ac9

File tree

1 file changed

+136
-122
lines changed

1 file changed

+136
-122
lines changed

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/NimbusOpaqueTokenIntrospectorTests.java

Lines changed: 136 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@
2020
import java.time.Instant;
2121
import java.util.Arrays;
2222
import java.util.Base64;
23-
import java.util.HashMap;
24-
import java.util.Map;
23+
import java.util.List;
2524
import java.util.Optional;
2625

27-
import net.minidev.json.JSONArray;
28-
import net.minidev.json.JSONObject;
26+
import okhttp3.HttpUrl;
2927
import okhttp3.mockwebserver.Dispatcher;
3028
import okhttp3.mockwebserver.MockResponse;
3129
import okhttp3.mockwebserver.MockWebServer;
@@ -36,6 +34,7 @@
3634

3735
import org.springframework.core.convert.converter.Converter;
3836
import org.springframework.http.HttpHeaders;
37+
import org.springframework.http.HttpMethod;
3938
import org.springframework.http.HttpStatus;
4039
import org.springframework.http.MediaType;
4140
import org.springframework.http.RequestEntity;
@@ -47,9 +46,6 @@
4746
import static org.assertj.core.api.Assertions.assertThat;
4847
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4948
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
50-
import static org.assertj.core.api.Assumptions.assumeThat;
51-
import static org.mockito.ArgumentMatchers.any;
52-
import static org.mockito.ArgumentMatchers.eq;
5349
import static org.mockito.BDDMockito.given;
5450
import static org.mockito.Mockito.mock;
5551
import static org.mockito.Mockito.verify;
@@ -122,16 +118,6 @@ public class NimbusOpaqueTokenIntrospectorTests {
122118
+ " }";
123119
// @formatter:on
124120

125-
private static final ResponseEntity<String> ACTIVE = response(ACTIVE_RESPONSE);
126-
127-
private static final ResponseEntity<String> INACTIVE = response(INACTIVE_RESPONSE);
128-
129-
private static final ResponseEntity<String> INVALID = response(INVALID_RESPONSE);
130-
131-
private static final ResponseEntity<String> MALFORMED_ISSUER = response(MALFORMED_ISSUER_RESPONSE);
132-
133-
private static final ResponseEntity<String> MALFORMED_SCOPE = response(MALFORMED_SCOPE_RESPONSE);
134-
135121
@Test
136122
public void introspectWhenActiveTokenThenOk() throws Exception {
137123
try (MockWebServer server = new MockWebServer()) {
@@ -170,96 +156,116 @@ public void introspectWhenBadClientCredentialsThenError() throws IOException {
170156
}
171157

172158
@Test
173-
public void introspectWhenInactiveTokenThenInvalidToken() {
174-
RestOperations restOperations = mock(RestOperations.class);
175-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
176-
restOperations);
177-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(INACTIVE);
178-
// @formatter:off
179-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
180-
.isThrownBy(() -> introspectionClient.introspect("token"))
181-
.withMessage("Provided token isn't active");
182-
// @formatter:on
159+
public void introspectWhenInactiveTokenThenInvalidToken() throws IOException {
160+
try (MockWebServer server = new MockWebServer()) {
161+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, INACTIVE_RESPONSE));
162+
String introspectUri = server.url("/introspect").toString();
163+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
164+
CLIENT_SECRET);
165+
// @formatter:off
166+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
167+
.isThrownBy(() -> introspectionClient.introspect("token"))
168+
.withMessage("Provided token isn't active");
169+
// @formatter:on
170+
}
183171
}
184172

185173
@Test
186-
public void introspectWhenActiveTokenThenParsesValuesInResponse() {
187-
Map<String, Object> introspectedValues = new HashMap<>();
188-
introspectedValues.put(OAuth2TokenIntrospectionClaimNames.ACTIVE, true);
189-
introspectedValues.put(OAuth2TokenIntrospectionClaimNames.AUD, Arrays.asList("aud"));
190-
introspectedValues.put(OAuth2TokenIntrospectionClaimNames.NBF, 29348723984L);
191-
RestOperations restOperations = mock(RestOperations.class);
192-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
193-
restOperations);
194-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
195-
.willReturn(response(new JSONObject(introspectedValues).toJSONString()));
196-
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token");
197-
// @formatter:off
198-
assertThat(authority.getAttributes())
199-
.isNotNull()
200-
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
201-
.containsEntry(OAuth2TokenIntrospectionClaimNames.AUD, Arrays.asList("aud"))
202-
.containsEntry(OAuth2TokenIntrospectionClaimNames.NBF, Instant.ofEpochSecond(29348723984L))
203-
.doesNotContainKey(OAuth2TokenIntrospectionClaimNames.CLIENT_ID)
204-
.doesNotContainKey(OAuth2TokenIntrospectionClaimNames.SCOPE);
205-
// @formatter:on
174+
public void introspectWhenActiveTokenThenParsesValuesInResponse() throws IOException {
175+
try (MockWebServer server = new MockWebServer()) {
176+
String introspectedValues = """
177+
{
178+
"active": true,
179+
"aud": "aud",
180+
"nbf": 29348723984
181+
}
182+
""";
183+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, introspectedValues));
184+
String introspectUri = server.url("/introspect").toString();
185+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
186+
CLIENT_SECRET);
187+
OAuth2AuthenticatedPrincipal authority = introspectionClient.introspect("token");
188+
// @formatter:off
189+
assertThat(authority.getAttributes())
190+
.isNotNull()
191+
.containsEntry(OAuth2TokenIntrospectionClaimNames.ACTIVE, true)
192+
.containsEntry(OAuth2TokenIntrospectionClaimNames.AUD, Arrays.asList("aud"))
193+
.containsEntry(OAuth2TokenIntrospectionClaimNames.NBF, Instant.ofEpochSecond(29348723984L))
194+
.doesNotContainKey(OAuth2TokenIntrospectionClaimNames.CLIENT_ID)
195+
.doesNotContainKey(OAuth2TokenIntrospectionClaimNames.SCOPE);
196+
// @formatter:on
197+
}
206198
}
207199

208200
@Test
209-
public void introspectWhenIntrospectionEndpointThrowsExceptionThenInvalidToken() {
210-
RestOperations restOperations = mock(RestOperations.class);
211-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
212-
restOperations);
213-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
214-
.willThrow(new IllegalStateException("server was unresponsive"));
215-
// @formatter:off
216-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
217-
.isThrownBy(() -> introspectionClient.introspect("token"))
218-
.withMessage("server was unresponsive");
219-
// @formatter:on
201+
public void introspectWhenIntrospectionEndpointThrowsExceptionThenInvalidToken() throws IOException {
202+
try (MockWebServer server = new MockWebServer()) {
203+
server.setDispatcher(new Dispatcher() {
204+
@Override
205+
public MockResponse dispatch(final RecordedRequest request) {
206+
return new MockResponse().setResponseCode(500);
207+
}
208+
});
209+
String introspectUri = server.url("/introspect").toString();
210+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
211+
CLIENT_SECRET);
212+
// @formatter:off
213+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
214+
.isThrownBy(() -> introspectionClient.introspect("token"))
215+
.withMessageContaining("500");
216+
// @formatter:on
217+
}
220218
}
221219

222220
@Test
223-
public void introspectWhenIntrospectionEndpointReturnsMalformedResponseThenInvalidToken() {
224-
RestOperations restOperations = mock(RestOperations.class);
225-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
226-
restOperations);
227-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(response("malformed"));
228-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
229-
.isThrownBy(() -> introspectionClient.introspect("token"));
221+
public void introspectWhenIntrospectionEndpointReturnsMalformedResponseThenInvalidToken() throws IOException {
222+
try (MockWebServer server = new MockWebServer()) {
223+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, "malformed"));
224+
String introspectUri = server.url("/introspect").toString();
225+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
226+
CLIENT_SECRET);
227+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
228+
.isThrownBy(() -> introspectionClient.introspect("token"));
229+
}
230230
}
231231

232232
@Test
233-
public void introspectWhenIntrospectionTokenReturnsInvalidResponseThenInvalidToken() {
234-
RestOperations restOperations = mock(RestOperations.class);
235-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
236-
restOperations);
237-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(INVALID);
238-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
239-
.isThrownBy(() -> introspectionClient.introspect("token"));
233+
public void introspectWhenIntrospectionTokenReturnsInvalidResponseThenInvalidToken() throws IOException {
234+
try (MockWebServer server = new MockWebServer()) {
235+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, INVALID_RESPONSE));
236+
String introspectUri = server.url("/introspect").toString();
237+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
238+
CLIENT_SECRET);
239+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
240+
.isThrownBy(() -> introspectionClient.introspect("token"));
241+
}
240242
}
241243

242244
@Test
243-
public void introspectWhenIntrospectionTokenReturnsMalformedIssuerResponseThenInvalidToken() {
244-
RestOperations restOperations = mock(RestOperations.class);
245-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
246-
restOperations);
247-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(MALFORMED_ISSUER);
248-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
249-
.isThrownBy(() -> introspectionClient.introspect("token"));
245+
public void introspectWhenIntrospectionTokenReturnsMalformedIssuerResponseThenInvalidToken() throws IOException {
246+
try (MockWebServer server = new MockWebServer()) {
247+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, MALFORMED_ISSUER_RESPONSE));
248+
String introspectUri = server.url("/introspect").toString();
249+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
250+
CLIENT_SECRET);
251+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
252+
.isThrownBy(() -> introspectionClient.introspect("token"));
253+
}
250254
}
251255

252256
// gh-7563
253257
@Test
254-
public void introspectWhenIntrospectionTokenReturnsMalformedScopeThenEmptyAuthorities() {
255-
RestOperations restOperations = mock(RestOperations.class);
256-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
257-
restOperations);
258-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(MALFORMED_SCOPE);
259-
OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token");
260-
assertThat(principal.getAuthorities()).isEmpty();
261-
JSONArray scope = principal.getAttribute("scope");
262-
assertThat(scope).containsExactly("read", "write", "dolphin");
258+
public void introspectWhenIntrospectionTokenReturnsMalformedScopeThenEmptyAuthorities() throws IOException {
259+
try (MockWebServer server = new MockWebServer()) {
260+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, MALFORMED_SCOPE_RESPONSE));
261+
String introspectUri = server.url("/introspect").toString();
262+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
263+
CLIENT_SECRET);
264+
OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token");
265+
assertThat(principal.getAuthorities()).isEmpty();
266+
List<String> scope = principal.getAttribute("scope");
267+
assertThat(scope).containsExactly("read", "write", "dolphin");
268+
}
263269
}
264270

265271
@Test
@@ -297,50 +303,58 @@ public void setRequestEntityConverterWhenConverterIsNullThenExceptionIsThrown()
297303

298304
@SuppressWarnings("unchecked")
299305
@Test
300-
public void setRequestEntityConverterWhenNonNullConverterGivenThenConverterUsed() {
301-
RestOperations restOperations = mock(RestOperations.class);
302-
Converter<String, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
303-
RequestEntity requestEntity = mock(RequestEntity.class);
304-
String tokenToIntrospect = "some token";
305-
given(requestEntityConverter.convert(tokenToIntrospect)).willReturn(requestEntity);
306-
given(restOperations.exchange(requestEntity, String.class)).willReturn(ACTIVE);
307-
NimbusOpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
308-
restOperations);
309-
introspectionClient.setRequestEntityConverter(requestEntityConverter);
310-
introspectionClient.introspect(tokenToIntrospect);
311-
verify(requestEntityConverter).convert(tokenToIntrospect);
306+
public void setRequestEntityConverterWhenNonNullConverterGivenThenConverterUsed() throws IOException {
307+
try (MockWebServer server = new MockWebServer()) {
308+
server.setDispatcher(requiresAuth(CLIENT_ID, CLIENT_SECRET, ACTIVE_RESPONSE));
309+
HttpUrl introspectUri = server.url("/introspect");
310+
Converter<String, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
311+
RequestEntity requestEntity = new RequestEntity<>(HttpHeaders.EMPTY, HttpMethod.POST, introspectUri.uri());
312+
String tokenToIntrospect = "some token";
313+
given(requestEntityConverter.convert(tokenToIntrospect)).willReturn(requestEntity);
314+
NimbusOpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(
315+
introspectUri.toString(), CLIENT_ID, CLIENT_SECRET);
316+
introspectionClient.setRequestEntityConverter(requestEntityConverter);
317+
introspectionClient.introspect(tokenToIntrospect);
318+
verify(requestEntityConverter).convert(tokenToIntrospect);
319+
}
312320
}
313321

314322
@Test
315-
public void handleMissingContentType() {
316-
RestOperations restOperations = mock(RestOperations.class);
317-
ResponseEntity<String> stubResponse = ResponseEntity.ok(ACTIVE_RESPONSE);
318-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(stubResponse);
319-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
320-
restOperations);
321-
322-
// Protect against potential regressions where a default content type might be
323-
// added by default.
324-
assumeThat(stubResponse.getHeaders().getContentType()).isNull();
323+
public void handleMissingContentType() throws IOException {
324+
try (MockWebServer server = new MockWebServer()) {
325+
server.setDispatcher(new Dispatcher() {
326+
@Override
327+
public MockResponse dispatch(final RecordedRequest request) {
328+
return ok(ACTIVE_RESPONSE).removeHeader(HttpHeaders.CONTENT_TYPE);
329+
}
330+
});
331+
String introspectUri = server.url("/introspect").toString();
332+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
333+
CLIENT_SECRET);
325334

326-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
327-
.isThrownBy(() -> introspectionClient.introspect("sometokenhere"));
335+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
336+
.isThrownBy(() -> introspectionClient.introspect("sometokenhere"));
337+
}
328338
}
329339

330340
@ParameterizedTest(name = "{displayName} when Content-Type={0}")
331341
@ValueSource(strings = { MediaType.APPLICATION_CBOR_VALUE, MediaType.TEXT_MARKDOWN_VALUE,
332342
MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_OCTET_STREAM_VALUE })
333-
public void handleNonJsonContentType(String type) {
334-
RestOperations restOperations = mock(RestOperations.class);
335-
ResponseEntity<String> stubResponse = ResponseEntity.ok()
336-
.contentType(MediaType.parseMediaType(type))
337-
.body(ACTIVE_RESPONSE);
338-
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(stubResponse);
339-
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(INTROSPECTION_URL,
340-
restOperations);
343+
public void handleNonJsonContentType(String type) throws IOException {
344+
try (MockWebServer server = new MockWebServer()) {
345+
server.setDispatcher(new Dispatcher() {
346+
@Override
347+
public MockResponse dispatch(final RecordedRequest request) {
348+
return ok(ACTIVE_RESPONSE).setHeader(HttpHeaders.CONTENT_TYPE, type);
349+
}
350+
});
351+
String introspectUri = server.url("/introspect").toString();
352+
OpaqueTokenIntrospector introspectionClient = new NimbusOpaqueTokenIntrospector(introspectUri, CLIENT_ID,
353+
CLIENT_SECRET);
341354

342-
assertThatExceptionOfType(OAuth2IntrospectionException.class)
343-
.isThrownBy(() -> introspectionClient.introspect("sometokenhere"));
355+
assertThatExceptionOfType(OAuth2IntrospectionException.class)
356+
.isThrownBy(() -> introspectionClient.introspect("sometokenhere"));
357+
}
344358
}
345359

346360
private static ResponseEntity<String> response(String content) {

0 commit comments

Comments
 (0)