From a9ad7f077bd65777e658977525dcacabf5dae1ac Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Sun, 10 Aug 2025 12:21:18 +0200 Subject: [PATCH] Create AuditWebFilterTest.java --- .../proprietary/web/AuditWebFilterTest.java | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 app/proprietary/src/test/java/stirling/software/proprietary/web/AuditWebFilterTest.java diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/web/AuditWebFilterTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/web/AuditWebFilterTest.java new file mode 100644 index 000000000..10a9636b2 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/web/AuditWebFilterTest.java @@ -0,0 +1,319 @@ +package stirling.software.proprietary.web; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.slf4j.MDC; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + * Tests for {@link AuditWebFilter}. + * + *

Note: The filter clears the MDC in its finally block. Therefore we capture the MDC values + * inside a special FilterChain before the clear happens (snapshot). + */ +class AuditWebFilterTest { + + private AuditWebFilter filter; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + /** Small helper chain that captures MDC values during the chain invocation. */ + static class CapturingFilterChain implements FilterChain { + final Map captured = new HashMap<>(); + boolean called = false; + + @Override + public void doFilter(ServletRequest req, ServletResponse res) + throws IOException, ServletException { + called = true; + // Snapshot of the MDC keys set by the filter (before the finally-clear) + captured.put("userAgent", MDC.get("userAgent")); + captured.put("referer", MDC.get("referer")); + captured.put("acceptLanguage", MDC.get("acceptLanguage")); + captured.put("contentType", MDC.get("contentType")); + captured.put("userRoles", MDC.get("userRoles")); + captured.put("queryParams", MDC.get("queryParams")); + } + } + + /** Variant that intentionally throws an exception after capturing. */ + static class ThrowingAfterCaptureChain extends CapturingFilterChain { + @Override + public void doFilter(ServletRequest req, ServletResponse res) + throws IOException, ServletException { + super.doFilter(req, res); + throw new IOException("Test Exception"); + } + } + + @BeforeEach + void setUp() { + filter = new AuditWebFilter(); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + MDC.clear(); + SecurityContextHolder.clearContext(); + } + + @AfterEach + void tearDown() { + MDC.clear(); + SecurityContextHolder.clearContext(); + } + + @Nested + @DisplayName("Header and query parameter handling") + class HeaderAndQueryTests { + + @Test + @DisplayName("Should store all provided headers and query parameters in MDC") + void shouldStoreHeadersAndQueryParamsInMdc() throws ServletException, IOException { + request.addHeader("User-Agent", "JUnit-Test-Agent"); + request.addHeader("Referer", "http://example.com"); + request.addHeader("Accept-Language", "de-DE"); + request.addHeader("Content-Type", "application/json"); + request.setParameter("param1", "value1"); + request.setParameter("param2", "value2"); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertTrue(chain.called, "FilterChain should have been called"); + assertEquals("JUnit-Test-Agent", chain.captured.get("userAgent")); + assertEquals("http://example.com", chain.captured.get("referer")); + assertEquals("de-DE", chain.captured.get("acceptLanguage")); + assertEquals("application/json", chain.captured.get("contentType")); + String params = chain.captured.get("queryParams"); + assertNotNull(params); + assertTrue(params.contains("param1")); + assertTrue(params.contains("param2")); + + assertNull(MDC.get("userAgent")); + assertNull(MDC.get("queryParams")); + } + + @Test + @DisplayName("Should only store present headers and set nothing for empty inputs") + void shouldNotStoreNullHeaders() throws ServletException, IOException { + request.setParameter("onlyParam", "123"); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertNull(chain.captured.get("userAgent")); + assertNull(chain.captured.get("referer")); + assertNull(chain.captured.get("acceptLanguage")); + assertNull(chain.captured.get("contentType")); + assertEquals("onlyParam", chain.captured.get("queryParams")); + } + + // New: empty parameter map case (branch: parameterMap != null && !isEmpty() -> false) + @Test + @DisplayName("Should not set queryParams when parameter map is empty") + void shouldNotStoreQueryParamsWhenEmpty() throws ServletException, IOException { + // no request.setParameter(...) + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertNull( + chain.captured.get("queryParams"), + "With an empty map, queryParams must not be set"); + } + + // New: parameterMap == null (branch: parameterMap != null -> false) + @Test + @DisplayName("Should handle getParameterMap() returning null safely") + void shouldHandleNullParameterMapSafely() throws ServletException, IOException { + MockHttpServletRequest reqWithNullParamMap = + new MockHttpServletRequest() { + @Override + public Map getParameterMap() { + // Assumption: defensive branch in the filter; simulate a broken/unusual + // implementation + return null; + } + }; + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(reqWithNullParamMap, response, chain); + + assertNull( + chain.captured.get("queryParams"), + "With a null parameter map, queryParams must not be set"); + } + } + + @Nested + @DisplayName("Authenticated users") + class AuthenticatedUserTests { + + @Test + @DisplayName("Should store roles of the authenticated user") + void shouldStoreUserRolesInMdc() throws ServletException, IOException { + SecurityContextHolder.getContext() + .setAuthentication( + new UsernamePasswordAuthenticationToken( + "user", + "pass", + Collections.singletonList( + new SimpleGrantedAuthority("ROLE_ADMIN")))); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertEquals("ROLE_ADMIN", chain.captured.get("userRoles")); + assertNull(MDC.get("userRoles")); + } + + @Test + @DisplayName("Should store multiple roles comma-separated") + void shouldStoreMultipleRolesCommaSeparated() throws ServletException, IOException { + SecurityContextHolder.getContext() + .setAuthentication( + new UsernamePasswordAuthenticationToken( + "user", + "pass", + List.of( + new SimpleGrantedAuthority("ROLE_USER"), + new SimpleGrantedAuthority("ROLE_ADMIN")))); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + String roles = chain.captured.get("userRoles"); + assertNotNull(roles, "Roles should be set"); + assertTrue(roles.contains("ROLE_USER")); + assertTrue(roles.contains("ROLE_ADMIN")); + assertTrue(roles.contains(","), "Roles should be separated by a comma"); + } + + // New: auth == null (branch: auth != null -> false) + @Test + @DisplayName("Should not set userRoles when no Authentication object is present") + void shouldNotStoreUserRolesWhenAuthIsNull() throws ServletException, IOException { + // SecurityContext remains empty + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertNull(chain.captured.get("userRoles")); + } + + // New: authorities == null (branch: auth != null && authorities != null -> false) + @Test + @DisplayName("Should not set userRoles when authorities are null") + void shouldNotStoreUserRolesWhenAuthoritiesIsNull() throws ServletException, IOException { + Authentication authWithNullAuthorities = + new Authentication() { + @Override + public Collection getAuthorities() { + return null; // important + } + + @Override + public Object getCredentials() { + return "cred"; + } + + @Override + public Object getDetails() { + return null; + } + + @Override + public Object getPrincipal() { + return "user"; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) + throws IllegalArgumentException {} + + @Override + public String getName() { + return "user"; + } + }; + SecurityContextHolder.getContext().setAuthentication(authWithNullAuthorities); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertNull( + chain.captured.get("userRoles"), + "With null authorities, userRoles must not be set"); + } + + // New: empty authorities list -> reduce(...).orElse("") → empty string is set + @Test + @DisplayName("Should set empty string when authorities list is empty") + void shouldStoreEmptyStringWhenAuthoritiesEmpty() throws ServletException, IOException { + SecurityContextHolder.getContext() + .setAuthentication( + new UsernamePasswordAuthenticationToken( + "user", "pass", Collections.emptyList())); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertEquals( + "", + chain.captured.get("userRoles"), + "With an empty roles list, an empty string should be set"); + } + } + + @Nested + @DisplayName("MDC cleanup logic") + class MdcCleanupTests { + + @Test + @DisplayName("Should clear MDC after processing") + void shouldClearMdcAfterProcessing() throws ServletException, IOException { + request.addHeader("User-Agent", "JUnit-Test-Agent"); + + CapturingFilterChain chain = new CapturingFilterChain(); + filter.doFilterInternal(request, response, chain); + + assertEquals("JUnit-Test-Agent", chain.captured.get("userAgent")); + assertNull(MDC.get("userAgent"), "MDC should be cleared after processing"); + } + + @Test + @DisplayName("Should clear MDC even when the FilterChain throws") + void shouldClearMdcOnException() throws ServletException, IOException { + request.addHeader("User-Agent", "JUnit-Test-Agent"); + ThrowingAfterCaptureChain chain = new ThrowingAfterCaptureChain(); + + IOException thrown = + assertThrows( + IOException.class, + () -> filter.doFilterInternal(request, response, chain)); + + assertEquals("Test Exception", thrown.getMessage()); + assertEquals("JUnit-Test-Agent", chain.captured.get("userAgent")); + assertNull(MDC.get("userAgent"), "MDC should also be cleared after exceptions"); + } + } +}