diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db7ac60..68f5d43 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,7 @@ version: 2 + updates: + - package-ecosystem: "gradle" directory: "/" schedule: @@ -14,4 +16,13 @@ updates: - "*" update-types: - "minor" - - "patch" \ No newline at end of file + - "patch" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + reviewers: + - "Jorich" + - "pr11t" + - "rammrain" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63dba0b..f624059 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,10 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + java: [ '17', '21' ] + name: Test with Java ${{ matrix.Java }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -19,15 +23,18 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: ${{ matrix.java }} - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: 8.6 - name: Run tests and generate reports run: ./gradlew testAndReport - name: Run Sonar analysis + if: matrix.Java == '17' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -36,7 +43,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: report + name: report-java-${{ matrix.Java }} path: build/reports/** retention-days: 5 diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index b87355b..676461f 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -9,14 +9,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: - gradle-version: 8.5 + gradle-version: 8.6 - name: Publish packages env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} diff --git a/build.gradle b/build.gradle index 832eb04..a3f0bdb 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ plugins { } group 'ee.bitweb' -version '3.2.0' +version '3.3.0' java { sourceCompatibility = '17' } diff --git a/src/main/java/ee/bitweb/core/trace/thread/MDCTaskDecorator.java b/src/main/java/ee/bitweb/core/trace/thread/MDCTaskDecorator.java index 5ecea2b..9910deb 100644 --- a/src/main/java/ee/bitweb/core/trace/thread/MDCTaskDecorator.java +++ b/src/main/java/ee/bitweb/core/trace/thread/MDCTaskDecorator.java @@ -1,33 +1,14 @@ package ee.bitweb.core.trace.thread; -import lombok.RequiredArgsConstructor; -import org.slf4j.MDC; -import org.springframework.core.task.TaskDecorator; -import org.springframework.security.core.context.SecurityContextHolder; +import ee.bitweb.core.trace.thread.decorator.SecurityAwareMDCTaskDecorator; -import java.util.Map; +/** + * @deprecated use BasicMDCTaskDecorator or SecurityAwareMDCTaskDecorator + */ +@Deprecated(since = "3.3.0", forRemoval = true) +public class MDCTaskDecorator extends SecurityAwareMDCTaskDecorator { -@RequiredArgsConstructor -public class MDCTaskDecorator implements TaskDecorator { - - private final ThreadTraceIdResolver resolver; - - @Override - public Runnable decorate(Runnable runnable) { - Map contextMap = MDC.getCopyOfContextMap(); - var securityContext = SecurityContextHolder.getContext(); - - return () -> { - try { - MDC.setContextMap(contextMap); - SecurityContextHolder.setContext(securityContext); - resolver.resolve(); - - runnable.run(); - } finally { - MDC.clear(); - SecurityContextHolder.clearContext(); - } - }; + public MDCTaskDecorator(ThreadTraceIdResolver resolver) { + super(resolver); } } diff --git a/src/main/java/ee/bitweb/core/trace/thread/decorator/BasicMDCTaskDecorator.java b/src/main/java/ee/bitweb/core/trace/thread/decorator/BasicMDCTaskDecorator.java new file mode 100644 index 0000000..e48ded4 --- /dev/null +++ b/src/main/java/ee/bitweb/core/trace/thread/decorator/BasicMDCTaskDecorator.java @@ -0,0 +1,31 @@ +package ee.bitweb.core.trace.thread.decorator; + +import ee.bitweb.core.trace.thread.ThreadTraceIdResolver; +import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +public class BasicMDCTaskDecorator implements TaskDecorator { + + private final ThreadTraceIdResolver resolver; + + @Override + public Runnable decorate(Runnable runnable) { + final Map contextMap = MDC.getCopyOfContextMap(); + + return () -> { + try { + MDC.setContextMap(contextMap == null ? new HashMap<>() : contextMap); + resolver.resolve(); + + runnable.run(); + } finally { + MDC.clear(); + } + }; + } +} diff --git a/src/main/java/ee/bitweb/core/trace/thread/decorator/SecurityAwareMDCTaskDecorator.java b/src/main/java/ee/bitweb/core/trace/thread/decorator/SecurityAwareMDCTaskDecorator.java new file mode 100644 index 0000000..f0aff57 --- /dev/null +++ b/src/main/java/ee/bitweb/core/trace/thread/decorator/SecurityAwareMDCTaskDecorator.java @@ -0,0 +1,36 @@ +package ee.bitweb.core.trace.thread.decorator; + +import ee.bitweb.core.trace.thread.ThreadTraceIdResolver; +import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +public class SecurityAwareMDCTaskDecorator implements TaskDecorator { + + private final ThreadTraceIdResolver resolver; + + @Override + public Runnable decorate(Runnable runnable) { + final Map contextMap = MDC.getCopyOfContextMap(); + final SecurityContext securityContext = SecurityContextHolder.getContext(); + + return () -> { + try { + MDC.setContextMap(contextMap == null ? new HashMap<>() : contextMap); + SecurityContextHolder.setContext(securityContext == null ? SecurityContextHolder.createEmptyContext() : securityContext); + resolver.resolve(); + + runnable.run(); + } finally { + MDC.clear(); + SecurityContextHolder.clearContext(); + } + }; + } +} diff --git a/src/test/java/ee/bitweb/core/trace/thread/decorator/BasicMDCTaskDecoratorTest.java b/src/test/java/ee/bitweb/core/trace/thread/decorator/BasicMDCTaskDecoratorTest.java new file mode 100644 index 0000000..7d42f61 --- /dev/null +++ b/src/test/java/ee/bitweb/core/trace/thread/decorator/BasicMDCTaskDecoratorTest.java @@ -0,0 +1,66 @@ +package ee.bitweb.core.trace.thread.decorator; + +import ee.bitweb.core.trace.thread.ThreadTraceIdResolver; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockSettings; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.MDC; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +@ExtendWith(MockitoExtension.class) +class BasicMDCTaskDecoratorTest { + + @Mock + private ThreadTraceIdResolver resolver; + + @Test + void testMDCIsPopulatedAndCleared() { + Mockito.when(resolver.resolve()).thenReturn(null); + MDC.put("custom-key", "custom-value"); + + new BasicMDCTaskDecorator(resolver).decorate(() -> { + assertEquals("custom-value", MDC.get("custom-key")); + }).run(); + + assertNull(MDC.get("custom-key")); + + Mockito.verifyNoMoreInteractions(resolver); + } + + @Test + void testMDCIsPopulatedAndClearedWhenExceptionIsThrown() { + Mockito.when(resolver.resolve()).thenReturn(null); + MDC.put("custom-key", "custom-value"); + + try { + new BasicMDCTaskDecorator(resolver).decorate(() -> { + assertEquals("custom-value", MDC.get("custom-key")); + + throw new RuntimeException(); + }).run(); + } catch (RuntimeException ignored) { + assertNull(MDC.get("custom-key")); + } + + Mockito.verifyNoMoreInteractions(resolver); + } + + @Test + void testMDCIsPopulatedGivenMDCContextMapIsNull() { + Mockito.when(resolver.resolve()).thenReturn(null); + assertNull(MDC.getCopyOfContextMap()); + + new BasicMDCTaskDecorator(resolver).decorate(() -> { + assertNotNull(MDC.getCopyOfContextMap()); + }).run(); + + Mockito.verifyNoMoreInteractions(resolver); + } +} diff --git a/src/test/java/ee/bitweb/core/trace/thread/decorator/SecurityAwareMDCTaskDecoratorTest.java b/src/test/java/ee/bitweb/core/trace/thread/decorator/SecurityAwareMDCTaskDecoratorTest.java new file mode 100644 index 0000000..22120d6 --- /dev/null +++ b/src/test/java/ee/bitweb/core/trace/thread/decorator/SecurityAwareMDCTaskDecoratorTest.java @@ -0,0 +1,86 @@ +package ee.bitweb.core.trace.thread.decorator; + +import ee.bitweb.core.trace.thread.ThreadTraceIdResolver; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.MDC; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +@ExtendWith(MockitoExtension.class) +class SecurityAwareMDCTaskDecoratorTest { + + @Mock + private ThreadTraceIdResolver resolver; + + @Test + void testMDCIsPopulatedAndCleared() { + Mockito.when(resolver.resolve()).thenReturn(null); + MDC.put("custom-key", "custom-value"); + + new SecurityAwareMDCTaskDecorator(resolver).decorate(() -> { + assertEquals("custom-value", MDC.get("custom-key")); + }).run(); + + assertNull(MDC.get("custom-key")); + + Mockito.verifyNoMoreInteractions(resolver); + } + + @Test + void testMDCIsPopulatedAndClearedWhenExceptionIsThrown() { + Mockito.when(resolver.resolve()).thenReturn(null); + MDC.put("custom-key", "custom-value"); + + try { + new SecurityAwareMDCTaskDecorator(resolver).decorate(() -> { + assertEquals("custom-value", MDC.get("custom-key")); + + throw new RuntimeException(); + }).run(); + } catch (RuntimeException ignored) { + assertNull(MDC.get("custom-key")); + } + + Mockito.verifyNoMoreInteractions(resolver); + } + + @Test + void testMDCIsPopulatedGivenMDCContextMapIsNull() { + Mockito.when(resolver.resolve()).thenReturn(null); + assertNull(MDC.getCopyOfContextMap()); + + new SecurityAwareMDCTaskDecorator(resolver).decorate(() -> { + assertNotNull(MDC.getCopyOfContextMap()); + }).run(); + + Mockito.verifyNoMoreInteractions(resolver); + } + + @Test + void testDecorateCanHandleNullSecurityContext() { + Mockito.when(resolver.resolve()).thenReturn(null); + + Runnable task; + + try (MockedStatic holder = Mockito.mockStatic(SecurityContextHolder.class)) { + holder.when(SecurityContextHolder::getContext).thenReturn(null); + + task = new SecurityAwareMDCTaskDecorator(resolver).decorate(() -> { + assertNotNull(SecurityContextHolder.getContext()); + }); + } + + task.run(); + + Mockito.verifyNoMoreInteractions(resolver); + } +}