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