|
16 | 16 |
|
17 | 17 | package org.springframework.security.oauth2.jwt; |
18 | 18 |
|
| 19 | +import java.time.Duration; |
19 | 20 | import java.util.Collections; |
20 | 21 | import java.util.List; |
| 22 | +import java.util.concurrent.TimeUnit; |
21 | 23 | import java.util.function.Supplier; |
22 | 24 |
|
23 | 25 | import com.nimbusds.jose.jwk.JWK; |
|
32 | 34 | import org.junit.jupiter.api.extension.ExtendWith; |
33 | 35 | import org.mockito.Mock; |
34 | 36 | import org.mockito.junit.jupiter.MockitoExtension; |
| 37 | +import reactor.core.publisher.Flux; |
35 | 38 | import reactor.core.publisher.Mono; |
| 39 | +import reactor.core.scheduler.Schedulers; |
36 | 40 |
|
37 | 41 | import org.springframework.web.reactive.function.client.WebClientResponseException; |
38 | 42 |
|
@@ -166,6 +170,43 @@ public void getWhenNoMatchAndKeyIdMatchThenEmpty() { |
166 | 170 | assertThat(this.source.get(this.selector).block()).isEmpty(); |
167 | 171 | } |
168 | 172 |
|
| 173 | + @Test |
| 174 | + public void getWhenConcurrentRequestsThenSingleFetch() { |
| 175 | + // given |
| 176 | + given(this.matcher.matches(any())).willReturn(true); |
| 177 | + int concurrentRequests = 10; |
| 178 | + for (int i = 0; i < concurrentRequests; i++) { |
| 179 | + this.server.enqueue(new MockResponse().setBody(this.keys).setBodyDelay(100, TimeUnit.MILLISECONDS)); |
| 180 | + } |
| 181 | + |
| 182 | + // when |
| 183 | + List<List<JWK>> results = Flux.range(0, concurrentRequests) |
| 184 | + .flatMap((i) -> this.source.get(this.selector).subscribeOn(Schedulers.parallel()), concurrentRequests) |
| 185 | + .collectList() |
| 186 | + .block(Duration.ofSeconds(5)); |
| 187 | + |
| 188 | + // then |
| 189 | + assertThat(results).hasSize(concurrentRequests); |
| 190 | + assertThat(this.server.getRequestCount()).isEqualTo(1); |
| 191 | + } |
| 192 | + |
| 193 | + @Test |
| 194 | + public void getWhenEmptyResponseThenNextCallSucceeds() { |
| 195 | + // given |
| 196 | + given(this.matcher.matches(any())).willReturn(true); |
| 197 | + this.source = new ReactiveRemoteJWKSource(Mono.fromSupplier(this.mockStringSupplier)); |
| 198 | + // first call: supplier returns null URL, causing empty Mono from jwkSetUrlProvider |
| 199 | + willReturn(null).given(this.mockStringSupplier).get(); |
| 200 | + |
| 201 | + // when: first call completes empty |
| 202 | + List<JWK> firstResult = this.source.get(this.selector).block(Duration.ofSeconds(5)); |
| 203 | + |
| 204 | + // then: inflight is cleared and second call can succeed |
| 205 | + willReturn(this.server.url("/").toString()).given(this.mockStringSupplier).get(); |
| 206 | + List<JWK> secondResult = this.source.get(this.selector).block(Duration.ofSeconds(5)); |
| 207 | + assertThat(secondResult).isNotEmpty(); |
| 208 | + } |
| 209 | + |
169 | 210 | @Test |
170 | 211 | public void getShouldRecoverAndReturnKeysAfterErrorCase() { |
171 | 212 | given(this.matcher.matches(any())).willReturn(true); |
|
0 commit comments