From e5316748e650727f70702d6d173679d1c892bd8c Mon Sep 17 00:00:00 2001 From: OZGCloud <ozgcloud@mgm-tp.com> Date: Wed, 4 Dec 2024 09:42:35 +0100 Subject: [PATCH] OZG-7092 add call context interceptor --- token-checker-server/pom.xml | 2 +- .../CallContextGrpcServerInterceptor.java | 95 +++++++++ .../CallContextGrpcServerInterceptorTest.java | 201 ++++++++++++++++++ 3 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java create mode 100644 token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java diff --git a/token-checker-server/pom.xml b/token-checker-server/pom.xml index 3ca6cc5..185d1cc 100644 --- a/token-checker-server/pom.xml +++ b/token-checker-server/pom.xml @@ -26,7 +26,7 @@ <parent> <groupId>de.ozgcloud.common</groupId> <artifactId>ozgcloud-common-parent</artifactId> - <version>4.6.0</version> + <version>4.7.0-SNAPSHOT</version> <relativePath/> </parent> diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java b/token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java new file mode 100644 index 0000000..b89d4fe --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.token.common; + +import java.util.UUID; + +import org.apache.logging.log4j.CloseableThreadContext; + +import de.ozgcloud.common.grpc.GrpcUtil; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import lombok.RequiredArgsConstructor; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +@GrpcGlobalServerInterceptor +@RequiredArgsConstructor +class CallContextGrpcServerInterceptor implements ServerInterceptor { + + @Override + public <A, B> Listener<A> interceptCall(ServerCall<A, B> call, Metadata headers, ServerCallHandler<A, B> next) { + return new LogContextSettingListener<>(next.startCall(call, headers), headers); + } + + class LogContextSettingListener<A> extends ForwardingServerCallListener.SimpleForwardingServerCallListener<A> { + + private final String requestId; + + public LogContextSettingListener(Listener<A> delegate, Metadata headers) { + super(delegate); + this.requestId = getRequestId(headers); + } + + String getRequestId(Metadata headers) { + return GrpcUtil.getRequestId(headers).orElseGet(() -> UUID.randomUUID().toString()); + } + + @Override + public void onMessage(A message) { + doSurroundOn(() -> super.onMessage(message)); + } + + @Override + public void onHalfClose() { + doSurroundOn(super::onHalfClose); + } + + @Override + public void onCancel() { + doSurroundOn(super::onCancel); + } + + @Override + public void onComplete() { + doSurroundOn(super::onComplete); + } + + @Override + public void onReady() { + doSurroundOn(super::onReady); + } + + void doSurroundOn(Runnable runnable) { + try (var ctc = CloseableThreadContext.put(GrpcUtil.KEY_REQUEST_ID, requestId)) { + runnable.run(); + } + } + + } + +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java new file mode 100644 index 0000000..6142e27 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.token.common; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; +import java.util.UUID; + +import org.apache.logging.log4j.CloseableThreadContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.util.ReflectionTestUtils; + +import de.ozgcloud.common.grpc.GrpcUtil; +import io.grpc.Metadata; +import io.grpc.ServerCall; + +class CallContextGrpcServerInterceptorTest { + + @InjectMocks + private CallContextGrpcServerInterceptor interceptor; + + @Mock + private ServerCall.Listener<RequestTest> callListener; + @Mock + private Metadata metadata; + + private CallContextGrpcServerInterceptor.LogContextSettingListener<RequestTest> listener; + + @BeforeEach + void init() { + listener = spy(interceptor.new LogContextSettingListener(callListener, metadata)); + } + + @Nested + class TestGetRequestId { + + @Test + void shouldCallGetRequestId() { + try (var grpcUtilMock = mockStatic(GrpcUtil.class)) { + listener.getRequestId(metadata); + + grpcUtilMock.verify(() -> GrpcUtil.getRequestId(metadata)); + } + } + + @Test + void shouldGetRequestIdFromMetadata() { + try (var grpcUtilMock = mockStatic(GrpcUtil.class)) { + var requestId = UUID.randomUUID().toString(); + grpcUtilMock.when(() -> GrpcUtil.getRequestId(any())).thenReturn(Optional.of(requestId)); + + var result = listener.getRequestId(metadata); + + assertThat(result).isEqualTo(requestId); + } + } + + @Test + void shouldReturnGeneratedId() { + try (var grpcUtilMock = mockStatic(GrpcUtil.class)) { + grpcUtilMock.when(() -> GrpcUtil.getRequestId(any())).thenReturn(Optional.empty()); + + var result = listener.getRequestId(metadata); + + assertThat(result).isNotEmpty(); + } + } + } + + @Nested + class TestOnMessage { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onMessage(new RequestTest()); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnHalfClose { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onHalfClose(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnCancel { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onCancel(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnComplete { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onComplete(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnReady { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onReady(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestDoSurroundOn { + + @Mock + private Runnable runnable; + + private String requestId; + + @BeforeEach + void init() { + requestId = (String) ReflectionTestUtils.getField(listener, "requestId"); + } + + @Test + void shouldSetThreadContext() { + try (var contextMock = mockStatic(CloseableThreadContext.class)) { + doSurroundOn(); + + contextMock.verify(() -> CloseableThreadContext.put(GrpcUtil.KEY_REQUEST_ID, requestId)); + } + } + + @Test + void shouldExecuteRunnable() { + doSurroundOn(); + + verify(runnable).run(); + } + + private void doSurroundOn() { + listener.doSurroundOn(runnable); + } + } + + private record RequestTest() { + } + + private record ResponseTest() { + } +} \ No newline at end of file -- GitLab