Skip to content

Commit a0a3161

Browse files
authored
Merge pull request #17 from hmcts/fix/fix-get-subscription
Fix/fix get subscription
2 parents 04dfd19 + 3de88af commit a0a3161

18 files changed

+255
-42
lines changed

docs/debugging.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Debugging
2+
3+
The command below will post to create a subscription and return a clientSubscriptionId which can then be queried with GET
4+
```
5+
export PORT=4550
6+
export HOST="localhost:$PORT"
7+
curl -XPOST http://$HOST/client-subscriptions \
8+
-H "Content-Type: application/json" \
9+
-d '{"eventTypes":["PCR","CUSTODIAL_RESULT"],"notificationEndpoint":{"webhookUrl":"https://my-callback-url"}}'
10+
```
11+
12+
... Creates and returns a subscription with a clientSubscriptionId
13+
ie.
14+
```
15+
{"clientSubscriptionId":"585fde62-44df-4b3d-ac38-5e1f4d7669e3",
16+
"createdAt":"2025-12-19T15:16:38.456506Z",
17+
"eventTypes":["CUSTODIAL_RESULT","PCR"],
18+
"notificationEndpoint":{"webhookUrl":"https://my-callback-url"},"updatedAt":"2025-12-19T15:16:38.456772Z"}
19+
```
20+
21+
Query
22+
```
23+
export SUBSCRIPTION_ID="585fde62-44df-4b3d-ac38-5e1f4d7669e3"
24+
curl http://$HOST/client-subscriptions/$SUBSCRIPTION_ID
25+
```

