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");
+ }
+
+}