1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-01 06:02:54 +00:00

fix: added missing gIcs connection status to configuration view * also small refactoring

This commit is contained in:
Jakub Lidke
2025-05-14 09:22:59 +02:00
parent 77df6f38ec
commit d991f2a94d
11 changed files with 179 additions and 36 deletions

View File

@ -96,7 +96,7 @@ Falls in diesem Fall die Statusprüfung fehlschlägt, wird Status **abgelehnt**
Ist die Prüfung über gIcs deaktiviert, wird der eingetragene Einwilligungsstatus der übermittelten MTB Datei geprüft.
* `APP_CONSENT_GICS_ENABLED`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn nicht gesetzt.
* `APP_CONSENT_GICS_GICSBASEURI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`)
* `APP_CONSENT_GICS_URI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`)
* `APP_CONSENT_GICS_USERNAME`: gIcs Basic-Auth Benutzername
* `APP_CONSENT_GICS_PASSWORD`: gIcs Basic-Auth Passwort
* `APP_CONSENT_GICS_PERSONIDENTIFIERSYSTEM`: Derzeit wird nur die PID unterstützt. wenn leer wird `https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID` angenommen
@ -423,4 +423,4 @@ Die Datei `application-dev.yml` enthält hierzu die Konfiguration für das Profi
Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet.
Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`.
Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.
Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.

View File

@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.config.AppConfiguration
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
import dev.dnpm.etl.processor.output.MtbFileSender
@ -89,7 +90,8 @@ abstract class MockSink : Sinks.Many<Boolean>
RequestProcessor::class,
TransformationService::class,
GPasConnectionCheckService::class,
RestConnectionCheckService::class
RestConnectionCheckService::class,
GIcsConnectionCheckService::class
]
)
class ConfigControllerTest {
@ -182,7 +184,13 @@ class ConfigControllerTest {
@Test
fun testShouldNotSaveTokenWithExstingName() {
whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure")))
whenever(tokenService.addToken(anyString())).thenReturn(
Result.failure(
RuntimeException(
"Testfailure"
)
)
)
mockMvc.post("/configs/tokens") {
with(user("admin").roles("ADMIN"))
@ -303,7 +311,10 @@ class ConfigControllerTest {
val idCaptor = argumentCaptor<Long>()
val roleCaptor = argumentCaptor<Role>()
verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture())
verify(userRoleService, times(1)).updateUserRole(
idCaptor.capture(),
roleCaptor.capture()
)
assertThat(idCaptor.firstValue).isEqualTo(42)
assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
@ -341,23 +352,26 @@ class ConfigControllerTest {
@BeforeEach
fun setup(
applicationContext: WebApplicationContext,
applicationContext: WebApplicationContext
) {
this.webClient = MockMvcWebTestClient
.bindToApplicationContext(applicationContext).build()
}
@Test
fun testShouldRequestSSE() {
val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
fun testShouldRequestGPasSSE() {
val expectedEvent =
ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange()
.expectStatus().isOk()
.expectHeader().contentType(TEXT_EVENT_STREAM)
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
val result =
webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(TEXT_EVENT_STREAM)
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
StepVerifier.create(result.responseBody)
.expectNext(expectedEvent)

View File

@ -1,7 +1,16 @@
package dev.dnpm.etl.processor.consent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConsentCheckFileBased implements ICheckConsent{
private static final Logger log = LoggerFactory.getLogger(ConsentCheckFileBased.class);
public ConsentCheckFileBased() {
log.info("ConsentCheckFileBased initialized...");
}
@Override
public TtpConsentStatus getTtpConsentStatus(String personIdentifierValue) {
return TtpConsentStatus.UNKNOWN_CHECK_FILE;

View File

@ -49,11 +49,12 @@ public class GicsConsentService implements ICheckConsent {
this.fhirContext = appFhirConfig.fhirContext();
httpHeader = buildHeader(gIcsConfigProperties.getUsername(),
gIcsConfigProperties.getPassword());
log.info("GicsConsentService initialized...");
}
public String getGicsUri() {
if (url == null) {
final String gIcsBaseUri = gIcsConfigProperties.getGIcsBaseUri();
final String gIcsBaseUri = gIcsConfigProperties.getUri();
if (StringUtils.isBlank(gIcsBaseUri)) {
throw new IllegalArgumentException(
"gICS base URL is empty - should call gICS with false configuration.");

View File

@ -62,7 +62,7 @@ data class GIcsConfigProperties(
* Base URL to gICS System
*
*/
val gIcsBaseUri: String?,
val uri: String?,
val username: String?,
val password: String?,

View File

@ -23,10 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.consent.ConsentCheckFileBased
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.GicsConsentService
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.*
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
@ -180,15 +177,9 @@ class AppConfiguration {
return AppJdbcConfiguration()
}
@Bean
@ConditionalOnMissingBean
fun constService(): ICheckConsent {
return ConsentCheckFileBased()
}
@Bean
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
fun gicsAccessConsent( gIcsConfigProperties: GIcsConfigProperties,
fun gicsConsentService( gIcsConfigProperties: GIcsConfigProperties,
retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): ICheckConsent {
return GicsConsentService(
gIcsConfigProperties,
@ -197,5 +188,21 @@ class AppConfiguration {
appFhirConfig
)
}
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
@Bean
fun gIcsConnectionCheckService(
restTemplate: RestTemplate,
gIcsConfigProperties: GIcsConfigProperties,
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
): ConnectionCheckService {
return GIcsConnectionCheckService(restTemplate, gIcsConfigProperties, connectionCheckUpdateProducer)
}
@Bean
@ConditionalOnMissingBean
fun constService(): ICheckConsent {
return ConsentCheckFileBased()
}
}

View File

@ -20,6 +20,7 @@
package dev.dnpm.etl.processor.monitoring
import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.config.GPasConfigProperties
import dev.dnpm.etl.processor.config.RestTargetProperties
import jakarta.annotation.PostConstruct
@ -68,6 +69,12 @@ sealed class ConnectionCheckResult {
override val timestamp: Instant,
override val lastChange: Instant
) : ConnectionCheckResult()
data class GIcsConnectionCheckResult(
override val available: Boolean,
override val timestamp: Instant,
override val lastChange: Instant
) : ConnectionCheckResult()
}
class KafkaConnectionCheckService(
@ -207,4 +214,57 @@ class GPasConnectionCheckService(
override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
return this.result
}
}
class GIcsConnectionCheckService(
private val restTemplate: RestTemplate,
private val gIcsConfigProperties: GIcsConfigProperties,
@Qualifier("connectionCheckUpdateProducer")
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : ConnectionCheckService {
private var result = ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
result = try {
val uri = UriComponentsBuilder.fromUriString(
gIcsConfigProperties.uri.toString()).path("/metadata").build().toUri()
val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_JSON
if (!gIcsConfigProperties.username.isNullOrBlank() && !gIcsConfigProperties.password.isNullOrBlank()) {
headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password)
}
val available = restTemplate.exchange(
uri,
HttpMethod.GET,
HttpEntity<Void>(headers),
Void::class.java
).statusCode == HttpStatus.OK
ConnectionCheckResult.GIcsConnectionCheckResult(
available,
Instant.now(),
if (result.available == available) { result.lastChange } else { Instant.now() }
)
} catch (_: Exception) {
ConnectionCheckResult.GIcsConnectionCheckResult(
false,
Instant.now(),
if (!result.available) { result.lastChange } else { Instant.now() }
)
}
connectionCheckUpdateProducer.emitNext(
result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult {
return this.result
}
}

View File

@ -19,10 +19,7 @@
package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
import dev.dnpm.etl.processor.monitoring.OutputConnectionCheckService
import dev.dnpm.etl.processor.monitoring.*
import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.security.Role
@ -61,11 +58,15 @@ class ConfigController(
val gPasConnectionAvailable =
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
val gIcsConnectionAvailable =
connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName)
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
model.addAttribute("outputConnectionAvailable", outputConnectionAvailable)
model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable)
model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
model.addAttribute("tokensEnabled", tokenService != null)
if (tokenService != null) {
model.addAttribute("tokens", tokenService.findAll())
@ -119,6 +120,24 @@ class ConfigController(
return "configs/gPasConnectionAvailable"
}
@GetMapping(params = ["gIcsConnectionAvailable"])
fun gIcsConnectionAvailable(model: Model): String {
val gIcsConnectionAvailable =
connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
if (tokenService != null) {
model.addAttribute("tokensEnabled", true)
model.addAttribute("tokens", tokenService.findAll())
} else {
model.addAttribute("tokens", listOf<Token>())
}
return "configs/gIcsConnectionAvailable"
}
@PostMapping(path = ["tokens"])
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
if (tokenService == null) {
@ -190,6 +209,7 @@ class ConfigController(
is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check"
is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check"
is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check"
is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check"
}
ServerSentEvent.builder<Any>()

View File

@ -49,6 +49,11 @@
</div>
</section>
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
<div th:insert="~{configs/gIcsConnectionAvailable.html}" th:hx-get="@{/configs?gIcsConnectionAvailable}" hx-trigger="sse:gics-connection-check">
</div>
</section>
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
<div th:insert="~{configs/outputConnectionAvailable.html}" th:hx-get="@{/configs?outputConnectionAvailable}" hx-trigger="sse:output-connection-check">
</div>

View File

@ -0,0 +1,24 @@
<th:block th:if="${gIcsConnectionAvailable == null}">
<h2><span>🟦</span> gICS nicht konfiguriert - Einwilligung wird über Dateiinhalt geprüft</h2>
</th:block>
<th:block th:if="${gIcsConnectionAvailable != null}">
<h2><span th:if="${gIcsConnectionAvailable.available}"></span><span th:if="${not(gIcsConnectionAvailable.available)}"></span> Verbindung zu gICS</h2>
<div>
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gIcsConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gIcsConnectionAvailable.timestamp)}"></time>
&nbsp;|&nbsp;
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gIcsConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gIcsConnectionAvailable.lastChange)}"></time>
</div>
<div>
<span>Die Verbindung ist aktuell</span>
<strong th:if="${gIcsConnectionAvailable.available}" style="color: green">verfügbar.</strong>
<strong th:if="${not(gIcsConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
</div>
<div class="connection-display border">
<img th:src="@{/server.png}" alt="ETL-Processor" />
<span class="connection" th:classappend="${gIcsConnectionAvailable.available ? 'available' : ''}"></span>
<img th:src="@{/server.png}" alt="gICS" />
<span>ETL-Processor</span>
<span></span>
<span>gICS</span>
</div>
</th:block>

View File

@ -3,6 +3,7 @@ package dev.dnpm.etl.processor.consent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.dnpm.etl.processor.config.AppConfiguration;
import dev.dnpm.etl.processor.config.AppFhirConfig;
@ -23,19 +24,19 @@ import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.client.MockRestServiceServer;
@ContextConfiguration(classes = {GicsConsentService.class,
AppConfiguration.class, ObjectMapper.class})
@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
@TestPropertySource(properties = {"app.consent.gics.enabled=true",
"app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"})
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"})
@RestClientTest
public class GicsConsentServiceTest {
public static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics";
@Autowired
MockRestServiceServer mockRestServiceServer;
@Autowired
GicsConsentService gicsConsentService;
@Autowired
AppConfiguration appConfiguration;
@ -45,6 +46,7 @@ public class GicsConsentServiceTest {
@BeforeEach
public void setUp() {
mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate());
}
@Test
@ -54,7 +56,8 @@ public class GicsConsentServiceTest {
.setValue(new BooleanType().setValue(true)));
mockRestServiceServer.expect(
requestTo("http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
requestTo("http://localhost:8090/ttp-fhir/fhir/gics"
+ GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
withSuccess(appFhirConfig.fhirContext().newJsonParser()
.encodeResourceToString(responseConsented),
MediaType.APPLICATION_JSON));
@ -63,7 +66,6 @@ public class GicsConsentServiceTest {
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENTED);
}
@Test
void consentRevoced() {
final Parameters responseRevoced = new Parameters().addParameter(
@ -71,7 +73,8 @@ public class GicsConsentServiceTest {
.setValue(new BooleanType().setValue(false)));
mockRestServiceServer.expect(
requestTo("http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
requestTo("http://localhost:8090/ttp-fhir/fhir/gics"
+ GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
withSuccess(appFhirConfig.fhirContext().newJsonParser()
.encodeResourceToString(responseRevoced),
MediaType.APPLICATION_JSON));