Skip to content

Commit b953143

Browse files
NIAD-3112 - Regenerate the Authorization token before each GPC request (#761)
* Regenerate the Authorization token before each GPC request That way the token is always "fresh". * [NIAD-3112] Minor adjustments. * [NIAD-3112] Add additional verifications to tests within GpcWebClientTest.java * [NIAD-3112] Modified verifications to be stricter in nature * [NIAD-3112] Update CHANGELOG.md to include fixes * [NIAD-3112] Added test to ensure tokens are present within Authorization header. * [NIAD-3112] Addressed PR comment https://github.com/NHSDigital/integration-adaptor-gp2gp/pull/758/files#r1639807041 and https://github.com/NHSDigital/integration-adaptor-gp2gp/pull/758/files#r1639804884 * [NIAD-3112] Tidied up logic. * [NIAD-3112] Tidied up logic. * [NIAD-3112] Improvements, renamed variable. * [NIAD-3112] Add comment. * [NIAD-3112] Enhance test readability, reduced clutter. * [NIAD-3112] Remove wildcard imports. --------- Co-authored-by: Adrian Clay <[email protected]>
1 parent 2e2d385 commit b953143

File tree

3 files changed

+65
-11
lines changed

3 files changed

+65
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
* The GPCC Adaptor JWT token is now refreshed with every request to prevent expiration issues during retries and ensure continuous, uninterrupted access; previously, we were seeing the JWT expire when a request failed and retried.
12+
913
## [2.0.3] - 2024-05-20
1014

1115
### Fixed

service/src/intTest/java/uk/nhs/adaptors/gp2gp/gpc/GpcWebClientTest.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
import static org.mockito.Mockito.doAnswer;
6+
import static org.mockito.Mockito.times;
7+
import static org.mockito.Mockito.verify;
58
import static org.mockito.Mockito.when;
69
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
710
import static org.springframework.http.HttpStatus.OK;
811

912
import java.io.IOException;
13+
import java.util.ArrayList;
14+
import java.util.Collection;
15+
import java.util.Objects;
1016
import java.util.concurrent.TimeoutException;
1117

1218
import org.junit.jupiter.api.AfterEach;
@@ -17,13 +23,15 @@
1723
import org.springframework.beans.factory.annotation.Autowired;
1824
import org.springframework.boot.test.context.SpringBootTest;
1925
import org.springframework.boot.test.mock.mockito.SpyBean;
26+
import org.springframework.http.HttpHeaders;
2027
import org.springframework.test.annotation.DirtiesContext;
2128
import org.springframework.test.context.junit.jupiter.SpringExtension;
2229

2330
import okhttp3.mockwebserver.MockResponse;
2431
import okhttp3.mockwebserver.MockWebServer;
2532
import okhttp3.mockwebserver.SocketPolicy;
2633
import uk.nhs.adaptors.gp2gp.common.exception.RetryLimitReachedException;
34+
import uk.nhs.adaptors.gp2gp.gpc.builder.GpcTokenBuilder;
2735
import uk.nhs.adaptors.gp2gp.gpc.configuration.GpcConfiguration;
2836
import uk.nhs.adaptors.gp2gp.gpc.exception.GpcServerErrorException;
2937
import uk.nhs.adaptors.gp2gp.testcontainers.MongoDBExtension;
@@ -54,6 +62,8 @@ public class GpcWebClientTest {
5462
private String baseUrl;
5563
@SpyBean
5664
private GpcConfiguration gpcConfiguration;
65+
@SpyBean
66+
private GpcTokenBuilder gpcTokenBuilder;
5767
@Autowired
5868
private GpcClient gpcWebClient;
5969

@@ -102,18 +112,17 @@ public void tearDown() throws IOException {
102112

103113
@Test
104114
public void When_GetDocumentRecord_With_HttpStatus200_Expect_NoException() {
105-
106115
mockWebServer.enqueue(STUB_OK);
107116

108117
var taskDefinition = buildDocumentTaskDefinition();
109118
var result = gpcWebClient.getDocumentRecord(taskDefinition);
110119

111120
assertThat(result).isEqualTo(TEST_BODY);
121+
verify(gpcTokenBuilder).buildToken(taskDefinition.getFromOdsCode());
112122
}
113123

114124
@Test
115125
public void When_GetDocumentRecord_With_HttpStatus5xx_Expect_RetryExceptionWithGpcServerErrorExceptionAsRootCause() {
116-
117126
for (int i = 0; i < FOUR; i++) {
118127
mockWebServer.enqueue(STUB_INTERNAL_SERVER_ERROR);
119128
}
@@ -128,11 +137,11 @@ public void When_GetDocumentRecord_With_HttpStatus5xx_Expect_RetryExceptionWithG
128137

129138

130139
assertThat(mockWebServer.getRequestCount()).isEqualTo(FOUR);
140+
verify(gpcTokenBuilder, times(FOUR)).buildToken(taskDefinition.getFromOdsCode());
131141
}
132142

133143
@Test
134144
public void When_GetDocumentRecord_With_HttpStatus5xxAndNoBody_Expect_AlternativeExceptionMessage() {
135-
136145
for (int i = 0; i < FOUR; i++) {
137146
mockWebServer.enqueue(STUB_INTERNAL_SERVER_ERROR_NO_BODY);
138147
}
@@ -147,11 +156,11 @@ public void When_GetDocumentRecord_With_HttpStatus5xxAndNoBody_Expect_Alternativ
147156

148157

149158
assertThat(mockWebServer.getRequestCount()).isEqualTo(FOUR);
159+
verify(gpcTokenBuilder, times(FOUR)).buildToken(taskDefinition.getFromOdsCode());
150160
}
151161

152162
@Test
153163
public void When_GetDocumentRecord_With_NoResponse_Expect_RetryExceptionWithTimeoutAsRootCause() {
154-
155164
for (int i = 0; i < FOUR; i++) {
156165
mockWebServer.enqueue(STUB_NO_RESPONSE);
157166
}
@@ -164,18 +173,19 @@ public void When_GetDocumentRecord_With_NoResponse_Expect_RetryExceptionWithTime
164173
.hasMessage("Retries exhausted: 3/3");
165174

166175
assertThat(mockWebServer.getRequestCount()).isEqualTo(FOUR);
176+
verify(gpcTokenBuilder, times(FOUR)).buildToken(taskDefinition.getFromOdsCode());
167177
}
168178

169179
@Test
170180
public void When_GetDocumentRecord_With_NoResponseBeforeHttpStatus200_Expect_RetryBeforeSuccess() {
171-
172181
mockWebServer.enqueue(STUB_NO_RESPONSE);
173182
mockWebServer.enqueue(STUB_OK);
174183

175184
var taskDefinition = buildDocumentTaskDefinition();
176185
var result = gpcWebClient.getDocumentRecord(taskDefinition);
177186

178187
assertThat(result).isEqualTo(TEST_BODY);
188+
verify(gpcTokenBuilder, times(2)).buildToken(taskDefinition.getFromOdsCode());
179189
}
180190

181191
@Test
@@ -187,11 +197,11 @@ public void When_GetStructuredRecord_With_HttpStatus200_Expect_NoException() {
187197
var result = gpcWebClient.getStructuredRecord(taskDefinition);
188198

189199
assertThat(result).isEqualTo(TEST_BODY);
200+
verify(gpcTokenBuilder).buildToken(taskDefinition.getFromOdsCode());
190201
}
191202

192203
@Test
193204
public void When_GetStructuredRecord_With_NoResponse_Expect_RetryExceptionWithTimeoutAsRootCause() {
194-
195205
for (int i = 0; i < FOUR; i++) {
196206
mockWebServer.enqueue(STUB_NO_RESPONSE);
197207
}
@@ -204,11 +214,11 @@ public void When_GetStructuredRecord_With_NoResponse_Expect_RetryExceptionWithTi
204214
.hasMessage("Retries exhausted: 3/3");
205215

206216
assertThat(mockWebServer.getRequestCount()).isEqualTo(FOUR);
217+
verify(gpcTokenBuilder, times(FOUR)).buildToken(taskDefinition.getFromOdsCode());
207218
}
208219

209220
@Test
210221
public void When_GetStructuredRecord_With_HttpStatus5xx_Expect_RetryWithGpcServerErrorExceptionAsRootCause() {
211-
212222
for (int i = 0; i < FOUR; i++) {
213223
mockWebServer.enqueue(STUB_INTERNAL_SERVER_ERROR);
214224
}
@@ -222,6 +232,34 @@ public void When_GetStructuredRecord_With_HttpStatus5xx_Expect_RetryWithGpcServe
222232
.hasRootCauseMessage("The following error occurred during GPC request: " + TEST_BODY);
223233

224234
assertThat(mockWebServer.getRequestCount()).isEqualTo(FOUR);
235+
verify(gpcTokenBuilder, times(FOUR)).buildToken(taskDefinition.getFromOdsCode());
236+
}
237+
238+
@Test
239+
void When_GetStructuredRecord_With_HttpStatus200_Expect_AuthorizationHeaderToBePresent() throws InterruptedException {
240+
// given
241+
final GetGpcStructuredTaskDefinition taskDefinition = getStructuredDefinition();
242+
final String fromOdsCode = taskDefinition.getFromOdsCode();
243+
final Collection<String> tokens = new ArrayList<>();
244+
245+
// when
246+
doAnswer(invocation -> {
247+
final String tokenFromSpy = (String) invocation.callRealMethod();
248+
tokens.add("Bearer %s".formatted(tokenFromSpy)); // Add initial token generated by gpcTokenBuilder.buildToken();
249+
return tokenFromSpy;
250+
}).when(gpcTokenBuilder).buildToken(fromOdsCode);
251+
252+
mockWebServer.enqueue(STUB_OK);
253+
gpcWebClient.getStructuredRecord(taskDefinition);
254+
tokens.add(Objects.requireNonNull(mockWebServer
255+
.takeRequest()
256+
.getHeader(HttpHeaders.AUTHORIZATION))); // Add token from Authorisation header (WebClient call).
257+
258+
// then
259+
final long distinctTokens = tokens.stream().distinct().count();
260+
verify(gpcTokenBuilder).buildToken(fromOdsCode);
261+
assertThat(tokens).hasSize(2).doesNotContainNull();
262+
assertThat(distinctTokens).isEqualTo(1);
225263
}
226264

227265
private GetGpcDocumentTaskDefinition buildDocumentTaskDefinition() {

service/src/main/java/uk/nhs/adaptors/gp2gp/gpc/builder/GpcRequestBuilder.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
import org.springframework.stereotype.Component;
1919
import org.springframework.web.reactive.function.BodyInserter;
2020
import org.springframework.web.reactive.function.BodyInserters;
21+
import org.springframework.web.reactive.function.client.ClientRequest;
22+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
2123
import org.springframework.web.reactive.function.client.WebClient;
2224
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
2325
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
26+
import reactor.core.publisher.Mono;
2427
import reactor.netty.http.client.HttpClient;
2528
import uk.nhs.adaptors.gp2gp.common.service.RequestBuilderService;
2629
import uk.nhs.adaptors.gp2gp.common.service.WebClientFilterService;
@@ -85,7 +88,7 @@ public RequestHeadersSpec<?> buildGetStructuredRecordRequest(
8588
GetGpcStructuredTaskDefinition structuredTaskDefinition, String gpcBaseUrl) {
8689
SslContext sslContext = requestBuilderService.buildSSLContext();
8790
HttpClient httpClient = buildHttpClient(sslContext);
88-
WebClient client = buildWebClient(httpClient, gpcBaseUrl);
91+
WebClient client = buildWebClient(httpClient, gpcBaseUrl, structuredTaskDefinition);
8992

9093
WebClient.RequestBodySpec uri = client
9194
.method(HttpMethod.POST)
@@ -101,7 +104,7 @@ public RequestHeadersSpec<?> buildGetStructuredRecordRequest(
101104
public RequestHeadersSpec<?> buildGetDocumentRecordRequest(GetGpcDocumentTaskDefinition documentTaskDefinition, String gpcBaseUrl) {
102105
SslContext sslContext = requestBuilderService.buildSSLContext();
103106
HttpClient httpClient = buildHttpClient(sslContext);
104-
WebClient client = buildWebClient(httpClient, gpcBaseUrl);
107+
WebClient client = buildWebClient(httpClient, gpcBaseUrl, documentTaskDefinition);
105108

106109
WebClient.RequestBodySpec uri = client
107110
.method(HttpMethod.GET)
@@ -115,14 +118,24 @@ private HttpClient buildHttpClient(SslContext sslContext) {
115118
.secure(t -> t.sslContext(sslContext));
116119
}
117120

118-
private WebClient buildWebClient(HttpClient httpClient, String baseUrl) {
121+
private WebClient buildWebClient(HttpClient httpClient, String baseUrl, TaskDefinition taskDefinition) {
119122
return WebClient
120123
.builder()
121124
.codecs(cfg -> cfg.defaultCodecs().maxInMemorySize(gpcConfiguration.getMaxRequestSize()))
122125
.exchangeStrategies(requestBuilderService.buildExchangeStrategies())
123126
.clientConnector(new ReactorClientHttpConnector(httpClient))
124127
.filters(filters -> WebClientFilterService
125128
.addWebClientFilters(filters, WebClientFilterService.RequestType.GPC, HttpStatus.OK, gpcClientConfig))
129+
.filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> Mono.defer(
130+
() -> {
131+
final ClientRequest filteredRequest = ClientRequest
132+
.from(clientRequest)
133+
.header(AUTHORIZATION, AUTHORIZATION_BEARER + gpcTokenBuilder.buildToken(taskDefinition.getFromOdsCode()))
134+
.build();
135+
136+
return Mono.just(filteredRequest);
137+
})
138+
))
126139
.baseUrl(baseUrl)
127140
.defaultUriVariables(Collections.singletonMap("url", baseUrl))
128141
.build();
@@ -133,7 +146,6 @@ private RequestBodySpec buildRequestWithHeaders(RequestBodySpec uri, TaskDefinit
133146
return uri.accept(MediaType.valueOf(FHIR_CONTENT_TYPE))
134147
.header(SSP_INTERACTION_ID, interactionId)
135148
.header(SSP_TRACE_ID, taskDefinition.getConversationId())
136-
.header(AUTHORIZATION, AUTHORIZATION_BEARER + gpcTokenBuilder.buildToken(taskDefinition.getFromOdsCode()))
137149
.header(CONTENT_TYPE, FHIR_CONTENT_TYPE);
138150
}
139151

0 commit comments

Comments
 (0)