From 00db1ccf951d4cb57c6a514fe71f55bb646e7425 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Wed, 9 Jul 2025 16:44:40 +0100 Subject: [PATCH] Added tests --- .../JWTAuthenticationEntryPointTest.java | 42 +++ .../filter/JWTAuthenticationFilterTest.java | 301 ++++++++++++++++++ .../security/service/JWTServiceTest.java | 287 +++++++++++++++++ 3 files changed, 630 insertions(+) create mode 100644 proprietary/src/test/java/stirling/software/proprietary/security/JWTAuthenticationEntryPointTest.java create mode 100644 proprietary/src/test/java/stirling/software/proprietary/security/filter/JWTAuthenticationFilterTest.java create mode 100644 proprietary/src/test/java/stirling/software/proprietary/security/service/JWTServiceTest.java diff --git a/proprietary/src/test/java/stirling/software/proprietary/security/JWTAuthenticationEntryPointTest.java b/proprietary/src/test/java/stirling/software/proprietary/security/JWTAuthenticationEntryPointTest.java new file mode 100644 index 000000000..50a7e0442 --- /dev/null +++ b/proprietary/src/test/java/stirling/software/proprietary/security/JWTAuthenticationEntryPointTest.java @@ -0,0 +1,42 @@ +package stirling.software.proprietary.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.AuthenticationException; + +import java.io.IOException; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JWTAuthenticationEntryPointTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private AuthenticationException authException; + + @InjectMocks + private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Test + void testCommence() throws IOException { + String errorMessage = "Authentication failed"; + when(authException.getMessage()).thenReturn(errorMessage); + + jwtAuthenticationEntryPoint.commence(request, response, authException); + + verify(response).sendError( + HttpServletResponse.SC_UNAUTHORIZED, + "Unauthorized: " + errorMessage); + } +} \ No newline at end of file diff --git a/proprietary/src/test/java/stirling/software/proprietary/security/filter/JWTAuthenticationFilterTest.java b/proprietary/src/test/java/stirling/software/proprietary/security/filter/JWTAuthenticationFilterTest.java new file mode 100644 index 000000000..f4e531458 --- /dev/null +++ b/proprietary/src/test/java/stirling/software/proprietary/security/filter/JWTAuthenticationFilterTest.java @@ -0,0 +1,301 @@ +package stirling.software.proprietary.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import stirling.software.proprietary.security.model.exception.AuthenticationFailureException; +import stirling.software.proprietary.security.service.CustomUserDetailsService; +import stirling.software.proprietary.security.service.JWTServiceInterface; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JWTAuthenticationFilterTest { + + @Mock + private JWTServiceInterface jwtService; + + @Mock + private CustomUserDetailsService userDetailsService; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private FilterChain filterChain; + + @Mock + private UserDetails userDetails; + + @Mock + private SecurityContext securityContext; + + @InjectMocks + private JWTAuthenticationFilter jwtAuthenticationFilter; + + @Test + void testDoFilterInternalWhenJwtDisabled() throws ServletException, IOException { + when(jwtService.isJwtEnabled()).thenReturn(false); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtService, never()).extractTokenFromRequest(any()); + } + + @Test + void testDoFilterInternalWhenShouldNotFilter() throws ServletException, IOException { + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/login"); + when(request.getMethod()).thenReturn("POST"); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + verify(jwtService, never()).extractTokenFromRequest(any()); + } + + @Test + void testDoFilterInternalWithValidToken() throws ServletException, IOException { + String token = "valid-jwt-token"; + String newToken = "new-jwt-token"; + String username = "testuser"; + + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(token); + when(jwtService.validateToken(token)).thenReturn(true); + when(jwtService.extractUsername(token)).thenReturn(username); + when(userDetails.getAuthorities()).thenReturn((Collection) Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))); + when(userDetailsService.loadUserByUsername(username)).thenReturn(userDetails); + + try (MockedStatic mockedSecurityContextHolder = mockStatic(SecurityContextHolder.class)) { + when(securityContext.getAuthentication()).thenReturn(null); + mockedSecurityContextHolder.when(SecurityContextHolder::getContext).thenReturn(securityContext); + when(jwtService.generateToken(any())).thenReturn(newToken); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(jwtService).validateToken(token); + verify(jwtService).extractUsername(token); + verify(userDetailsService).loadUserByUsername(username); + verify(securityContext).setAuthentication(any(UsernamePasswordAuthenticationToken.class)); + verify(jwtService).generateToken(any()); + verify(jwtService).addTokenToResponse(response, newToken); + verify(filterChain).doFilter(request, response); + } + } + + @Test + void testDoFilterInternalWithMissingTokenForRootPath() throws ServletException, IOException { + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(null); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(response).sendRedirect("/login"); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void testDoFilterInternalWithMissingTokenForNonRootPath() throws ServletException, IOException { + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(null); + + assertThrows(AuthenticationFailureException.class, () -> { + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + }); + + verify(response, never()).sendRedirect(anyString()); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void testDoFilterInternalWithInvalidToken() throws ServletException, IOException { + String token = "invalid-jwt-token"; + + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(token); + when(jwtService.validateToken(token)).thenReturn(false); + + assertThrows(AuthenticationFailureException.class, () -> { + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + }); + + verify(jwtService).validateToken(token); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void testDoFilterInternalWithUserNotFound() throws ServletException, IOException { + String token = "valid-jwt-token"; + String username = "nonexistentuser"; + + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(token); + when(jwtService.validateToken(token)).thenReturn(true); + when(jwtService.extractUsername(token)).thenReturn(username); + when(userDetailsService.loadUserByUsername(username)).thenReturn(null); + + try (MockedStatic mockedSecurityContextHolder = mockStatic(SecurityContextHolder.class)) { + when(securityContext.getAuthentication()).thenReturn(null); + mockedSecurityContextHolder.when(SecurityContextHolder::getContext).thenReturn(securityContext); + + assertThrows(UsernameNotFoundException.class, () -> { + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + }); + + verify(userDetailsService).loadUserByUsername(username); + verify(filterChain, never()).doFilter(request, response); + } + } + + @Test + void testDoFilterInternalWithExistingAuthentication() throws ServletException, IOException { + String token = "valid-jwt-token"; + String newToken = "new-jwt-token"; + String username = "testuser"; + + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(token); + when(jwtService.validateToken(token)).thenReturn(true); + when(jwtService.extractUsername(token)).thenReturn(username); + + try (MockedStatic mockedSecurityContextHolder = mockStatic(SecurityContextHolder.class)) { + Authentication existingAuth = mock(Authentication.class); + when(securityContext.getAuthentication()).thenReturn(existingAuth); + mockedSecurityContextHolder.when(SecurityContextHolder::getContext).thenReturn(securityContext); + when(jwtService.generateToken(existingAuth)).thenReturn(newToken); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(userDetailsService, never()).loadUserByUsername(anyString()); + verify(jwtService).generateToken(existingAuth); + verify(jwtService).addTokenToResponse(response, newToken); + verify(filterChain).doFilter(request, response); + } + } + + @Test + void testShouldNotFilterLoginPost() { + when(request.getRequestURI()).thenReturn("/login"); + when(request.getMethod()).thenReturn("POST"); + + assertTrue(jwtAuthenticationFilter.shouldNotFilter(request)); + } + + @Test + void testShouldNotFilterLoginGet() { + when(request.getRequestURI()).thenReturn("/login"); + when(request.getMethod()).thenReturn("GET"); + + assertTrue(jwtAuthenticationFilter.shouldNotFilter(request)); + } + + @Test + void testShouldNotFilterPublicPaths() { + String[] publicPaths = { + "/register", + "/error", + "/images/logo.png", + "/public/file.txt", + "/css/style.css", + "/fonts/font.ttf", + "/js/script.js", + "/pdfjs/viewer.js", + "/pdfjs-legacy/viewer.js", + "/api/v1/info/status", + "/site.webmanifest", + "/favicon.ico" + }; + + for (String path : publicPaths) { + when(request.getRequestURI()).thenReturn(path); + when(request.getMethod()).thenReturn("GET"); + + assertTrue(jwtAuthenticationFilter.shouldNotFilter(request), + "Should not filter path: " + path); + } + } + + @Test + void testShouldNotFilterStaticFiles() { + String[] staticFiles = { + "/some/path/file.svg", + "/another/path/image.png", + "/path/to/icon.ico" + }; + + for (String file : staticFiles) { + when(request.getRequestURI()).thenReturn(file); + when(request.getMethod()).thenReturn("GET"); + + assertTrue(jwtAuthenticationFilter.shouldNotFilter(request), + "Should not filter file: " + file); + } + } + + @Test + void testShouldFilterProtectedPaths() { + String[] protectedPaths = { + "/protected", + "/api/v1/user/profile", + "/admin", + "/dashboard" + }; + + for (String path : protectedPaths) { + when(request.getRequestURI()).thenReturn(path); + when(request.getMethod()).thenReturn("GET"); + + assertFalse(jwtAuthenticationFilter.shouldNotFilter(request), + "Should filter path: " + path); + } + } + + @Test + void testShouldFilterRootPath() { + when(request.getRequestURI()).thenReturn("/"); + when(request.getMethod()).thenReturn("GET"); + + assertFalse(jwtAuthenticationFilter.shouldNotFilter(request)); + } +} diff --git a/proprietary/src/test/java/stirling/software/proprietary/security/service/JWTServiceTest.java b/proprietary/src/test/java/stirling/software/proprietary/security/service/JWTServiceTest.java new file mode 100644 index 000000000..db07dd3a9 --- /dev/null +++ b/proprietary/src/test/java/stirling/software/proprietary/security/service/JWTServiceTest.java @@ -0,0 +1,287 @@ +package stirling.software.proprietary.security.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.SignatureException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import stirling.software.common.model.ApplicationProperties; + +import java.security.KeyPair; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JWTServiceTest { + + @Mock + private ApplicationProperties.Security securityProperties; + + @Mock + private ApplicationProperties.Security.JWT jwtProperties; + + @Mock + private Authentication authentication; + + @Mock + private UserDetails userDetails; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private JWTService jwtService; + + @BeforeEach + void setUp() { + lenient().when(securityProperties.getJwt()).thenReturn(jwtProperties); + lenient().when(securityProperties.isJwtActive()).thenReturn(true); + lenient().when(jwtProperties.isSettingsValid()).thenReturn(true); + lenient().when(jwtProperties.getExpiration()).thenReturn(3600000L); + lenient().when(jwtProperties.getIssuer()).thenReturn("Stirling-PDF"); + + jwtService = new JWTService(securityProperties); + } + + @Test + void testGenerateTokenWithAuthentication() { + String username = "testuser"; + when(authentication.getPrincipal()).thenReturn(userDetails); + when(userDetails.getUsername()).thenReturn(username); + + String token = jwtService.generateToken(authentication); + + assertNotNull(token); + assertTrue(token.length() > 0); + assertEquals(username, jwtService.extractUsername(token)); + } + + @Test + void testGenerateTokenWithUsernameAndClaims() { + String username = "testuser"; + Map claims = new HashMap<>(); + claims.put("role", "admin"); + claims.put("department", "IT"); + + String token = jwtService.generateToken(username, claims); + + assertNotNull(token); + assertTrue(token.length() > 0); + assertEquals(username, jwtService.extractUsername(token)); + + Map extractedClaims = jwtService.extractAllClaims(token); + assertEquals("admin", extractedClaims.get("role")); + assertEquals("IT", extractedClaims.get("department")); + } + + @Test + void testGenerateTokenWhenJwtDisabled() { + when(securityProperties.isJwtActive()).thenReturn(false); + + assertThrows(IllegalStateException.class, () -> { + jwtService.generateToken("testuser", new HashMap<>()); + }); + } + + @Test + void testValidateTokenSuccess() { + String token = jwtService.generateToken("testuser", new HashMap<>()); + + assertTrue(jwtService.validateToken(token)); + } + + @Test + void testValidateTokenWhenJwtDisabled() { + when(securityProperties.isJwtActive()).thenReturn(false); + + assertFalse(jwtService.validateToken("any-token")); + } + + @Test + void testValidateTokenWithInvalidToken() { + assertFalse(jwtService.validateToken("invalid-token")); + } + + @Test + void testValidateTokenWithExpiredToken() { + // Create a token that expires immediately + when(jwtProperties.getExpiration()).thenReturn(1L); + JWTService shortLivedJwtService = new JWTService(securityProperties); + String token = shortLivedJwtService.generateToken("testuser", new HashMap<>()); + + // Wait a bit to ensure expiration + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + assertFalse(shortLivedJwtService.validateToken(token)); + } + + @Test + void testExtractUsername() { + String username = "testuser"; + String token = jwtService.generateToken(username, new HashMap<>()); + + assertEquals(username, jwtService.extractUsername(token)); + } + + @Test + void testExtractAllClaims() { + String username = "testuser"; + Map claims = new HashMap<>(); + claims.put("role", "admin"); + claims.put("department", "IT"); + + String token = jwtService.generateToken(username, claims); + Map extractedClaims = jwtService.extractAllClaims(token); + + assertEquals("admin", extractedClaims.get("role")); + assertEquals("IT", extractedClaims.get("department")); + assertEquals(username, extractedClaims.get("sub")); + assertEquals("Stirling-PDF", extractedClaims.get("iss")); + } + + @Test + void testExtractAllClaimsWhenJwtDisabled() { + when(securityProperties.isJwtActive()).thenReturn(false); + + assertThrows(IllegalStateException.class, () -> { + jwtService.extractAllClaims("any-token"); + }); + } + + @Test + void testIsTokenExpired() { + String token = jwtService.generateToken("testuser", new HashMap<>()); + assertFalse(jwtService.isTokenExpired(token)); + + when(jwtProperties.getExpiration()).thenReturn(1L); + JWTService shortLivedJwtService = new JWTService(securityProperties); + String expiredToken = shortLivedJwtService.generateToken("testuser", new HashMap<>()); + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + assertThrows(ExpiredJwtException.class, () -> assertTrue(shortLivedJwtService.isTokenExpired(expiredToken))); + } + + @Test + void testExtractTokenFromRequestWithAuthorizationHeader() { + String token = "test-token"; + when(request.getHeader("Authorization")).thenReturn("Bearer " + token); + + assertEquals(token, jwtService.extractTokenFromRequest(request)); + } + + @Test + void testExtractTokenFromRequestWithCookie() { + String token = "test-token"; + Cookie[] cookies = {new Cookie("STIRLING_JWT", token)}; + when(request.getHeader("Authorization")).thenReturn(null); + when(request.getCookies()).thenReturn(cookies); + + assertEquals(token, jwtService.extractTokenFromRequest(request)); + } + + @Test + void testExtractTokenFromRequestWithNoCookies() { + when(request.getHeader("Authorization")).thenReturn(null); + when(request.getCookies()).thenReturn(null); + + assertNull(jwtService.extractTokenFromRequest(request)); + } + + @Test + void testExtractTokenFromRequestWithWrongCookie() { + Cookie[] cookies = {new Cookie("OTHER_COOKIE", "value")}; + when(request.getHeader("Authorization")).thenReturn(null); + when(request.getCookies()).thenReturn(cookies); + + assertNull(jwtService.extractTokenFromRequest(request)); + } + + @Test + void testExtractTokenFromRequestWithInvalidAuthorizationHeader() { + when(request.getHeader("Authorization")).thenReturn("Basic token"); + when(request.getCookies()).thenReturn(null); + + assertNull(jwtService.extractTokenFromRequest(request)); + } + + @Test + void testAddTokenToResponse() { + String token = "test-token"; + + jwtService.addTokenToResponse(response, token); + + verify(response).setHeader("Authorization", "Bearer " + token); + verify(response).addHeader(eq("Set-Cookie"), contains("STIRLING_JWT=" + token)); + verify(response).addHeader(eq("Set-Cookie"), contains("HttpOnly")); + verify(response).addHeader(eq("Set-Cookie"), contains("Secure")); + verify(response).addHeader(eq("Set-Cookie"), contains("SameSite=Strict")); + } + + @Test + void testClearTokenFromResponse() { + jwtService.clearTokenFromResponse(response); + + verify(response).setHeader("Authorization", ""); + verify(response).addHeader(eq("Set-Cookie"), contains("STIRLING_JWT=")); + verify(response).addHeader(eq("Set-Cookie"), contains("Max-Age=0")); + } + + @Test + void testIsJwtEnabledWhenEnabled() { + when(securityProperties.isJwtActive()).thenReturn(true); + when(jwtProperties.isSettingsValid()).thenReturn(true); + + assertTrue(jwtService.isJwtEnabled()); + } + + @Test + void testIsJwtEnabledWhenDisabled() { + when(securityProperties.isJwtActive()).thenReturn(false); + + assertFalse(jwtService.isJwtEnabled()); + } + + @Test + void testIsJwtEnabledWhenInvalidSettings() { + when(securityProperties.isJwtActive()).thenReturn(true); + when(jwtProperties.isSettingsValid()).thenReturn(false); + + assertFalse(jwtService.isJwtEnabled()); + } + + @Test + void testIsJwtEnabledWhenJwtPropertiesNull() { + when(securityProperties.isJwtActive()).thenReturn(true); + when(securityProperties.getJwt()).thenReturn(null); + + JWTService jwtServiceWithNullProps = new JWTService(securityProperties); + assertFalse(jwtServiceWithNullProps.isJwtEnabled()); + } +}