Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ dependencies {
// Yet we still need to add it manually. Grrr. TBC.
implementation "io.swagger.core.v3:swagger-annotations:2.2.42"
implementation("org.owasp.encoder:encoder:1.4.0")
implementation 'org.awaitility:awaitility:4.2.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
import uk.gov.hmcts.cp.subscription.services.ClockService;
import uk.gov.hmcts.cp.subscription.services.exceptions.MaterialMetadataNotReadyException;
import uk.gov.hmcts.cp.subscription.services.exceptions.CallbackUrlDeliveryException;

import java.time.Clock;
import java.util.Map;

@Configuration
public class AppConfig {
Expand All @@ -29,12 +25,4 @@ public RestClient restClient() {
public ClockService clockService() {
return new ClockService(Clock.systemDefaultZone());
}

@Bean
public RetryTemplate retryTemplate() {
return RetryTemplateConfig.retryConfig().toRetryTemplate( Map.of(
MaterialMetadataNotReadyException.class, true,
CallbackUrlDeliveryException.class, true
));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.core.ConditionTimeoutException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand Down Expand Up @@ -79,4 +80,12 @@ public ResponseEntity<String> handleUnsupportedOperation(final UnsupportedOperat
.status(HttpStatus.NOT_IMPLEMENTED)
.body("Unsupported");
}

@ExceptionHandler(ConditionTimeoutException.class)
public ResponseEntity<String> handleConditionTimeout(final ConditionTimeoutException ex) {
log.error("Material metadata timed out: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.GATEWAY_TIMEOUT)
.body("Material metadata not ready");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import uk.gov.hmcts.cp.openapi.model.PcrEventPayload;
import uk.gov.hmcts.cp.openapi.model.EventType;
import uk.gov.hmcts.cp.openapi.model.PcrEventPayloadDefendant;

import java.time.Instant;
import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PcrOutboundPayload {
private PcrEventPayload pcrEventPayload;
private String documentId;

private UUID eventId;
private EventType eventType;
private UUID documentId;
private Instant timestamp;
private PcrEventPayloadDefendant defendant;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ private void deliverToSubscriber(final Subscriber subscriber, final UUID documen
log.info("Subscriber {} notified via callbackUrl {} for documentId {}", subscriber.getId(), callbackURL, documentId);
}

private PcrOutboundPayload createPcrOutboundPayload(final PcrEventPayload pcrEventPayload, final UUID documentId) {
private PcrOutboundPayload createPcrOutboundPayload(final PcrEventPayload pcrEventPayload,
final UUID documentId) {
return PcrOutboundPayload.builder()
.pcrEventPayload(pcrEventPayload)
.documentId(documentId.toString())
.eventId(pcrEventPayload.getEventId())
.eventType(pcrEventPayload.getEventType())
.documentId(documentId)
.timestamp(pcrEventPayload.getTimestamp())
//TBD - post to api changes
.defendant(pcrEventPayload.getDefendant())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,63 @@
package uk.gov.hmcts.cp.subscription.services;

import lombok.RequiredArgsConstructor;
import static java.time.Duration.ofSeconds;
import static org.awaitility.Awaitility.await;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import uk.gov.hmcts.cp.material.openapi.api.MaterialApi;
import uk.gov.hmcts.cp.material.openapi.model.MaterialMetadata;
import uk.gov.hmcts.cp.openapi.model.PcrEventPayload;
import uk.gov.hmcts.cp.subscription.model.EntityEventType;
import uk.gov.hmcts.cp.subscription.services.exceptions.MaterialMetadataNotReadyException;

import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationService {

private final MaterialApi materialApi;
private final DocumentService documentService;
@Qualifier("retryTemplate")
private final RetryTemplate materialRetryTemplate;
private final Duration waitTimeout;
private final Duration pollInterval;

@Autowired
public NotificationService(final MaterialApi materialApi,
final DocumentService documentService) {
this(materialApi, documentService, ofSeconds(30), ofSeconds(5));
}

public NotificationService(final MaterialApi materialApi,
final DocumentService documentService,
final Duration waitTimeout,
final Duration pollInterval) {
this.materialApi = materialApi;
this.documentService = documentService;
this.waitTimeout = waitTimeout;
this.pollInterval = pollInterval;
}

public void processInboundEvent(final PcrEventPayload pcrEventPayload) {
final MaterialMetadata materialMetadata = materialRetryTemplate.execute(context ->
waitForMaterialMetadata(pcrEventPayload.getMaterialId()));
final MaterialMetadata materialMetadata = waitForMaterialMetadata(pcrEventPayload.getMaterialId());

final EntityEventType eventType = EntityEventType.valueOf(pcrEventPayload.getEventType().name());
documentService.saveDocumentMapping(materialMetadata.getMaterialId(), eventType);
}

private MaterialMetadata waitForMaterialMetadata(final UUID materialId) {
final MaterialMetadata response = materialApi.getMaterialMetadataByMaterialId(materialId);
if (response == null) {
throw new MaterialMetadataNotReadyException("PCR - Material metadata not ready for materialId: " + materialId);
}
return response;
final AtomicReference<MaterialMetadata> materialResponse = new AtomicReference<>();
await()
.atMost(waitTimeout)
.pollInterval(pollInterval)
.until(() -> {
final MaterialMetadata response = materialApi.getMaterialMetadataByMaterialId(materialId);
materialResponse.set(response);
return response != null;
});
return materialResponse.get();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package uk.gov.hmcts.cp.subscription.controllers;

import jakarta.persistence.EntityNotFoundException;
import org.awaitility.core.ConditionTimeoutException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
Expand Down Expand Up @@ -58,7 +60,36 @@ void generic_exception_should_handle_ok() {
assertErrorFields(response, INTERNAL_SERVER_ERROR, "message");
}

// TODO - Add tests for the other exception handlers ... if we decide that we really do need 10
@Test
void unsupported_media_type_should_handle_ok() {
HttpMediaTypeNotSupportedException e =
new HttpMediaTypeNotSupportedException("application/xml");

ResponseEntity<String> response =
globalExceptionHandler.handleHttpMediaTypeNotSupportedException(e);

assertErrorFields(response, HttpStatus.UNSUPPORTED_MEDIA_TYPE, e.getMessage());
}

@Test
void unsupported_operation_should_handle_ok() {
UnsupportedOperationException e = new UnsupportedOperationException("not implemented");

ResponseEntity<String> response =
globalExceptionHandler.handleUnsupportedOperation(e);

assertErrorFields(response, HttpStatus.NOT_IMPLEMENTED, "Unsupported");
}

@Test
void condition_timeout_should_handle_ok() {
ConditionTimeoutException e = new ConditionTimeoutException("timed out");

ResponseEntity<String> response =
globalExceptionHandler.handleConditionTimeout(e);

assertErrorFields(response, HttpStatus.GATEWAY_TIMEOUT, "Material metadata not ready");
}

private void assertErrorFields(ResponseEntity<String> errorResponse, HttpStatusCode httpStatusCode, String message) {
assertThat(errorResponse.getStatusCode()).isEqualTo(httpStatusCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package uk.gov.hmcts.cp.subscription.services;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import uk.gov.hmcts.cp.openapi.model.EventType;
import uk.gov.hmcts.cp.openapi.model.PcrEventPayload;
import uk.gov.hmcts.cp.openapi.model.PcrEventPayloadDefendant;
import uk.gov.hmcts.cp.subscription.entities.ClientSubscriptionEntity;
import uk.gov.hmcts.cp.subscription.mappers.SubscriberMapper;
import uk.gov.hmcts.cp.subscription.model.EntityEventType;
import uk.gov.hmcts.cp.subscription.model.PcrOutboundPayload;
import uk.gov.hmcts.cp.subscription.model.Subscriber;
import uk.gov.hmcts.cp.subscription.repositories.SubscriptionRepository;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

import static java.util.UUID.randomUUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class CallbackDeliveryServiceTest {

@Mock
private SubscriptionRepository subscriptionRepository;

@Mock
private SubscriberMapper subscriberMapper;

@Mock
private CallbackService callbackService;

@Mock
private PcrEventPayload pcrEventPayload;

@Mock
private PcrEventPayloadDefendant defendant;

@InjectMocks
private CallbackDeliveryService callbackDeliveryService;

private static final UUID DOCUMENT_ID = randomUUID();

@Test
void processPcrEvent_custodialResult_shouldThrowUnsupportedOperation() {
when(pcrEventPayload.getEventType()).thenReturn(EventType.CUSTODIAL_RESULT);

assertThrows(UnsupportedOperationException.class,
() -> callbackDeliveryService.processPcrEvent(pcrEventPayload, DOCUMENT_ID));

verifyNoInteractions(subscriptionRepository, subscriberMapper, callbackService);
}

@Test
void processPcrEvent_shouldMapOutboundPayloadFieldsCorrectly() {
UUID eventId = randomUUID();
Instant timestamp = Instant.now();

when(pcrEventPayload.getEventType()).thenReturn(EventType.PRISON_COURT_REGISTER_GENERATED);
when(pcrEventPayload.getEventId()).thenReturn(eventId);
when(pcrEventPayload.getTimestamp()).thenReturn(timestamp);
when(pcrEventPayload.getDefendant()).thenReturn(defendant);

ClientSubscriptionEntity entity = ClientSubscriptionEntity.builder()
.id(randomUUID())
.notificationEndpoint("https://callback.example.com")
.eventTypes(List.of(EntityEventType.PRISON_COURT_REGISTER_GENERATED))
.build();

when(subscriptionRepository.findByEventType(EntityEventType.PRISON_COURT_REGISTER_GENERATED.name()))
.thenReturn(List.of(entity));

Subscriber subscriber = Subscriber.builder()
.id(entity.getId())
.notificationEndpoint(entity.getNotificationEndpoint())
.eventTypes(entity.getEventTypes())
.clientId(randomUUID())
.build();

when(subscriberMapper.toSubscriber(entity)).thenReturn(subscriber);

ArgumentCaptor<PcrOutboundPayload> payloadCaptor = ArgumentCaptor.forClass(PcrOutboundPayload.class);
ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);

callbackDeliveryService.processPcrEvent(pcrEventPayload, DOCUMENT_ID);

verify(callbackService).sendToSubscriber(urlCaptor.capture(), payloadCaptor.capture());

assertThat(urlCaptor.getValue()).isEqualTo("https://callback.example.com");

PcrOutboundPayload outbound = payloadCaptor.getValue();
assertThat(outbound.getEventId()).isEqualTo(eventId);
assertThat(outbound.getEventType()).isEqualTo(EventType.PRISON_COURT_REGISTER_GENERATED);
assertThat(outbound.getDocumentId()).isEqualTo(DOCUMENT_ID);
assertThat(outbound.getTimestamp()).isEqualTo(timestamp);
assertThat(outbound.getDefendant()).isEqualTo(defendant);
}
}
Loading
Loading