diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e1bfe57..8fd17f5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -## [UNRELEASED] - yyyy-mm-dd +## [4.3.0] - 2024-02-02 ### Added +- Basic auth for direct broker ([#210](https://github.com/medizininformatik-initiative/feasibility-backend/issues/210)) ### Changed -### Deprecated -### Removed -### Fixed +- Updated sq2cql to 0.2.14 ([#253](https://github.com/medizininformatik-initiative/feasibility-backend/issues/253)) +- Reduce verbosity of DSF Webservice client ([#247](https://github.com/medizininformatik-initiative/feasibility-backend/issues/247)) ### Security - -The full changelog can be found [here](https://todo). +- Updated spring boot to 3.2.2 ([#251](https://github.com/medizininformatik-initiative/feasibility-backend/issues/251)) ## [4.2.0] - 2023-11-17 diff --git a/README.md b/README.md index b6a1d24e..6e0dc0ba 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ | QUERY_VALIDATION_ENABLED | When enabled, any structured query submitted via the `run-query` endpoint is validated against the JSON schema located in `src/main/resources/query/query-schema.json` | true / false | true | | QUERYRESULT_EXPIRY_MINUTES | How many minutes should query results be kept in memory? | | 5 | | QUERYRESULT_PUBLIC_KEY | The public key in Base64-encoded DER format without banners and line breaks. Mandatory if _QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION_ is _false_ | -| QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION | Disable encryption of the result log file. | true / false | | +| QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION | Disable encryption of the result log file. | true / false | | | ALLOWED_ORIGINS | Allowed origins for cross-origin requests. This should at least cover the frontend address. | | http://localhost | | MAX_SAVED_QUERIES_PER_USER | How many slots does a user have to store saved queries. | | 10 | @@ -45,10 +45,12 @@ The DIRECT path can be run **either** with FLARE **or** with a CQL compatible se Result counts from the direct path can be obfuscated for privacy reasons. The current implementation handles obfuscation by adding or subtracting a random number <=5. -| EnvVar | Description | Example | Default | -|--------------------------------------|--------------------------------------------------------------------------------|---------|---------| -| BROKER_CLIENT_DIRECT_USE_CQL | Whether to use a CQL server or not. | | false | -| BROKER_CLIENT_OBFUSCATE_RESULT_COUNT | Whether the result counts retrieved from the direct broker shall be obfuscated | | false | +| EnvVar | Description | Example | Default | +|-------------------------------------------|--------------------------------------------------------------------------------|--------------------|---------| +| BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME | Username to use to connect to flare or directly to the FHIR server via CQL | feas-user | | +| BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD | Password for that user | verysecurepassword | | +| BROKER_CLIENT_DIRECT_USE_CQL | Whether to use a CQL server or not. | | false | +| BROKER_CLIENT_OBFUSCATE_RESULT_COUNT | Whether the result counts retrieved from the direct broker shall be obfuscated | | false | This is irrelevant if _BROKER_CLIENT_DIRECT_ENABLED_ is set to false. @@ -92,10 +94,10 @@ In order to run the backend using the DSF path, the following environment variab | DSF_PROXY_USERNAME | Proxy username to be used. | | | | DSF_PROXY_PASSWORD | Proxy password to be used. | | | | DSF_WEBSERVICE_BASE_URL | Base URL pointing to the local ZARS FHIR server. | `https://zars/fhir` | | +| DSF_WEBSERVICE_LOG_REQUESTS | Log webservice client communication at log level INFO or below (**WARNING**: potentially contains sensitive data) | `true` | `false` | | DSF_WEBSOCKET_URL | URL pointing to the local ZARS FHIR server websocket endpoint. | `wss://zars/fhir/ws` | | | DSF_ORGANIZATION_ID | Identifier for the local organization this backend is part of. | `MY ZARS` | | - ### Privacy and Obfuscation In order to prevent potentially malicious attempts to obtain critical patient data, several diff --git a/docker-compose.yml b/docker-compose.yml index 8fac7976..a6a2b395 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,8 @@ services: QUERYRESULT_EXPIRY_MINUTES: ${CODEX_FEASIBILITY_BACKEND_QUERYRESULT_EXPIRY_MINUTES:-5} MAX_SAVED_QUERIES_PER_USER: ${CODEX_FEASIBILITY_BACKEND_MAX_SAVED_QUERIES_PER_USER:-10} # ---- Direct + BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME} + BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD} BROKER_CLIENT_DIRECT_USE_CQL: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_DIRECT_USE_CQL:-false} BROKER_CLIENT_OBFUSCATE_RESULT_COUNT: ${CODEX_FEASIBILITY_BACKEND_BROKER_CLIENT_OBFUSCATE_RESULT_COUNT:-false} # ---- Aktin @@ -65,6 +67,7 @@ services: PRIVACY_THRESHOLD_RESULTS: ${CODEX_FEASIBILITY_BACKEND_PRIVACY_THRESHOLD_RESULTS} PRIVACY_THRESHOLD_SITES: ${CODEX_FEASIBILITY_BACKEND_PRIVACY_THRESHOLD_SITES} PRIVACY_THRESHOLD_SITES_RESULT: ${CODEX_FEASIBILITY_BACKEND_PRIVACY_THRESHOLD_SITES_RESULT} + QUERYRESULT_DISABLE_LOG_FILE_ENCRYPTION: "false" volumes: - ${CODEX_FEASIBILITY_BACKEND_LOCAL_CONCEPT_TREE_PATH:-./ontology/codex-code-tree.json}:${CODEX_FEASIBILITY_BACKEND_ONTOLOGY_FILES_FOLDER:-/opt/codex-feasibility-backend/ontology}/codex-code-tree.json - ${CODEX_FEASIBILITY_BACKEND_LOCAL_TERM_CODE_MAPPING_PATH:-./ontology/codex-term-code-mapping.json}:${CODEX_FEASIBILITY_BACKEND_ONTOLOGY_FILES_FOLDER:-/opt/codex-feasibility-backend/ontology}/codex-term-code-mapping.json @@ -79,3 +82,11 @@ services: - POSTGRES_USER=codex-postgres - POSTGRES_PASSWORD=codex-password - POSTGRES_DB=codex_ui + volumes: + - type: volume + source: feas-backend-db-data + target: /var/lib/postgresql/data + +volumes: + feas-backend-db-data: + name: "feas-backend-db-data" diff --git a/pom.xml b/pom.xml index d0691b9a..9fc60884 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ org.springframework.boot spring-boot-starter-parent - 3.1.5 + 3.2.2 de.medizininformatik-initiative FeasibilityGuiBackend - 4.2.0 + 4.3.0 FeasibilityGuiBackend Backend of the Feasibility GUI @@ -198,7 +198,7 @@ de.medizininformatik-initiative sq2cql - 0.2.4 + 0.2.14 diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfig.java index e955ca1c..1a7fd638 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfig.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfig.java @@ -1,7 +1,9 @@ package de.numcodex.feasibility_gui_backend.query.broker.direct; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; import de.numcodex.feasibility_gui_backend.query.broker.BrokerClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -10,6 +12,8 @@ import org.springframework.context.annotation.Lazy; import org.springframework.web.reactive.function.client.WebClient; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; + /** * Spring configuration for providing a {@link DirectBrokerClient} implementation instance. * Either {@link DirectBrokerClientCql} or {@link DirectBrokerClientFlare} @@ -18,36 +22,55 @@ @Configuration public class DirectSpringConfig { - @Value("${app.broker.direct.useCql:false}") - private boolean useCql; + private final boolean useCql; + + private final String flareBaseUrl; + + private final String cqlBaseUrl; - @Value("${app.flare.baseUrl}") - private String flareBaseUrl; + private final String username; - @Value("${app.cql.baseUrl}") - private String cqlBaseUrl; + private final String password; + + public DirectSpringConfig(@Value("${app.broker.direct.useCql:false}") boolean useCql, @Value("${app.flare.baseUrl}") String flareBaseUrl, @Value("${app.cql.baseUrl}") String cqlBaseUrl, @Value("${app.broker.direct.auth.basic.username}") String username, @Value("${app.broker.direct.auth.basic.password}") String password) { + this.useCql = useCql; + this.flareBaseUrl = flareBaseUrl; + this.cqlBaseUrl = cqlBaseUrl; + this.username = username; + this.password = password; + } @Qualifier("direct") @Bean public BrokerClient directBrokerClient(WebClient directWebClientFlare, @Value("${app.broker.direct.obfuscateResultCount:false}") boolean obfuscateResultCount, FhirConnector fhirConnector, FhirHelper fhirHelper) { if (useCql) { - - return new DirectBrokerClientCql(fhirConnector, obfuscateResultCount, - fhirHelper); + return new DirectBrokerClientCql(fhirConnector, obfuscateResultCount, fhirHelper); } else { return new DirectBrokerClientFlare(directWebClientFlare, obfuscateResultCount); } } @Bean - public IGenericClient getFhirClient(FhirContext fhirContext){ - return fhirContext.newRestfulGenericClient(cqlBaseUrl); + public IGenericClient getFhirClient(FhirContext fhirContext) { + IGenericClient iGenericClient = fhirContext.newRestfulGenericClient(cqlBaseUrl); + if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) { + IClientInterceptor authInterceptor = new BasicAuthInterceptor(username, password); + iGenericClient.registerInterceptor(authInterceptor); + } + return iGenericClient; } @Bean public WebClient directWebClientFlare() { - return WebClient.create(flareBaseUrl); + if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) { + return WebClient.builder() + .filter(basicAuthentication(username, password)) + .baseUrl(flareBaseUrl) + .build(); + } else { + return WebClient.create(flareBaseUrl); + } } } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProvider.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProvider.java index 673f4a2c..17d58d05 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProvider.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProvider.java @@ -38,12 +38,14 @@ class DSFFhirWebClientProvider implements FhirWebClientProvider { private final FhirSecurityContextProvider securityContextProvider; private FhirSecurityContext securityContext; private final FhirProxyContext proxyContext; + private boolean logRequests; public DSFFhirWebClientProvider(FhirContext fhirContext, String webserviceBaseUrl, int webserviceReadTimeout, int webserviceConnectTimeout, String websocketUrl, FhirSecurityContextProvider securityContextProvider, - FhirProxyContext proxyContext) { + FhirProxyContext proxyContext, + boolean logRequests) { this.fhirContext = fhirContext; this.webserviceBaseUrl = webserviceBaseUrl; this.webserviceReadTimeout = webserviceReadTimeout; @@ -51,6 +53,7 @@ public DSFFhirWebClientProvider(FhirContext fhirContext, String webserviceBaseUr this.websocketUrl = websocketUrl; this.securityContextProvider = securityContextProvider; this.proxyContext = proxyContext; + this.logRequests = logRequests; } @Override @@ -76,7 +79,7 @@ public FhirWebserviceClient provideFhirWebserviceClient() throws FhirWebClientPr proxyContext.getPassword(), webserviceConnectTimeout, webserviceReadTimeout, - true, + logRequests, null, fhirContext, cleaner); diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFSpringConfig.java index c2269ed0..f720d5c3 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFSpringConfig.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFSpringConfig.java @@ -43,6 +43,9 @@ public class DSFSpringConfig { @Value("${app.broker.dsf.webservice.connectTimeout}") private int webserviceConnectTimeout; + @Value("${app.broker.dsf.webservice.logRequests}") + private boolean logRequests; + @Value("${app.broker.dsf.websocket.url}") private String websocketUrl; @@ -102,7 +105,7 @@ FhirWebClientProvider fhirWebClientProvider(FhirContext fhirContext, FhirSecurityContextProvider securityContextProvider, FhirProxyContext proxyContext) { return new DSFFhirWebClientProvider(fhirContext, webserviceBaseUrl, webserviceReadTimeout, - webserviceConnectTimeout, websocketUrl, securityContextProvider, proxyContext); + webserviceConnectTimeout, websocketUrl, securityContextProvider, proxyContext, logRequests); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3cfa027e..da779be3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -56,6 +56,10 @@ app: mock: enabled: ${BROKER_CLIENT_MOCK_ENABLED:false} direct: + auth: + basic: + username: ${BROKER_CLIENT_DIRECT_AUTH_BASIC_USERNAME:} + password: ${BROKER_CLIENT_DIRECT_AUTH_BASIC_PASSWORD:} enabled: ${BROKER_CLIENT_DIRECT_ENABLED:false} useCql: ${BROKER_CLIENT_DIRECT_USE_CQL:false} obfuscateResultCount: ${BROKER_CLIENT_OBFUSCATE_RESULT_COUNT:false} @@ -79,6 +83,7 @@ app: baseUrl: ${DSF_WEBSERVICE_BASE_URL} readTimeout: 20000 connectTimeout: 2000 + logRequests: ${DSF_WEBSERVICE_LOG_REQUESTS:false} websocket: url: ${DSF_WEBSOCKET_URL} organizationId: ${DSF_ORGANIZATION_ID} diff --git a/src/main/resources/static/v3/api-docs/swagger.yaml b/src/main/resources/static/v3/api-docs/swagger.yaml index 4eb366de..a7fc50ba 100644 --- a/src/main/resources/static/v3/api-docs/swagger.yaml +++ b/src/main/resources/static/v3/api-docs/swagger.yaml @@ -823,9 +823,13 @@ components: format: int64 label: type: string - created_at: + comment: + type: string + createdAt: type: string format: 'date-time' + totalNumberOfPatients: + type: integer Query: type: object required: diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfigIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfigIT.java new file mode 100644 index 00000000..453ca9db --- /dev/null +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfigIT.java @@ -0,0 +1,79 @@ +package de.numcodex.feasibility_gui_backend.query.broker.direct; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; + + +@ExtendWith(MockitoExtension.class) +public class DirectSpringConfigIT { + + private static final String USERNAME = "some-user-123"; + private static final String PASSWORD = "vALBAi95WW84x3"; + MockWebServer mockWebServer; + + private DirectSpringConfig directSpringConfig; + + @BeforeEach + void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + } + + @AfterEach + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @Test + void testDirectWebClientFlare_withCredentials() throws InterruptedException { + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("Foo")); + directSpringConfig = new DirectSpringConfig(true, String.format("http://localhost:%s", mockWebServer.getPort()), null, USERNAME, PASSWORD); + var authHeaderValue = "Basic " + Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes(StandardCharsets.UTF_8)); + + WebClient webClient = directSpringConfig.directWebClientFlare(); + + webClient + .get() + .uri("/foo") + .retrieve() + .bodyToMono(String.class) + .subscribe(responseBody -> { + }) + ; + var recordedRequest = mockWebServer.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo(authHeaderValue); + } + + @Test + void testDirectWebClientFlare_withoutCredentials() throws InterruptedException { + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("Foo")); + directSpringConfig = new DirectSpringConfig(true, String.format("http://localhost:%s", mockWebServer.getPort()), null, null, null); + + WebClient webClient = directSpringConfig.directWebClientFlare(); + + webClient + .get() + .uri("/foo") + .retrieve() + .bodyToMono(String.class) + .subscribe(responseBody -> { + }) + ; + var recordedRequest = mockWebServer.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); + } + +} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfigTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfigTest.java new file mode 100644 index 00000000..1cbb5c63 --- /dev/null +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/direct/DirectSpringConfigTest.java @@ -0,0 +1,100 @@ +package de.numcodex.feasibility_gui_backend.query.broker.direct; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; +import de.numcodex.feasibility_gui_backend.query.broker.BrokerClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest( + classes = DirectSpringConfig.class +) +class DirectSpringConfigTest { + + @Mock + private WebClient webClient; + + private final FhirContext fhirContext = FhirContext.forR4(); + + @Mock() + private FhirConnector fhirConnector; + + @Mock + private FhirHelper fhirHelper; + + private DirectSpringConfig directSpringConfig; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void directWebClientFlare_withCredentials() { + directSpringConfig = new DirectSpringConfig(true, "http://my.flare.url", null, "username", "password"); + + WebClient webClient = directSpringConfig.directWebClientFlare(); + + assertNotNull(webClient); + // Since there is no way to check whether the webclient has a auth filter set, see DirectSpringConfigIT.java for a check for that + } + + @Test + void directWebClientFlare_withoutCredentials() { + directSpringConfig = new DirectSpringConfig(true, "http://my.flare.url", null, null, null); + + WebClient webClient = directSpringConfig.directWebClientFlare(); + + assertNotNull(webClient); + // Since there is no way to check whether the webclient has a auth filter set, see DirectSpringConfigIT.java for a check for that + } + + @Test + void getFhirClient_withCredentials() { + directSpringConfig = new DirectSpringConfig(true, null, "http://my.fhir.url", "username", "password"); + + IGenericClient fhirClient = directSpringConfig.getFhirClient(fhirContext); + + assertNotNull(fhirClient); + Assertions.assertThat(fhirClient.getInterceptorService().getAllRegisteredInterceptors()) + .anySatisfy(interceptor -> Assertions.assertThat(interceptor).isInstanceOf(BasicAuthInterceptor.class)); + } + + @Test + void getFhirClient_withoutCredentials() { + directSpringConfig = new DirectSpringConfig(true, null, "http://my.fhir.url", null, null); + + IGenericClient fhirClient = directSpringConfig.getFhirClient(fhirContext); + + assertNotNull(fhirClient); + Assertions.assertThat(fhirClient.getInterceptorService().getAllRegisteredInterceptors()) + .noneSatisfy(interceptor -> Assertions.assertThat(interceptor).isInstanceOf(BasicAuthInterceptor.class)); + } + + @Test + void directBrokerClient_useCql() { + directSpringConfig = new DirectSpringConfig(true, null, null, null, null); + + BrokerClient brokerClient = directSpringConfig.directBrokerClient(webClient, false, fhirConnector, fhirHelper); + + assertInstanceOf(DirectBrokerClientCql.class, brokerClient); + } + + @Test + void directBrokerClient_useFlare() { + directSpringConfig = new DirectSpringConfig(false, null, null, null, null); + + BrokerClient brokerClient = directSpringConfig.directBrokerClient(webClient, false, fhirConnector, fhirHelper); + + assertInstanceOf(DirectBrokerClientFlare.class, brokerClient); + } + +} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProviderTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProviderTest.java new file mode 100644 index 00000000..804bb670 --- /dev/null +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/broker/dsf/DSFFhirWebClientProviderTest.java @@ -0,0 +1,61 @@ +package de.numcodex.feasibility_gui_backend.query.broker.dsf; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.fhir.client.FhirWebserviceClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +@ExtendWith(OutputCaptureExtension.class) +public class DSFFhirWebClientProviderTest { + + @Container + private GenericContainer blaze = new GenericContainer<>("samply/blaze:0.23.3") + .withExposedPorts(8080) + .withNetwork(Network.newNetwork()) + .withReuse(true); + + @Test + void settingLogRequestsFlagToTrueEnablesRequestLogs(CapturedOutput output) throws Exception { + String webserviceBaseUrl = format("http://%s:%s/fhir", blaze.getHost(), blaze.getFirstMappedPort()); + String websocketBaseUrl = format("ws://%s:%s/fhir", blaze.getHost(), blaze.getFirstMappedPort()); + FhirSecurityContextProvider securityContextProvider = () -> new FhirSecurityContext(null, null, null); + FhirProxyContext proxyContext = new FhirProxyContext(null, null, null); + DSFFhirWebClientProvider clientProvider = new DSFFhirWebClientProvider(FhirContext.forR4(), webserviceBaseUrl, + 20000, 2000, websocketBaseUrl, securityContextProvider, proxyContext, true); + FhirWebserviceClient client = clientProvider.provideFhirWebserviceClient(); + + client.getConformance(); + + assertThat(output) + .containsIgnoringCase("sending client request") + .containsIgnoringCase("client response received"); + } + + @Test + void settingLogRequestsFlagToFalseDisablesRequestLogs(CapturedOutput output) throws Exception { + String webserviceBaseUrl = format("http://%s:%s/fhir", blaze.getHost(), blaze.getFirstMappedPort()); + String websocketBaseUrl = format("ws://%s:%s/fhir", blaze.getHost(), blaze.getFirstMappedPort()); + FhirSecurityContextProvider securityContextProvider = () -> new FhirSecurityContext(null, null, null); + FhirProxyContext proxyContext = new FhirProxyContext(null, null, null); + DSFFhirWebClientProvider clientProvider = new DSFFhirWebClientProvider(FhirContext.forR4(), webserviceBaseUrl, + 20000, 2000, websocketBaseUrl, securityContextProvider, proxyContext, false); + FhirWebserviceClient client = clientProvider.provideFhirWebserviceClient(); + + client.getConformance(); + + assertThat(output) + .doesNotContainIgnoringCase("sending client request") + .doesNotContainIgnoringCase("client response received"); + } + +}