diff --git a/.github/test/tc-agent/compose.yaml b/.github/test/tc-agent/compose.yaml index 6e912687..957a1d6c 100644 --- a/.github/test/tc-agent/compose.yaml +++ b/.github/test/tc-agent/compose.yaml @@ -16,7 +16,7 @@ services: depends_on: keystore: condition: service_healthy - gics: # CI_ONLY + gics: # CI_ONLY condition: service_healthy # CI_ONLY gpas: # CI_ONLY condition: service_healthy # CI_ONLY diff --git a/clinical-domain-agent/src/main/java/care/smith/fts/cda/impl/TCACohortSelector.java b/clinical-domain-agent/src/main/java/care/smith/fts/cda/impl/TCACohortSelector.java index 170d8e7a..058671a8 100644 --- a/clinical-domain-agent/src/main/java/care/smith/fts/cda/impl/TCACohortSelector.java +++ b/clinical-domain-agent/src/main/java/care/smith/fts/cda/impl/TCACohortSelector.java @@ -2,16 +2,16 @@ import static care.smith.fts.util.MediaTypes.APPLICATION_FHIR_JSON; import static care.smith.fts.util.RetryStrategies.defaultRetryStrategy; -import static care.smith.fts.util.error.FhirErrorResponseUtil.operationOutcomeWithIssue; import static java.util.Map.entry; import static java.util.Optional.ofNullable; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.MediaType.APPLICATION_JSON; import care.smith.fts.api.ConsentedPatient; import care.smith.fts.api.cda.CohortSelector; import care.smith.fts.util.ConsentedPatientExtractor; -import care.smith.fts.util.error.FhirErrorResponseUtil; import care.smith.fts.util.error.TransferProcessException; +import care.smith.fts.util.error.fhir.FhirException; import io.micrometer.core.instrument.MeterRegistry; import jakarta.validation.constraints.NotNull; import java.time.Duration; @@ -22,7 +22,6 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent; import org.hl7.fhir.r4.model.OperationOutcome; -import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; @@ -74,7 +73,7 @@ private Mono fetchBundle(String uri, List pids) { .headers(h -> h.setContentType(APPLICATION_JSON)) .headers(h -> h.setAccept(List.of(APPLICATION_FHIR_JSON))) .retrieve() - .onStatus(r -> r.equals(HttpStatus.BAD_REQUEST), TCACohortSelector::handleBadRequest) + .onStatus(r -> r.equals(BAD_REQUEST), TCACohortSelector::handleBadRequest) .bodyToMono(Bundle.class) .retryWhen(defaultRetryStrategy(meterRegistry, "fetchBundle")); } @@ -131,7 +130,8 @@ private static Mono handleBadRequest(ClientResponse r) { .onErrorResume( e -> { log.error("Failed to parse error response", e); - return Mono.just(operationOutcomeWithIssue(e)); + return Mono.just( + new FhirException(BAD_REQUEST, e.getMessage()).getOperationOutcome()); }) .flatMap( outcome -> diff --git a/clinical-domain-agent/src/test/java/care/smith/fts/cda/impl/TCACohortSelectorIT.java b/clinical-domain-agent/src/test/java/care/smith/fts/cda/impl/TCACohortSelectorIT.java index c456f332..31959c7d 100644 --- a/clinical-domain-agent/src/test/java/care/smith/fts/cda/impl/TCACohortSelectorIT.java +++ b/clinical-domain-agent/src/test/java/care/smith/fts/cda/impl/TCACohortSelectorIT.java @@ -4,7 +4,7 @@ import static care.smith.fts.test.MockServerUtil.fhirResponse; import static care.smith.fts.test.MockServerUtil.jsonResponse; import static care.smith.fts.util.FhirUtils.toBundle; -import static care.smith.fts.util.error.FhirErrorResponseUtil.operationOutcomeWithIssue; +import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.operationOutcomeWithIssue; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.post; diff --git a/trust-center-agent/src/main/java/care/smith/fts/tca/consent/FhirConsentedPatientsProvider.java b/trust-center-agent/src/main/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProvider.java similarity index 58% rename from trust-center-agent/src/main/java/care/smith/fts/tca/consent/FhirConsentedPatientsProvider.java rename to trust-center-agent/src/main/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProvider.java index c3260df0..d1722e2f 100644 --- a/trust-center-agent/src/main/java/care/smith/fts/tca/consent/FhirConsentedPatientsProvider.java +++ b/trust-center-agent/src/main/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProvider.java @@ -3,25 +3,33 @@ import static care.smith.fts.tca.consent.GicsFhirUtil.filterOuterBundle; import static care.smith.fts.util.MediaTypes.APPLICATION_FHIR_JSON; import static care.smith.fts.util.RetryStrategies.defaultRetryStrategy; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.MediaType.APPLICATION_JSON; -import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirException; +import care.smith.fts.util.error.fhir.FhirUnknownDomainException; +import care.smith.fts.util.error.fhir.NoFhirServerException; import care.smith.fts.util.tca.ConsentFetchAllRequest; import care.smith.fts.util.tca.ConsentFetchRequest; import care.smith.fts.util.tca.ConsentRequest; import io.micrometer.core.instrument.MeterRegistry; import java.util.List; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.hl7.fhir.r4.model.*; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; /** This class provides functionalities for handling FHIR consents using an HTTP client. */ @Slf4j -public class FhirConsentedPatientsProvider implements ConsentedPatientsProvider { +public class GicsFhirConsentedPatientsProvider implements ConsentedPatientsProvider { private final WebClient gicsClient; private final MeterRegistry meterRegistry; @@ -30,7 +38,7 @@ public class FhirConsentedPatientsProvider implements ConsentedPatientsProvider * * @param gicsClient the WebClient used for HTTP requests */ - public FhirConsentedPatientsProvider(WebClient gicsClient, MeterRegistry meterRegistry) { + public GicsFhirConsentedPatientsProvider(WebClient gicsClient, MeterRegistry meterRegistry) { this.gicsClient = gicsClient; this.meterRegistry = meterRegistry; } @@ -76,30 +84,48 @@ private Mono doFetch( .headers(h -> h.setContentType(APPLICATION_FHIR_JSON)) .headers(h -> h.setAccept(List.of(APPLICATION_FHIR_JSON, APPLICATION_JSON))) .retrieve() - .onStatus( - r -> r.equals(HttpStatus.NOT_FOUND), FhirConsentedPatientsProvider::handleGicsNotFound) + .onStatus(HttpStatusCode::is4xxClientError, this::handle4xxError) .bodyToMono(Bundle.class) .doOnNext(b -> log.trace("body(n: {})", b.getEntry().size())) .retryWhen(defaultRetryStrategy(meterRegistry, helper.requestName())) + .onErrorResume( + e -> { + if (e.getCause() instanceof WebClientRequestException) { + return Mono.error(new NoFhirServerException("No connection to gICS server")); + } else if (e.getCause() instanceof WebClientResponseException) { + return Mono.error(new FhirException(INTERNAL_SERVER_ERROR, e.getMessage())); + } else { + return Mono.error(e); + } + }) .doOnError(b -> log.error("Unable to fetch consent from gICS", b)) .map(outerBundle -> filterOuterBundle(req.policySystem(), req.policies(), outerBundle)) .map(bundle -> helper.processResponse(bundle, req, requestUrl, paging)); } - private static Mono handleGicsNotFound(ClientResponse r) { + private Mono handle4xxError(ClientResponse r) { log.trace("response headers: {}", r.headers().asHttpHeaders()); - return r.bodyToMono(OperationOutcome.class) - .doOnNext(re -> log.info("{}", re)) - .flatMap( - b -> { - log.info("issue: {}", b.getIssueFirstRep()); - var diagnostics = b.getIssueFirstRep().getDiagnostics(); - log.error(diagnostics); - if (diagnostics != null && diagnostics.startsWith("No consents found for domain")) { - return Mono.error(new UnknownDomainException(diagnostics)); - } else { - return Mono.error(new IllegalArgumentException()); - } - }); + if (Set.of(400, 401, 404, 422).contains(r.statusCode().value())) { + log.debug("Status code: {}", r.statusCode().value()); + return r.bodyToMono(OperationOutcome.class) + .onErrorResume( + e -> { + log.error("Cannot parse OperationOutcome expected from gICS", e); + return Mono.error( + new NoFhirServerException("Cannot parse OperationOutcome received from gICS")); + }) + .flatMap( + b -> { + var diagnostics = b.getIssueFirstRep().getDiagnostics(); + log.error(diagnostics); + if (r.statusCode() == NOT_FOUND) { + return Mono.error(new FhirUnknownDomainException(b)); + } else { + return Mono.error(new FhirException(BAD_REQUEST, b)); + } + }); + } else { + return Mono.error(new NoFhirServerException("Unexpected error connecting to gICS")); + } } } diff --git a/trust-center-agent/src/main/java/care/smith/fts/tca/consent/configuration/GicsFhirConfiguration.java b/trust-center-agent/src/main/java/care/smith/fts/tca/consent/configuration/GicsFhirConfiguration.java index 30c83e48..6982107d 100644 --- a/trust-center-agent/src/main/java/care/smith/fts/tca/consent/configuration/GicsFhirConfiguration.java +++ b/trust-center-agent/src/main/java/care/smith/fts/tca/consent/configuration/GicsFhirConfiguration.java @@ -1,17 +1,27 @@ package care.smith.fts.tca.consent.configuration; -import care.smith.fts.tca.consent.FhirConsentedPatientsProvider; +import static care.smith.fts.util.FhirClientUtils.fetchCapabilityStatement; +import static care.smith.fts.util.FhirClientUtils.verifyOperationsExist; + +import care.smith.fts.tca.consent.GicsFhirConsentedPatientsProvider; import care.smith.fts.util.HttpClientConfig; import care.smith.fts.util.WebClientFactory; import care.smith.fts.util.auth.HttpClientAuth; +import care.smith.fts.util.error.fhir.FhirConnectException; import io.micrometer.core.instrument.MeterRegistry; import jakarta.validation.constraints.NotBlank; +import java.util.List; import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.ApplicationRunner; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +@Slf4j @Configuration @ConfigurationProperties(prefix = "consent.gics.fhir") @Data @@ -31,11 +41,32 @@ public WebClient gicsClient(WebClientFactory clientFactory) { return clientFactory.create(config); } + @Bean("gicsApplicationRunner") + ApplicationRunner runner(@Qualifier("gicsFhirHttpClient") WebClient gicsClient) { + return args -> + fetchCapabilityStatement(gicsClient) + .flatMap( + c -> + verifyOperationsExist( + c, List.of("allConsentsForDomain", "allConsentsForPerson")) + ? Mono.just(c) + : Mono.error(new FhirConnectException("Server is missing capabilities"))) + .doOnNext(i -> log.info("gCIS available")) + .doOnError( + e -> { + log.warn( + "Connection to gICS could not be established on agent startup. The agent will continue startup anyway, in case gICS connection will be available later on."); + log.debug("", e); + }) + .onErrorComplete() + .block(); + } + @Bean - FhirConsentedPatientsProvider fhirConsentedPatientsProvider( + GicsFhirConsentedPatientsProvider fhirConsentedPatientsProvider( WebClientFactory clientFactory, MeterRegistry meterRegistry) { var config = new HttpClientConfig(baseUrl, auth); var client = clientFactory.create(config); - return new FhirConsentedPatientsProvider(client, meterRegistry); + return new GicsFhirConsentedPatientsProvider(client, meterRegistry); } } diff --git a/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/GpasClient.java b/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/GpasClient.java index 4843dd02..b08c5154 100644 --- a/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/GpasClient.java +++ b/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/GpasClient.java @@ -1,15 +1,17 @@ package care.smith.fts.tca.deidentification; +import static org.springframework.http.HttpStatus.OK; + import care.smith.fts.util.MediaTypes; import care.smith.fts.util.RetryStrategies; -import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirUnknownDomainException; +import care.smith.fts.util.error.fhir.NoFhirServerException; import io.micrometer.core.instrument.MeterRegistry; import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.hl7.fhir.r4.model.OperationOutcome; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; @@ -49,7 +51,7 @@ public Mono fetchOrCreatePseudonyms(String domain, String id) { .bodyValue(params) .headers(h -> h.setAccept(List.of(MediaTypes.APPLICATION_FHIR_JSON))) .retrieve() - .onStatus(r1 -> r1.equals(HttpStatus.BAD_REQUEST), GpasClient::handleGpasBadRequest) + .onStatus(r -> !r.equals(OK), this::handleError) .bodyToMono(GpasParameterResponse.class) .retryWhen( RetryStrategies.defaultRetryStrategy(meterRegistry, "fetchOrCreatePseudonymsOnGpas")) @@ -59,14 +61,20 @@ public Mono fetchOrCreatePseudonyms(String domain, String id) { .map(map -> map.get(id)); } - private static Mono handleGpasBadRequest(ClientResponse r) { + private Mono handleError(ClientResponse r) { return r.bodyToMono(OperationOutcome.class) + .onErrorResume( + e -> { + log.error("Cannot parse OperationOutcome expected from gPAS", e); + return Mono.error( + new NoFhirServerException("Cannot parse OperationOutcome received from gPAS")); + }) .flatMap( b -> { var diagnostics = b.getIssueFirstRep().getDiagnostics(); log.error("Bad Request: {}", diagnostics); - if (diagnostics != null && diagnostics.startsWith("Unknown domain")) { - return Mono.error(new UnknownDomainException(diagnostics)); + if (diagnostics.startsWith("Unknown domain")) { + return Mono.error(new FhirUnknownDomainException(b)); } else { return Mono.error(new IllegalArgumentException(diagnostics)); } diff --git a/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/configuration/GpasFhirDeIdentificationConfiguration.java b/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/configuration/GpasFhirDeIdentificationConfiguration.java index cc830320..6e1f2d2d 100644 --- a/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/configuration/GpasFhirDeIdentificationConfiguration.java +++ b/trust-center-agent/src/main/java/care/smith/fts/tca/deidentification/configuration/GpasFhirDeIdentificationConfiguration.java @@ -1,18 +1,28 @@ package care.smith.fts.tca.deidentification.configuration; +import static care.smith.fts.util.FhirClientUtils.fetchCapabilityStatement; +import static care.smith.fts.util.FhirClientUtils.verifyOperationsExist; + import care.smith.fts.util.HttpClientConfig; import care.smith.fts.util.WebClientFactory; import care.smith.fts.util.auth.HttpClientAuth; +import care.smith.fts.util.error.fhir.FhirConnectException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.validation.constraints.NotBlank; import java.security.SecureRandom; +import java.util.List; import java.util.random.RandomGenerator; import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.ApplicationRunner; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +@Slf4j @Configuration @ConfigurationProperties(prefix = "de-identification.gpas.fhir") @Data @@ -30,6 +40,26 @@ public WebClient gpasClient(WebClientFactory clientFactory) { return clientFactory.create(new HttpClientConfig(baseUrl, auth)); } + @Bean("gpasApplicationRunner") + ApplicationRunner runner(@Qualifier("gpasFhirHttpClient") WebClient gpasClient) { + return args -> + fetchCapabilityStatement(gpasClient) + .flatMap( + c -> + verifyOperationsExist(c, List.of("pseudonymizeAllowCreate")) + ? Mono.just(c) + : Mono.error(new FhirConnectException("Server is missing capabilities"))) + .doOnNext(i -> log.info("gPAS available")) + .doOnError( + e -> { + log.warn( + "Connection to gPAS could not be established on agent startup. The agent will continue startup anyway, in case gPAS connection will be available later on."); + log.debug("", e); + }) + .onErrorComplete() + .block(); + } + @Bean public RandomGenerator secureRandom() { return new SecureRandom(); diff --git a/trust-center-agent/src/main/java/care/smith/fts/tca/rest/ConsentController.java b/trust-center-agent/src/main/java/care/smith/fts/tca/rest/ConsentController.java index 4efa8999..03be1430 100644 --- a/trust-center-agent/src/main/java/care/smith/fts/tca/rest/ConsentController.java +++ b/trust-center-agent/src/main/java/care/smith/fts/tca/rest/ConsentController.java @@ -1,11 +1,13 @@ package care.smith.fts.tca.rest; +import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.fromFhirException; +import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.internalServerError; + import care.smith.fts.tca.consent.ConsentedPatientsProvider; import care.smith.fts.tca.consent.ConsentedPatientsProvider.PagingParams; import care.smith.fts.util.MediaTypes; -import care.smith.fts.util.error.ErrorResponseUtil; -import care.smith.fts.util.error.FhirErrorResponseUtil; -import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirErrorResponseUtil; +import care.smith.fts.util.error.fhir.FhirException; import care.smith.fts.util.tca.ConsentFetchAllRequest; import care.smith.fts.util.tca.ConsentFetchRequest; import io.swagger.v3.oas.annotations.Operation; @@ -123,10 +125,10 @@ public Mono> fetch( } private static Mono> errorResponse(Throwable e) { - if (e instanceof UnknownDomainException) { - return FhirErrorResponseUtil.badRequest(e); + if (e instanceof FhirException) { + return fromFhirException((FhirException) e); } else { - return FhirErrorResponseUtil.internalServerError(e); + return internalServerError(e); } } } diff --git a/trust-center-agent/src/main/java/care/smith/fts/tca/rest/DeIdentificationController.java b/trust-center-agent/src/main/java/care/smith/fts/tca/rest/DeIdentificationController.java index 759efee3..fe6b4f0a 100644 --- a/trust-center-agent/src/main/java/care/smith/fts/tca/rest/DeIdentificationController.java +++ b/trust-center-agent/src/main/java/care/smith/fts/tca/rest/DeIdentificationController.java @@ -1,8 +1,12 @@ package care.smith.fts.tca.rest; +import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.fromFhirException; +import static care.smith.fts.util.error.fhir.FhirErrorResponseUtil.internalServerError; + import care.smith.fts.tca.deidentification.MappingProvider; import care.smith.fts.util.error.ErrorResponseUtil; import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirException; import care.smith.fts.util.tca.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -13,6 +17,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.extern.slf4j.Slf4j; +import org.hl7.fhir.r4.model.Bundle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/trust-center-agent/src/test/java/care/smith/fts/tca/consent/FhirConsentedPatientsProviderFetchAllIT.java b/trust-center-agent/src/test/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProviderFetchAllIT.java similarity index 80% rename from trust-center-agent/src/test/java/care/smith/fts/tca/consent/FhirConsentedPatientsProviderFetchAllIT.java rename to trust-center-agent/src/test/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProviderFetchAllIT.java index 6c2e0afe..a56b1219 100644 --- a/trust-center-agent/src/test/java/care/smith/fts/tca/consent/FhirConsentedPatientsProviderFetchAllIT.java +++ b/trust-center-agent/src/test/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProviderFetchAllIT.java @@ -5,12 +5,17 @@ import static care.smith.fts.util.FhirUtils.toBundle; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.status; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.matching.UrlPattern.ANY; import static java.lang.String.valueOf; import static java.util.Map.entry; import static java.util.Map.ofEntries; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.web.util.UriComponentsBuilder.fromUriString; import static reactor.test.StepVerifier.create; @@ -20,7 +25,9 @@ import care.smith.fts.test.FhirGenerators; import care.smith.fts.test.TestWebClientFactory; import care.smith.fts.util.FhirUtils; -import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirException; +import care.smith.fts.util.error.fhir.FhirUnknownDomainException; +import care.smith.fts.util.error.fhir.NoFhirServerException; import care.smith.fts.util.tca.ConsentFetchAllRequest; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; @@ -48,7 +55,7 @@ @SpringBootTest @WireMockTest @Import(TestWebClientFactory.class) -class FhirConsentedPatientsProviderFetchAllIT { +class GicsFhirConsentedPatientsProviderFetchAllIT { @Autowired WebClient.Builder httpClientBuilder; @Autowired MeterRegistry meterRegistry; @@ -71,7 +78,6 @@ class FhirConsentedPatientsProviderFetchAllIT { "MDAT_speichern_verarbeiten"); private static final ConsentFetchAllRequest CONSENT_FETCH_ALL_REQUEST = new ConsentFetchAllRequest("MII", POLICIES, POLICY_SYSTEM); - private String address; private FhirGenerator gicsConsentGenerator; private static final String jsonBody = """ @@ -81,12 +87,16 @@ class FhirConsentedPatientsProviderFetchAllIT { } """; private static WireMock wireMock; + private GicsFhirConsentedPatientsProvider fhirConsentProvider; @BeforeEach void setUp(WireMockRuntimeInfo wireMockRuntime) throws IOException { - address = wireMockRuntime.getHttpBaseUrl(); + var address = wireMockRuntime.getHttpBaseUrl(); wireMock = wireMockRuntime.getWireMock(); gicsConsentGenerator = FhirGenerators.gicsResponse(randomUuid(), randomUuid()); + fhirConsentProvider = + new GicsFhirConsentedPatientsProvider( + httpClientBuilder.baseUrl(address).build(), meterRegistry); } @AfterEach @@ -97,9 +107,6 @@ void tearDown() { @Test void paging() { int totalEntries = 2 * defaultPageSize; - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); Bundle bundle = gicsConsentGenerator @@ -154,11 +161,7 @@ void noNextLinkOnLastPage() { int totalEntries = 1; int pageSize = 1; - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - - Bundle bundle = + var bundle = Stream.generate(gicsConsentGenerator::generateString) .limit(totalEntries) .map(FhirUtils::stringToFhirBundle) @@ -186,11 +189,7 @@ void noNextLinkOnLastPage() { void noConsents() { int totalEntries = 0; int pageSize = 1; - - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - Bundle bundle = + var bundle = Stream.generate(gicsConsentGenerator::generateString) .limit(totalEntries) .map(FhirUtils::stringToFhirBundle) @@ -222,10 +221,6 @@ void noConsents() { void unknownDomainCausesGicsNotFound() { int pageSize = 2; - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var operationOutcome = new OperationOutcome(); var issue = operationOutcome.addIssue().setSeverity(IssueSeverity.ERROR); issue.setDiagnostics("No consents found for domain"); @@ -243,18 +238,13 @@ void unknownDomainCausesGicsNotFound() { CONSENT_FETCH_ALL_REQUEST, fromUriString("http://trustcenteragent:1234"), new PagingParams(0, pageSize))) - .expectError(UnknownDomainException.class) + .expectError(FhirUnknownDomainException.class) .verify(); } @Test void somethingElseCausesGicsNotFound() { int pageSize = 2; - - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var operationOutcome = new OperationOutcome(); var issue = operationOutcome.addIssue().setSeverity(IssueSeverity.ERROR); issue.setDiagnostics("Something's not right"); @@ -272,18 +262,13 @@ void somethingElseCausesGicsNotFound() { CONSENT_FETCH_ALL_REQUEST, fromUriString("http://trustcenteragent:1234"), new PagingParams(0, pageSize))) - .expectError(IllegalArgumentException.class) + .expectError(FhirUnknownDomainException.class) .verify(); } @Test - void diagnosticsIsNullInHandleGicsNotFound() { + void diagnosticsIsNullInHandle4xxError() { int pageSize = 2; - - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var operationOutcome = new OperationOutcome(); operationOutcome.addIssue().setSeverity(IssueSeverity.ERROR); @@ -300,7 +285,7 @@ void diagnosticsIsNullInHandleGicsNotFound() { CONSENT_FETCH_ALL_REQUEST, fromUriString("http://trustcenteragent:1234"), new PagingParams(0, pageSize))) - .expectError(IllegalArgumentException.class) + .expectError(FhirUnknownDomainException.class) .verify(); } @@ -308,12 +293,8 @@ void diagnosticsIsNullInHandleGicsNotFound() { void emptyPoliciesYieldEmptyBundle() { int totalEntries = 0; int pageSize = 2; - var consentRequest = new ConsentFetchAllRequest("MII", Set.of(), POLICY_SYSTEM); - var fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - Bundle bundle = + var bundle = Stream.generate(gicsConsentGenerator::generateString) .limit(totalEntries) .map(FhirUtils::stringToFhirBundle) @@ -339,4 +320,53 @@ void emptyPoliciesYieldEmptyBundle() { }) .verifyComplete(); } + + @Test + void responseIsNotFHIR() { + wireMock.register( + post("/$allConsentsForDomain") + .withRequestBody(equalToJson(jsonBody)) + .willReturn(status(I_AM_A_TEAPOT.value()).withBody("Would you like some milk?"))); + wireMock.register( + get("/metadata") + .willReturn(status(I_AM_A_TEAPOT.value()).withBody("Would you like some milk?"))); + create( + fhirConsentProvider.fetchAll( + CONSENT_FETCH_ALL_REQUEST, + fromUriString("http://trustcenteragent:8080"), + new PagingParams(0, 2))) + .expectError(NoFhirServerException.class) + .verify(); + } + + @Test + void gicsReturns500() { + wireMock.register( + post(ANY) + .willReturn( + status(INTERNAL_SERVER_ERROR.value()) + .withBody("what was I supposed to do again?"))); + create( + fhirConsentProvider.fetchAll( + CONSENT_FETCH_ALL_REQUEST, + fromUriString("http://trustcenteragent:8080"), + new PagingParams(0, 2))) + .expectError(FhirException.class) + .verify(); + } + + @Test + void noGicsServer() { + fhirConsentProvider = + new GicsFhirConsentedPatientsProvider( + httpClientBuilder.baseUrl("http://does-not-exist").build(), meterRegistry); + + create( + fhirConsentProvider.fetchAll( + CONSENT_FETCH_ALL_REQUEST, + fromUriString("http://trustcenteragent:8080"), + new PagingParams(0, 2))) + .expectError(NoFhirServerException.class) + .verify(); + } } diff --git a/trust-center-agent/src/test/java/care/smith/fts/tca/consent/FhirConsentedPatientsProviderFetchIT.java b/trust-center-agent/src/test/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProviderFetchIT.java similarity index 81% rename from trust-center-agent/src/test/java/care/smith/fts/tca/consent/FhirConsentedPatientsProviderFetchIT.java rename to trust-center-agent/src/test/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProviderFetchIT.java index 5db1ec03..e523cf87 100644 --- a/trust-center-agent/src/test/java/care/smith/fts/tca/consent/FhirConsentedPatientsProviderFetchIT.java +++ b/trust-center-agent/src/test/java/care/smith/fts/tca/consent/GicsFhirConsentedPatientsProviderFetchIT.java @@ -5,8 +5,12 @@ import static care.smith.fts.test.MockServerUtil.fhirResponse; import static care.smith.fts.util.FhirUtils.toBundle; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.status; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.web.util.UriComponentsBuilder.fromUriString; import static reactor.test.StepVerifier.create; @@ -16,7 +20,9 @@ import care.smith.fts.test.FhirGenerators; import care.smith.fts.test.TestWebClientFactory; import care.smith.fts.util.FhirUtils; -import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirException; +import care.smith.fts.util.error.fhir.FhirUnknownDomainException; +import care.smith.fts.util.error.fhir.NoFhirServerException; import care.smith.fts.util.tca.ConsentFetchRequest; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; @@ -44,7 +50,7 @@ @SpringBootTest @WireMockTest @Import(TestWebClientFactory.class) -class FhirConsentedPatientsProviderFetchIT { +class GicsFhirConsentedPatientsProviderFetchIT { @Autowired WebClient.Builder httpClientBuilder; @Autowired MeterRegistry meterRegistry; @@ -56,7 +62,7 @@ class FhirConsentedPatientsProviderFetchIT { "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy"; private static final String PATIENT_IDENTIFIER_SYSTEM = "https://ths-greifswald.de/fhir/gics/identifiers/Pseudonym"; - private FhirConsentedPatientsProvider fhirConsentProvider; + private GicsFhirConsentedPatientsProvider fhirConsentProvider; private static final Set POLICIES = Set.of( @@ -83,16 +89,18 @@ class FhirConsentedPatientsProviderFetchIT { ]} """; - private String address; private WireMock wireMock; private FhirGenerator gicsConsentGenerator; @BeforeEach void setUp(WireMockRuntimeInfo wireMockRuntime) throws IOException { - address = wireMockRuntime.getHttpBaseUrl(); + var address = wireMockRuntime.getHttpBaseUrl(); wireMock = wireMockRuntime.getWireMock(); gicsConsentGenerator = FhirGenerators.gicsResponse(randomUuid(), fromList(List.of("id1", "id2", "id3", "id4"))); + fhirConsentProvider = + new GicsFhirConsentedPatientsProvider( + httpClientBuilder.baseUrl(address).build(), meterRegistry); } @AfterEach @@ -105,10 +113,6 @@ void paging() { int pageSize = 2; int totalEntries = 2 * pageSize; - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var bundle1 = gicsConsentGenerator .generateResources() @@ -171,10 +175,7 @@ void noConsents() { int totalEntries = 0; int pageSize = 2; - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - Bundle bundle = + var bundle = Stream.generate(gicsConsentGenerator::generateString) .limit(totalEntries) .map(FhirUtils::stringToFhirBundle) @@ -205,11 +206,6 @@ void noConsents() { @Test void unknownDomainCausesGicsNotFound() { int pageSize = 2; - - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var operationOutcome = new OperationOutcome(); var issue = operationOutcome.addIssue().setSeverity(IssueSeverity.ERROR); issue.setDiagnostics("No consents found for domain"); @@ -224,18 +220,13 @@ void unknownDomainCausesGicsNotFound() { consentRequest, fromUriString("http://trustcenteragent:1234"), new PagingParams(0, pageSize))) - .expectError(UnknownDomainException.class) + .expectError(FhirUnknownDomainException.class) .verify(); } @Test void somethingElseCausesGicsNotFound() { int pageSize = 2; - - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var operationOutcome = new OperationOutcome(); var issue = operationOutcome.addIssue().setSeverity(IssueSeverity.ERROR); issue.setDiagnostics("Something's not right"); @@ -250,18 +241,13 @@ void somethingElseCausesGicsNotFound() { consentRequest, fromUriString("http://trustcenteragent:1234"), new PagingParams(0, pageSize))) - .expectError(IllegalArgumentException.class) + .expectError(FhirUnknownDomainException.class) .verify(); } @Test - void diagnosticsIsNullInHandleGicsNotFound() { + void diagnosticsIsNullInHandle4xxError() { int pageSize = 2; - - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); - var operationOutcome = new OperationOutcome(); operationOutcome.addIssue().setSeverity(IssueSeverity.ERROR); @@ -275,7 +261,29 @@ void diagnosticsIsNullInHandleGicsNotFound() { consentRequest, fromUriString("http://trustcenteragent:1234"), new PagingParams(0, pageSize))) - .expectError(IllegalArgumentException.class) + .expectError(FhirUnknownDomainException.class) + .verify(); + } + + @Test + void diagnosticsBadRequestInHandle4xxError() { + int pageSize = 2; + var operationOutcome = new OperationOutcome(); + var issue = operationOutcome.addIssue(); + issue.setSeverity(IssueSeverity.ERROR); + issue.setDiagnostics("Something's not right"); + + wireMock.register( + post("/$allConsentsForPerson") + .withRequestBody(equalToJson(jsonBody1, true, true)) + .willReturn(fhirResponse(operationOutcome, BAD_REQUEST))); + + create( + fhirConsentProvider.fetch( + consentRequest, + fromUriString("http://trustcenteragent:1234"), + new PagingParams(0, pageSize))) + .expectError(FhirException.class) .verify(); } @@ -288,9 +296,6 @@ void emptyPoliciesYieldEmptyBundle() { POLICY_SYSTEM, PATIENT_IDENTIFIER_SYSTEM, List.of("id1", "id2", "id3", "id4")); - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); create( fhirConsentProvider.fetch( @@ -306,12 +311,9 @@ void emptyPoliciesYieldEmptyBundle() { @Test void emptyPidsYieldEmptyBundle() { - ConsentFetchRequest consentRequest = + var consentRequest = new ConsentFetchRequest( "MII", POLICIES, POLICY_SYSTEM, PATIENT_IDENTIFIER_SYSTEM, List.of()); - fhirConsentProvider = - new FhirConsentedPatientsProvider( - httpClientBuilder.baseUrl(address).build(), meterRegistry); create( fhirConsentProvider.fetch( consentRequest, @@ -323,4 +325,22 @@ void emptyPidsYieldEmptyBundle() { }) .verifyComplete(); } + + @Test + void responseIsNotFHIR() { + wireMock.register( + post("/$allConsentsForPerson") + .withRequestBody(equalToJson(jsonBody1)) + .willReturn(status(I_AM_A_TEAPOT.value()).withBody("Would you like some milk?"))); + wireMock.register( + get("/metadata") + .willReturn(status(I_AM_A_TEAPOT.value()).withBody("Would you like some milk?"))); + create( + fhirConsentProvider.fetch( + consentRequest, + fromUriString("http://trustcenteragent:8080"), + new PagingParams(0, 2))) + .expectError(NoFhirServerException.class) + .verify(); + } } diff --git a/trust-center-agent/src/test/java/care/smith/fts/tca/consent/configuration/GicsFhirConfigurationTest.java b/trust-center-agent/src/test/java/care/smith/fts/tca/consent/configuration/GicsFhirConfigurationTest.java index 500f63a0..7b442f76 100644 --- a/trust-center-agent/src/test/java/care/smith/fts/tca/consent/configuration/GicsFhirConfigurationTest.java +++ b/trust-center-agent/src/test/java/care/smith/fts/tca/consent/configuration/GicsFhirConfigurationTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import care.smith.fts.tca.consent.FhirConsentedPatientsProvider; +import care.smith.fts.tca.consent.GicsFhirConsentedPatientsProvider; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -16,7 +16,7 @@ class GicsFhirConfigurationIT { @Autowired private GicsFhirConfiguration gicsFhirConfiguration; - @Autowired private FhirConsentedPatientsProvider fhirConsentProvider; + @Autowired private GicsFhirConsentedPatientsProvider fhirConsentProvider; @MockitoBean RedissonClient redisClient; // We need to mock the redisClient otherwise the tests won't start diff --git a/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/FhirMappingProviderTest.java b/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/FhirMappingProviderTest.java index 26f2092d..746e88ee 100644 --- a/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/FhirMappingProviderTest.java +++ b/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/FhirMappingProviderTest.java @@ -20,6 +20,7 @@ import care.smith.fts.test.FhirGenerators; import care.smith.fts.test.TestWebClientFactory; import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirUnknownDomainException; import care.smith.fts.util.tca.TCADomains; import care.smith.fts.util.tca.TransportMappingRequest; import com.github.tomakehurst.wiremock.client.WireMock; @@ -184,7 +185,7 @@ void fetchResearchMappingWithUnknownDomainException() { given(mapCache.expire(Duration.ofMinutes(10))).willReturn(Mono.just(false)); create(mappingProvider.generateTransportMapping(DEFAULT_REQUEST)) - .expectError(UnknownDomainException.class) + .expectError(FhirUnknownDomainException.class) .verify(); } diff --git a/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/GpasClientIT.java b/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/GpasClientIT.java new file mode 100644 index 00000000..a8784f94 --- /dev/null +++ b/trust-center-agent/src/test/java/care/smith/fts/tca/deidentification/GpasClientIT.java @@ -0,0 +1,55 @@ +package care.smith.fts.tca.deidentification; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT; +import static reactor.test.StepVerifier.create; + +import care.smith.fts.util.error.fhir.NoFhirServerException; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@SpringBootTest +@WireMockTest +public class GpasClientIT { + @Autowired WebClient.Builder httpClientBuilder; + + @MockitoBean + RedissonClient redisClient; // We need to mock the redisClient otherwise the tests won't start + + private static WireMock wireMock; + + GpasClient gpasClient; + + @BeforeEach + void setUp(WireMockRuntimeInfo wireMockRuntime, @Autowired MeterRegistry meterRegistry) { + var address = wireMockRuntime.getHttpBaseUrl(); + wireMock = wireMockRuntime.getWireMock(); + gpasClient = new GpasClient(httpClientBuilder.baseUrl(address).build(), meterRegistry); + } + + @Test + void responseIsNotFHIR() { + wireMock.register( + post("/$pseudonymizeAllowCreate") + .willReturn(status(I_AM_A_TEAPOT.value()).withBody("Would you like some milk?"))); + wireMock.register( + get("/metadata") + .willReturn(status(I_AM_A_TEAPOT.value()).withBody("Would you like some milk?"))); + create(gpasClient.fetchOrCreatePseudonyms("domain", "id")) + .expectError(NoFhirServerException.class) + .verify(); + } +} diff --git a/trust-center-agent/src/test/java/care/smith/fts/tca/rest/ConsentControllerTest.java b/trust-center-agent/src/test/java/care/smith/fts/tca/rest/ConsentControllerTest.java index 8327cf14..421b23bc 100644 --- a/trust-center-agent/src/test/java/care/smith/fts/tca/rest/ConsentControllerTest.java +++ b/trust-center-agent/src/test/java/care/smith/fts/tca/rest/ConsentControllerTest.java @@ -9,6 +9,7 @@ import care.smith.fts.tca.consent.ConsentedPatientsProvider; import care.smith.fts.tca.consent.ConsentedPatientsProvider.PagingParams; import care.smith.fts.util.error.UnknownDomainException; +import care.smith.fts.util.error.fhir.FhirUnknownDomainException; import care.smith.fts.util.tca.ConsentFetchAllRequest; import care.smith.fts.util.tca.ConsentFetchRequest; import java.util.List; @@ -70,15 +71,14 @@ void fetchAllEmptyPageYieldsEmptyBundle() { @Test void fetchErrorResponseYieldsBadRequest() { given(provider.fetchAll(consentFetchAllRequest, requestUrl, new PagingParams(0, 1))) - .willReturn(Mono.error(new UnknownDomainException(""))); + .willReturn(Mono.error(new FhirUnknownDomainException(""))); given(provider.fetch(consentFetchRequest, requestUrl, new PagingParams(0, 1))) - .willReturn(Mono.error(new UnknownDomainException(""))); + .willReturn(Mono.error(new FhirUnknownDomainException(""))); responses() .forEach( response -> create(response) - .assertNext( - b -> assertThat(b.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)) + .assertNext(b -> assertThat(b.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)) .verifyComplete()); } diff --git a/util/src/main/java/care/smith/fts/util/FhirClientUtils.java b/util/src/main/java/care/smith/fts/util/FhirClientUtils.java new file mode 100644 index 00000000..1e3568eb --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/FhirClientUtils.java @@ -0,0 +1,38 @@ +package care.smith.fts.util; + +import jakarta.validation.constraints.NotNull; +import java.util.HashSet; +import java.util.List; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceOperationComponent; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public interface FhirClientUtils { + + static Mono fetchCapabilityStatement(WebClient client) { + return client.get().uri("/metadata").retrieve().bodyToMono(CapabilityStatement.class); + } + + /** + * Verifies if all specified Resource operation names are present in the given + * CapabilityStatement. + * + * @param capabilityStatement The FHIR CapabilityStatement to check + * @param operationNames List of operation names to verify + * @return boolean indicating if all operations were found (true) or not (false) + */ + static boolean verifyOperationsExist( + @NotNull CapabilityStatement capabilityStatement, @NotNull List operationNames) { + + var remainingOperations = new HashSet<>(operationNames); + return capabilityStatement.getRest().stream() + .flatMap(resource -> resource.getOperation().stream()) + .map(CapabilityStatementRestResourceOperationComponent::getName) + .anyMatch( + name -> { + remainingOperations.remove(name); + return remainingOperations.isEmpty(); + }); + } +} diff --git a/util/src/main/java/care/smith/fts/util/FhirUtils.java b/util/src/main/java/care/smith/fts/util/FhirUtils.java index 54d83794..01fe48a4 100644 --- a/util/src/main/java/care/smith/fts/util/FhirUtils.java +++ b/util/src/main/java/care/smith/fts/util/FhirUtils.java @@ -10,7 +10,9 @@ import java.util.stream.Stream; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CapabilityStatement; import org.hl7.fhir.r4.model.Resource; +import org.springframework.web.reactive.function.client.WebClient; public interface FhirUtils { FhirContext fctx = FhirContext.forR4(); @@ -76,4 +78,5 @@ private static Bundle toBundle(List l) { l.stream().map(r -> new Bundle.BundleEntryComponent().setResource(r)).toList(); return new Bundle().setTotal(l.size()).setEntry(list); } + } diff --git a/util/src/main/java/care/smith/fts/util/RetryStrategies.java b/util/src/main/java/care/smith/fts/util/RetryStrategies.java index 0fab7e50..20766386 100644 --- a/util/src/main/java/care/smith/fts/util/RetryStrategies.java +++ b/util/src/main/java/care/smith/fts/util/RetryStrategies.java @@ -6,18 +6,27 @@ import io.micrometer.core.instrument.MeterRegistry; import java.time.Duration; import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.reactive.function.client.WebClientRequestException; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.util.retry.Retry; import reactor.util.retry.RetryBackoffSpec; public interface RetryStrategies { - + Logger LOGGER = LoggerFactory.getLogger(RetryStrategies.class); boolean RETRY_TIMEOUT = parseBoolean(System.getProperty("fts.retryTimeout", "true")); static RetryBackoffSpec defaultRetryStrategy(MeterRegistry meterRegistry, String name) { + LOGGER.info("Using default retry strategy: {}", name); var counter = meterRegistry.counter("http.client.requests.retries", "request_name", name); return Retry.backoff(3, Duration.ofSeconds(1)) - .filter(or(RetryStrategies::is5xxServerError, RetryStrategies::isTimeout)) + .doBeforeRetry(retry -> LOGGER.info("RetrySignal {}", retry)) + .filter( + or( + RetryStrategies::is5xxServerError, + RetryStrategies::isTimeout, + RetryStrategies::isConnectException)) .doAfterRetry(i -> counter.increment()); } @@ -29,4 +38,8 @@ private static boolean is5xxServerError(Throwable e) { return e instanceof WebClientResponseException && ((WebClientResponseException) e).getStatusCode().is5xxServerError(); } + + private static boolean isConnectException(Throwable e) { + return e instanceof WebClientRequestException; + } } diff --git a/util/src/main/java/care/smith/fts/util/error/fhir/FhirConnectException.java b/util/src/main/java/care/smith/fts/util/error/fhir/FhirConnectException.java new file mode 100644 index 00000000..8ebeaa47 --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/error/fhir/FhirConnectException.java @@ -0,0 +1,13 @@ +package care.smith.fts.util.error.fhir; + +import static org.springframework.http.HttpStatus.BAD_GATEWAY; + +import org.springframework.http.HttpStatus; + +public class FhirConnectException extends FhirException { + private static final HttpStatus status = BAD_GATEWAY; + + public FhirConnectException(String message) { + super(status, message); + } +} diff --git a/util/src/main/java/care/smith/fts/util/error/FhirErrorResponseUtil.java b/util/src/main/java/care/smith/fts/util/error/fhir/FhirErrorResponseUtil.java similarity index 58% rename from util/src/main/java/care/smith/fts/util/error/FhirErrorResponseUtil.java rename to util/src/main/java/care/smith/fts/util/error/fhir/FhirErrorResponseUtil.java index 0f352ad5..93e57a9c 100644 --- a/util/src/main/java/care/smith/fts/util/error/FhirErrorResponseUtil.java +++ b/util/src/main/java/care/smith/fts/util/error/fhir/FhirErrorResponseUtil.java @@ -1,4 +1,4 @@ -package care.smith.fts.util.error; +package care.smith.fts.util.error.fhir; import org.hl7.fhir.r4.model.OperationOutcome; import org.springframework.http.HttpStatus; @@ -7,6 +7,21 @@ public interface FhirErrorResponseUtil { + @SuppressWarnings("unchecked") + static Mono> fromFhirException(FhirException e) { + return Mono.just((ResponseEntity) ResponseEntity.status(e.statusCode).body(e.getMessage())); + } + + @SuppressWarnings("unchecked") + private static Mono> onError( + OperationOutcome outcome, HttpStatus httpStatus) { + return Mono.just((ResponseEntity) ResponseEntity.status(httpStatus).body(outcome)); + } + + static Mono> internalServerError(OperationOutcome outcome) { + return onError(outcome, HttpStatus.INTERNAL_SERVER_ERROR); + } + @SuppressWarnings("unchecked") private static Mono> onError(Throwable e, HttpStatus httpStatus) { var outcome = operationOutcomeWithIssue(e); @@ -22,10 +37,6 @@ static OperationOutcome operationOutcomeWithIssue(Throwable e) { return outcome; } - static Mono> badRequest(Throwable e) { - return onError(e, HttpStatus.BAD_REQUEST); - } - static Mono> internalServerError(Throwable e) { return onError(e, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/util/src/main/java/care/smith/fts/util/error/fhir/FhirException.java b/util/src/main/java/care/smith/fts/util/error/fhir/FhirException.java new file mode 100644 index 00000000..a6bc4123 --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/error/fhir/FhirException.java @@ -0,0 +1,27 @@ +package care.smith.fts.util.error.fhir; + +import lombok.Getter; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.springframework.http.HttpStatusCode; + +@Getter +public class FhirException extends Exception { + HttpStatusCode statusCode; + OperationOutcome operationOutcome; + + public FhirException(HttpStatusCode statusCode, String message) { + super(message); + this.statusCode = statusCode; + operationOutcome = new OperationOutcome(); + operationOutcome + .addIssue() + .setSeverity(OperationOutcome.IssueSeverity.ERROR) + .setDiagnostics(message); + } + + public FhirException(HttpStatusCode statusCode, OperationOutcome operationOutcome) { + super(operationOutcome.getIssueFirstRep().getDiagnostics()); + this.statusCode = statusCode; + this.operationOutcome = operationOutcome; + } +} diff --git a/util/src/main/java/care/smith/fts/util/error/fhir/FhirUnknownDomainException.java b/util/src/main/java/care/smith/fts/util/error/fhir/FhirUnknownDomainException.java new file mode 100644 index 00000000..2e5bebcf --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/error/fhir/FhirUnknownDomainException.java @@ -0,0 +1,18 @@ +package care.smith.fts.util.error.fhir; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import org.hl7.fhir.r4.model.OperationOutcome; +import org.springframework.http.HttpStatus; + +public class FhirUnknownDomainException extends FhirException { + private static final HttpStatus status = NOT_FOUND; + + public FhirUnknownDomainException(String message) { + super(status, message); + } + + public FhirUnknownDomainException(OperationOutcome outcome) { + super(status, outcome); + } +} diff --git a/util/src/main/java/care/smith/fts/util/error/fhir/NoFhirServerException.java b/util/src/main/java/care/smith/fts/util/error/fhir/NoFhirServerException.java new file mode 100644 index 00000000..86a98ce5 --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/error/fhir/NoFhirServerException.java @@ -0,0 +1,13 @@ +package care.smith.fts.util.error.fhir; + +import static org.springframework.http.HttpStatus.BAD_GATEWAY; + +import org.springframework.http.HttpStatus; + +public class NoFhirServerException extends FhirException { + private static final HttpStatus status = BAD_GATEWAY; + + public NoFhirServerException(String message) { + super(status, message); + } +} diff --git a/util/src/test/java/care/smith/fts/util/FhirClientUtilsTest.java b/util/src/test/java/care/smith/fts/util/FhirClientUtilsTest.java new file mode 100644 index 00000000..08e87b13 --- /dev/null +++ b/util/src/test/java/care/smith/fts/util/FhirClientUtilsTest.java @@ -0,0 +1,54 @@ +package care.smith.fts.util; + +import static care.smith.fts.util.FhirClientUtils.verifyOperationsExist; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceOperationComponent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FhirClientUtilsTest { + + private CapabilityStatement capabilityStatement; + + @BeforeEach + public void setUp() { + capabilityStatement = new CapabilityStatement(); + + var restComponent = new CapabilityStatementRestComponent(); + + var operation1 = new CapabilityStatementRestResourceOperationComponent(); + operation1.setName("operation1"); + + var operation2 = new CapabilityStatementRestResourceOperationComponent(); + operation2.setName("operation2"); + + restComponent.setOperation(Arrays.asList(operation1, operation2)); + capabilityStatement.setRest(Collections.singletonList(restComponent)); + } + + @Test + public void testAllOperationsPresent() { + var operationNames = List.of("operation1", "operation2"); + assertThat(verifyOperationsExist(capabilityStatement, operationNames)).isTrue(); + } + + @Test + public void testSomeOperationsMissing() { + var operationNames = List.of("operation1", "operation3"); + assertThat(verifyOperationsExist(capabilityStatement, operationNames)).isFalse(); + } + + @Test + public void testEmptyOperationList() { + var operationNames = List.of(); + assertThat(verifyOperationsExist(capabilityStatement, operationNames)).isTrue(); + } +} diff --git a/util/src/test/java/care/smith/fts/util/error/FhirErrorResponseUtilTest.java b/util/src/test/java/care/smith/fts/util/error/FhirErrorResponseUtilTest.java deleted file mode 100644 index 86d4a03a..00000000 --- a/util/src/test/java/care/smith/fts/util/error/FhirErrorResponseUtilTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package care.smith.fts.util.error; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; -import reactor.test.StepVerifier; - -class FhirErrorResponseUtilTest { - - @Test - void badRequest() { - StepVerifier.create(FhirErrorResponseUtil.badRequest(new RuntimeException())) - .assertNext(r -> assertThat(r.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)); - } - - @Test - void internalServerError() { - StepVerifier.create(FhirErrorResponseUtil.internalServerError(new RuntimeException())) - .assertNext(r -> assertThat(r.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)); - } -} diff --git a/util/src/test/java/care/smith/fts/util/error/fhir/FhirConnectExceptionTest.java b/util/src/test/java/care/smith/fts/util/error/fhir/FhirConnectExceptionTest.java new file mode 100644 index 00000000..a2a9364c --- /dev/null +++ b/util/src/test/java/care/smith/fts/util/error/fhir/FhirConnectExceptionTest.java @@ -0,0 +1,17 @@ +package care.smith.fts.util.error.fhir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.BAD_GATEWAY; + +import org.junit.jupiter.api.Test; + +class FhirConnectExceptionTest { + + @Test + void fhirConnectException() { + var e = new FhirConnectException("message"); + assertThat(e.statusCode).isEqualTo(BAD_GATEWAY); + assertThat(e).hasMessage("message"); + assertThat(e.getOperationOutcome().getIssueFirstRep().getDiagnostics()).isEqualTo("message"); + } +} diff --git a/util/src/test/java/care/smith/fts/util/error/fhir/FhirErrorResponseUtilTest.java b/util/src/test/java/care/smith/fts/util/error/fhir/FhirErrorResponseUtilTest.java new file mode 100644 index 00000000..3ccdd859 --- /dev/null +++ b/util/src/test/java/care/smith/fts/util/error/fhir/FhirErrorResponseUtilTest.java @@ -0,0 +1,31 @@ +package care.smith.fts.util.error.fhir; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hl7.fhir.r4.model.OperationOutcome; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import reactor.test.StepVerifier; + +class FhirErrorResponseUtilTest { + + @Test + void fromFhirException() { + StepVerifier.create( + FhirErrorResponseUtil.fromFhirException( + new FhirException(HttpStatus.INTERNAL_SERVER_ERROR, "test"))) + .assertNext(r -> assertThat(r.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)); + } + + @Test + void internalServerErrorViaException() { + StepVerifier.create(FhirErrorResponseUtil.internalServerError(new RuntimeException())) + .assertNext(r -> assertThat(r.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)); + } + + @Test + void internalServerError() { + StepVerifier.create(FhirErrorResponseUtil.internalServerError(new OperationOutcome())) + .assertNext(r -> assertThat(r.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)); + } +} diff --git a/util/src/test/java/care/smith/fts/util/error/fhir/FhirUnknownDomainExceptionTest.java b/util/src/test/java/care/smith/fts/util/error/fhir/FhirUnknownDomainExceptionTest.java new file mode 100644 index 00000000..fea5e478 --- /dev/null +++ b/util/src/test/java/care/smith/fts/util/error/fhir/FhirUnknownDomainExceptionTest.java @@ -0,0 +1,27 @@ +package care.smith.fts.util.error.fhir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import org.hl7.fhir.r4.model.OperationOutcome; +import org.junit.jupiter.api.Test; + +class FhirUnknownDomainExceptionTest { + @Test + void fhirUnknownDomainException() { + var e = new FhirUnknownDomainException("message"); + assertThat(e.statusCode).isEqualTo(NOT_FOUND); + assertThat(e).hasMessage("message"); + assertThat(e.getOperationOutcome().getIssueFirstRep().getDiagnostics()).isEqualTo("message"); + } + + @Test + void fhirUnknownDomainExceptionFromOperationOutcome() { + var outcome = new OperationOutcome(); + outcome.addIssue().setDiagnostics("message"); + var e = new FhirUnknownDomainException(outcome); + assertThat(e.statusCode).isEqualTo(NOT_FOUND); + assertThat(e).hasMessage("message"); + assertThat(e.getOperationOutcome().getIssueFirstRep().getDiagnostics()).isEqualTo("message"); + } +} diff --git a/util/src/test/java/care/smith/fts/util/error/fhir/NoFhirServerExceptionTest.java b/util/src/test/java/care/smith/fts/util/error/fhir/NoFhirServerExceptionTest.java new file mode 100644 index 00000000..d9eeae67 --- /dev/null +++ b/util/src/test/java/care/smith/fts/util/error/fhir/NoFhirServerExceptionTest.java @@ -0,0 +1,16 @@ +package care.smith.fts.util.error.fhir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.BAD_GATEWAY; + +import org.junit.jupiter.api.Test; + +class NoFhirServerExceptionTest { + @Test + void noFhirServerException() { + var e = new FhirConnectException("message"); + assertThat(e.statusCode).isEqualTo(BAD_GATEWAY); + assertThat(e).hasMessage("message"); + assertThat(e.getOperationOutcome().getIssueFirstRep().getDiagnostics()).isEqualTo("message"); + } +}