src/apiTest/java/uk/gov/hmcts/cp/subscription/http/ActuatorApiTest.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,6 @@ class ActuatorApiTest {
1515
private final String baseUrl = System.getProperty("app.baseUrl", "http://localhost:8082");
1616
private final RestTemplate http = new RestTemplate();
1717

18-
@Test
19-
void root_endpoint_should_be_ok() {
20-
final ResponseEntity<String> res = http.exchange(
21-
baseUrl + "/", HttpMethod.GET,
22-
new HttpEntity<>(new HttpHeaders()),
23-
String.class
24-
);
25-
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
26-
assertThat(res.getBody()).contains("DEPRECATED root endpoint");
27-
}
28-
2918
@Test
3019
void health_endpoint_should_be_up() {
3120
final ResponseEntity<String> res = http.exchange(
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package uk.gov.hmcts.cp.subscription.http;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.http.HttpEntity;
5+
import org.springframework.http.HttpHeaders;
6+
import org.springframework.http.HttpMethod;
7+
import org.springframework.http.HttpStatus;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.client.RestTemplate;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class RootApiTest {
14+
15+
private final String baseUrl = System.getProperty("app.baseUrl", "http://localhost:8082");
16+
private final RestTemplate http = new RestTemplate();
17+
18+
@Test
19+
void root_endpoint_should_be_ok() throws InterruptedException {
20+
final ResponseEntity<String> res = http.exchange(
21+
baseUrl + "/", HttpMethod.GET,
22+
new HttpEntity<>(new HttpHeaders()),
23+
String.class
24+
);
25+
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
26+
assertThat(res.getBody()).contains("DEPRECATED root endpoint");
27+
}
28+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package uk.gov.hmcts.cp.subscription.http;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.http.HttpEntity;
5+
import org.springframework.http.HttpHeaders;
6+
import org.springframework.http.HttpMethod;
7+
import org.springframework.http.HttpStatus;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.client.RestTemplate;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
/**
15+
* This is a bit verbose and not using sensible plugins to log, parse response etc
16+
* This is because these tests run inside the app context. But we dont want to bloat the app with configs or plugins
17+
* that are needed for api-test
18+
* We need to decide how to run api-tests against built docker file.
19+
* Probably best as a separate app with its own gradle build
20+
*/
21+
class SubscriptionApiTest {
22+
private final String baseUrl = System.getProperty("app.baseUrl", "http://localhost:8082");
23+
private final RestTemplate http = new RestTemplate();
24+
25+
@Test
26+
void round_trip_subscription_should_work_ok() throws InterruptedException {
27+
final String postUrl = String.format("%s/client-subscriptions", baseUrl);
28+
final HttpHeaders headers = new HttpHeaders();
29+
headers.setContentType(MediaType.APPLICATION_JSON);
30+
final String body = "{\"eventTypes\":[\"PCR\",\"CUSTODIAL_RESULT\"],\n" +
31+
"\"notificationEndpoint\":{\"webhookUrl\":\"https://my-callback-url\"}}";
32+
final ResponseEntity<String> postResult = http.exchange(
33+
postUrl,
34+
HttpMethod.POST,
35+
new HttpEntity<>(body, headers),
36+
String.class
37+
);
38+
assertThat(postResult.getStatusCode()).isEqualTo(HttpStatus.CREATED);
39+
assertThat(postResult.getBody()).contains("clientSubscriptionId");
40+
final String subscriptionId = postResult.getBody()
41+
.replaceAll(".*clientSubscriptionId\":\"", "")
42+
.replaceAll("\".*$", "");
43+
44+
final String getUrl = String.format("%s/client-subscriptions/%s", baseUrl, subscriptionId);
45+
final ResponseEntity<String> getResult = http.exchange(
46+
getUrl,
47+
HttpMethod.GET,
48+
new HttpEntity<>(new HttpHeaders()),
49+
String.class
50+
);
51+
assertThat(getResult.getStatusCode()).isEqualTo(HttpStatus.OK);
52+
assertThat(getResult.getBody()).contains("clientSubscriptionId\":\"" + subscriptionId);
53+
}
54+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package uk.gov.hmcts.cp.subscription.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import uk.gov.hmcts.cp.subscription.services.ClockService;
6+
7+
import java.time.Clock;
8+
9+
@Configuration
10+
public class AppConfig {
11+
12+
@Bean
13+
public ClockService clockService() {
14+
return new ClockService(Clock.systemDefaultZone());
15+
}
16+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package uk.gov.hmcts.cp.subscription.controllers;
2+
3+
import jakarta.persistence.EntityNotFoundException;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.annotation.ExceptionHandler;
8+
import org.springframework.web.bind.annotation.RestControllerAdvice;
9+
import org.springframework.web.client.HttpClientErrorException;
10+
11+
@RestControllerAdvice
12+
@Slf4j
13+
public class GlobalExceptionHandler {
14+
15+
private static final String NOT_FOUND_MESSAGE = "No row with the given identifier exists";
16+
17+
@ExceptionHandler(EntityNotFoundException.class)
18+
public ResponseEntity<String> handleNotFoundException(final EntityNotFoundException exception) {
19+
log.error("NotFoundException {}", exception.getMessage());
20+
return ResponseEntity
21+
.status(HttpStatus.NOT_FOUND)
22+
.body(NOT_FOUND_MESSAGE);
23+
}
24+
25+
@ExceptionHandler(HttpClientErrorException.class)
26+
public ResponseEntity<String> handleClientException(final HttpClientErrorException exception) {
27+
log.error("HttpClientErrorException {}", exception.getMessage());
28+
return ResponseEntity
29+
.status(exception.getStatusCode())
30+
.body(exception.getMessage());
31+
}
32+
33+
@ExceptionHandler(Exception.class)
34+
public ResponseEntity<String> handleUnknownException(final Exception exception) {
35+
log.error("Exception {}", exception.getMessage());
36+
return ResponseEntity
37+
.status(HttpStatus.INTERNAL_SERVER_ERROR)
38+
.body(exception.getMessage());
39+
}
40+
}

src/main/java/uk/gov/hmcts/cp/subscription/controllers/SubscriptionController.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.extern.slf4j.Slf4j;
55
import org.springframework.http.HttpStatus;
66
import org.springframework.http.ResponseEntity;
7+
import org.springframework.transaction.annotation.Transactional;
78
import org.springframework.web.bind.annotation.RestController;
89
import uk.gov.hmcts.cp.openapi.api.SubscriptionApi;
910
import uk.gov.hmcts.cp.openapi.model.ClientSubscription;
@@ -22,6 +23,7 @@ public class SubscriptionController implements SubscriptionApi {
2223
private static final String CLIENT_ID = "TODO";
2324

2425
@Override
26+
@Transactional
2527
public ResponseEntity<ClientSubscription> createClientSubscription(final ClientSubscriptionRequest request) {
2628
log.info("createClientSubscription clientId:{}", CLIENT_ID);
2729
final ClientSubscription response = subscriptionService.saveSubscription(request);
@@ -30,20 +32,23 @@ public ResponseEntity<ClientSubscription> createClientSubscription(final ClientS
3032
}
3133

3234
@Override
35+
@Transactional
3336
public ResponseEntity<ClientSubscription> updateClientSubscription(final UUID clientSubscriptionId, final ClientSubscriptionRequest request) {
3437
log.info("updateClientSubscription clientSubscriptionId:{} clientId:{}", clientSubscriptionId, CLIENT_ID);
3538
final ClientSubscription response = subscriptionService.updateSubscription(clientSubscriptionId, request);
3639
return new ResponseEntity<>(response, HttpStatus.OK);
3740
}
3841

3942
@Override
43+
@Transactional
4044
public ResponseEntity<ClientSubscription> getClientSubscription(final UUID clientSubscriptionId) {
4145
log.info("getClientSubscription clientSubscriptionId:{} clientId:{}", clientSubscriptionId, CLIENT_ID);
4246
final ClientSubscription response = subscriptionService.getSubscription(clientSubscriptionId);
4347
return new ResponseEntity<>(response, HttpStatus.OK);
4448
}
4549

4650
@Override
51+
@Transactional
4752
public ResponseEntity<Void> deleteClientSubscription(final UUID clientSubscriptionId) {
4853
log.info("deleteClientSubscription clientSubscriptionId:{} clientId:{}", clientSubscriptionId, CLIENT_ID);
4954
subscriptionService.deleteSubscription(clientSubscriptionId);

src/main/java/uk/gov/hmcts/cp/subscription/entities/ClientSubscriptionEntity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package uk.gov.hmcts.cp.subscription.entities;
22

33
import jakarta.persistence.Entity;
4+
import jakarta.persistence.EnumType;
5+
import jakarta.persistence.Enumerated;
46
import jakarta.persistence.GeneratedValue;
57
import jakarta.persistence.GenerationType;
68
import jakarta.persistence.Id;
@@ -28,6 +30,7 @@ public class ClientSubscriptionEntity {
2830
private UUID id;
2931

3032
private String notificationEndpoint;
33+
@Enumerated(EnumType.STRING)
3134
private List<EntityEventType> eventTypes;
3235
private OffsetDateTime createdAt;
3336
private OffsetDateTime updatedAt;

src/main/java/uk/gov/hmcts/cp/subscription/mappers/SubscriptionMapper.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package uk.gov.hmcts.cp.subscription.mappers;
22

3+
import org.mapstruct.Context;
34
import org.mapstruct.Mapper;
45
import org.mapstruct.Mapping;
56
import org.mapstruct.Named;
@@ -9,6 +10,7 @@
910
import uk.gov.hmcts.cp.openapi.model.NotificationEndpoint;
1011
import uk.gov.hmcts.cp.subscription.entities.ClientSubscriptionEntity;
1112
import uk.gov.hmcts.cp.subscription.model.EntityEventType;
13+
import uk.gov.hmcts.cp.subscription.services.ClockService;
1214

1315
import java.util.List;
1416

@@ -20,16 +22,16 @@ public interface SubscriptionMapper {
2022
@Mapping(target = "id", expression = "java(null)")
2123
@Mapping(source = "request.eventTypes", target = "eventTypes", qualifiedByName = "mapWithSortedEventTypes")
2224
@Mapping(source = "request.notificationEndpoint", target = "notificationEndpoint", qualifiedByName = "mapFromNotificationEndpoint")
23-
@Mapping(target = "createdAt", expression = "java(java.time.OffsetDateTime.now())")
24-
@Mapping(target = "updatedAt", expression = "java(java.time.OffsetDateTime.now())")
25-
ClientSubscriptionEntity mapCreateRequestToEntity(ClientSubscriptionRequest request);
25+
@Mapping(target = "createdAt", expression = "java(clockService.now())")
26+
@Mapping(target = "updatedAt", expression = "java(clockService.now())")
27+
ClientSubscriptionEntity mapCreateRequestToEntity(@Context ClockService clockService, ClientSubscriptionRequest request);
2628

2729
@Mapping(source = "existing.id", target = "id")
2830
@Mapping(source = "request.eventTypes", target = "eventTypes", qualifiedByName = "mapWithSortedEventTypes")
2931
@Mapping(source = "request.notificationEndpoint", target = "notificationEndpoint", qualifiedByName = "mapFromNotificationEndpoint")
3032
@Mapping(source = "existing.createdAt", target = "createdAt")
31-
@Mapping(expression = "java(java.time.OffsetDateTime.now())", target = "updatedAt")
32-
ClientSubscriptionEntity mapUpdateRequestToEntity(ClientSubscriptionEntity existing, ClientSubscriptionRequest request);
33+
@Mapping(expression = "java(clockService.now())", target = "updatedAt")
34+
ClientSubscriptionEntity mapUpdateRequestToEntity(@Context ClockService clockService, ClientSubscriptionEntity existing, ClientSubscriptionRequest request);
3335

3436
@Mapping(source = "id", target = "clientSubscriptionId")
3537
ClientSubscription mapEntityToResponse(ClientSubscriptionEntity entity);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package uk.gov.hmcts.cp.subscription.services;
2+
3+
import lombok.AllArgsConstructor;
4+
import org.springframework.stereotype.Service;
5+
6+
import java.time.Clock;
7+
import java.time.OffsetDateTime;
8+
import java.time.ZoneOffset;
9+
10+
@Service
11+
@AllArgsConstructor
12+
/**
13+
* We use a ClockService to expose the clock time in a simple method to allow mocking in Tests
14+
*/
15+
public class ClockService {
16+
17+
private Clock clock;
18+
19+
public OffsetDateTime now() {
20+
return clock.instant().atOffset(ZoneOffset.UTC);
21+
}
22+
}

0 commit comments

Comments
 (0)