From dfeb17c8868b9ee123efab417317706c19c1d8e6 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Wed, 9 Jul 2025 17:07:38 +0100 Subject: [PATCH] Updated tests --- .../security/service/JWTService.java | 41 ++----- .../filter/JWTAuthenticationFilterTest.java | 104 ++++++++++++++---- .../security/service/JWTServiceTest.java | 70 +++++++++--- 3 files changed, 151 insertions(+), 64 deletions(-) diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/service/JWTService.java b/proprietary/src/main/java/stirling/software/proprietary/security/service/JWTService.java index 63847ba63..2872309de 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/service/JWTService.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/service/JWTService.java @@ -70,29 +70,12 @@ public class JWTService implements JWTServiceInterface { } @Override - public void validateToken(String token) { + public void validateToken(String token) throws AuthenticationFailureException { if (!isJwtEnabled()) { throw new IllegalStateException("JWT is not enabled"); } - try { - extractAllClaimsFromToken(token); - } catch (SignatureException e) { - log.warn("Invalid signature: {}", e.getMessage()); - throw new AuthenticationFailureException("Invalid signature", e); - } catch (MalformedJwtException e) { - log.warn("Invalid token: {}", e.getMessage()); - throw new AuthenticationFailureException("Invalid token", e); - } catch (ExpiredJwtException e) { - log.warn("The token has expired: {}", e.getMessage()); - throw new AuthenticationFailureException("The token has expired", e); - } catch (UnsupportedJwtException e) { - log.warn("The token is unsupported: {}", e.getMessage()); - throw new AuthenticationFailureException("The token is unsupported", e); - } catch (IllegalArgumentException e) { - log.warn("Claims are empty: {}", e.getMessage()); - throw new AuthenticationFailureException("Claims are empty", e); - } + extractAllClaimsFromToken(token); } @Override @@ -128,20 +111,20 @@ public class JWTService implements JWTServiceInterface { .parseSignedClaims(token) .getPayload(); } catch (SignatureException e) { - log.warn("Invalid JWT signature: {}", e.getMessage()); - throw new AuthenticationFailureException("Invalid JWT signature", e); + log.warn("Invalid signature: {}", e.getMessage()); + throw new AuthenticationFailureException("Invalid signature", e); } catch (MalformedJwtException e) { - log.warn("Invalid JWT token: {}", e.getMessage()); - throw new AuthenticationFailureException("Invalid JWT token", e); + log.warn("Invalid token: {}", e.getMessage()); + throw new AuthenticationFailureException("Invalid token", e); } catch (ExpiredJwtException e) { - log.warn("JWT token is expired: {}", e.getMessage()); - throw new AuthenticationFailureException("JWT token is expired", e); + log.warn("The token has expired: {}", e.getMessage()); + throw new AuthenticationFailureException("The token has expired", e); } catch (UnsupportedJwtException e) { - log.warn("JWT token is unsupported: {}", e.getMessage()); - throw new AuthenticationFailureException("JWT token is unsupported", e); + log.warn("The token is unsupported: {}", e.getMessage()); + throw new AuthenticationFailureException("The token is unsupported", e); } catch (IllegalArgumentException e) { - log.warn("JWT claims are empty: {}", e.getMessage()); - throw new AuthenticationFailureException("JWT claims are empty", e); + log.warn("Claims are empty: {}", e.getMessage()); + throw new AuthenticationFailureException("Claims are empty", e); } } 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 index f4e531458..7eb2caa9f 100644 --- a/proprietary/src/test/java/stirling/software/proprietary/security/filter/JWTAuthenticationFilterTest.java +++ b/proprietary/src/test/java/stirling/software/proprietary/security/filter/JWTAuthenticationFilterTest.java @@ -4,16 +4,15 @@ 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.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -24,13 +23,15 @@ import stirling.software.proprietary.security.service.CustomUserDetailsService; import stirling.software.proprietary.security.service.JWTServiceInterface; import java.io.IOException; -import java.util.Arrays; +import java.io.PrintWriter; import java.util.Collection; +import java.util.Collections; 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.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -57,9 +58,16 @@ class JWTAuthenticationFilterTest { @Mock private SecurityContext securityContext; - @InjectMocks + @Mock + private PrintWriter printWriter; + private JWTAuthenticationFilter jwtAuthenticationFilter; + @BeforeEach + void setUp() { + jwtAuthenticationFilter = new JWTAuthenticationFilter(jwtService, userDetailsService); + } + @Test void testDoFilterInternalWhenJwtDisabled() throws ServletException, IOException { when(jwtService.isJwtEnabled()).thenReturn(false); @@ -92,15 +100,22 @@ class JWTAuthenticationFilterTest { when(request.getRequestURI()).thenReturn("/protected"); when(request.getMethod()).thenReturn("GET"); when(jwtService.extractTokenFromRequest(request)).thenReturn(token); - when(jwtService.validateToken(token)).thenReturn(true); + doNothing().when(jwtService).validateToken(token); when(jwtService.extractUsername(token)).thenReturn(username); - when(userDetails.getAuthorities()).thenReturn((Collection) Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))); + when(userDetails.getAuthorities()).thenReturn(Collections.emptyList()); when(userDetailsService.loadUserByUsername(username)).thenReturn(userDetails); try (MockedStatic mockedSecurityContextHolder = mockStatic(SecurityContextHolder.class)) { - when(securityContext.getAuthentication()).thenReturn(null); + // Create the authentication token that will be set and returned + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + // Mock the security context behavior: + // - First call (in createAuthToken): returns null + // - Second call (in createAuthToken after setting): returns the created token + when(securityContext.getAuthentication()).thenReturn(null).thenReturn(authToken); mockedSecurityContextHolder.when(SecurityContextHolder::getContext).thenReturn(securityContext); - when(jwtService.generateToken(any())).thenReturn(newToken); + when(jwtService.generateToken(authToken)).thenReturn(newToken); jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); @@ -108,7 +123,7 @@ class JWTAuthenticationFilterTest { verify(jwtService).extractUsername(token); verify(userDetailsService).loadUserByUsername(username); verify(securityContext).setAuthentication(any(UsernamePasswordAuthenticationToken.class)); - verify(jwtService).generateToken(any()); + verify(jwtService).generateToken(authToken); verify(jwtService).addTokenToResponse(response, newToken); verify(filterChain).doFilter(request, response); } @@ -133,12 +148,14 @@ class JWTAuthenticationFilterTest { when(request.getRequestURI()).thenReturn("/protected"); when(request.getMethod()).thenReturn("GET"); when(jwtService.extractTokenFromRequest(request)).thenReturn(null); + when(response.getWriter()).thenReturn(printWriter); - assertThrows(AuthenticationFailureException.class, () -> { - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - }); + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - verify(response, never()).sendRedirect(anyString()); + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json"); + verify(response).setCharacterEncoding("UTF-8"); + verify(printWriter).write(contains("JWT is missing from the request")); verify(filterChain, never()).doFilter(request, response); } @@ -150,13 +167,37 @@ class JWTAuthenticationFilterTest { when(request.getRequestURI()).thenReturn("/protected"); when(request.getMethod()).thenReturn("GET"); when(jwtService.extractTokenFromRequest(request)).thenReturn(token); - when(jwtService.validateToken(token)).thenReturn(false); + doThrow(new AuthenticationFailureException("Invalid token")).when(jwtService).validateToken(token); + when(response.getWriter()).thenReturn(printWriter); - assertThrows(AuthenticationFailureException.class, () -> { - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - }); + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); verify(jwtService).validateToken(token); + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json"); + verify(response).setCharacterEncoding("UTF-8"); + verify(printWriter).write(contains("Invalid token")); + verify(filterChain, never()).doFilter(request, response); + } + + @Test + void testDoFilterInternalWithExpiredToken() throws ServletException, IOException { + String token = "expired-jwt-token"; + + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(token); + doThrow(new AuthenticationFailureException("The token has expired")).when(jwtService).validateToken(token); + when(response.getWriter()).thenReturn(printWriter); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(jwtService).validateToken(token); + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + verify(response).setContentType("application/json"); + verify(response).setCharacterEncoding("UTF-8"); + verify(printWriter).write(contains("The token has expired")); verify(filterChain, never()).doFilter(request, response); } @@ -169,7 +210,7 @@ class JWTAuthenticationFilterTest { when(request.getRequestURI()).thenReturn("/protected"); when(request.getMethod()).thenReturn("GET"); when(jwtService.extractTokenFromRequest(request)).thenReturn(token); - when(jwtService.validateToken(token)).thenReturn(true); + doNothing().when(jwtService).validateToken(token); when(jwtService.extractUsername(token)).thenReturn(username); when(userDetailsService.loadUserByUsername(username)).thenReturn(null); @@ -196,7 +237,7 @@ class JWTAuthenticationFilterTest { when(request.getRequestURI()).thenReturn("/protected"); when(request.getMethod()).thenReturn("GET"); when(jwtService.extractTokenFromRequest(request)).thenReturn(token); - when(jwtService.validateToken(token)).thenReturn(true); + doNothing().when(jwtService).validateToken(token); when(jwtService.extractUsername(token)).thenReturn(username); try (MockedStatic mockedSecurityContextHolder = mockStatic(SecurityContextHolder.class)) { @@ -298,4 +339,25 @@ class JWTAuthenticationFilterTest { assertFalse(jwtAuthenticationFilter.shouldNotFilter(request)); } -} + + @Test + void testSendUnauthorizedResponseFormat() throws ServletException, IOException { + when(jwtService.isJwtEnabled()).thenReturn(true); + when(request.getRequestURI()).thenReturn("/protected"); + when(request.getMethod()).thenReturn("GET"); + when(jwtService.extractTokenFromRequest(request)).thenReturn(null); + when(response.getWriter()).thenReturn(printWriter); + + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + verify(response).setStatus(401); + verify(response).setContentType("application/json"); + verify(response).setCharacterEncoding("UTF-8"); + verify(printWriter).write(argThat((String json) -> + json.contains("\"error\": \"Unauthorized\"") && + json.contains("\"message\": \"JWT is missing from the request\"") && + json.contains("\"status\": 401") + )); + verify(printWriter).flush(); + } +} \ No newline at end of file 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 index db07dd3a9..7d03a6393 100644 --- a/proprietary/src/test/java/stirling/software/proprietary/security/service/JWTServiceTest.java +++ b/proprietary/src/test/java/stirling/software/proprietary/security/service/JWTServiceTest.java @@ -9,7 +9,6 @@ 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; @@ -18,6 +17,7 @@ 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 stirling.software.proprietary.security.model.exception.AuthenticationFailureException; import java.security.KeyPair; import java.util.Date; @@ -86,7 +86,7 @@ class JWTServiceTest { 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")); @@ -105,19 +105,23 @@ class JWTServiceTest { void testValidateTokenSuccess() { String token = jwtService.generateToken("testuser", new HashMap<>()); - assertTrue(jwtService.validateToken(token)); + assertDoesNotThrow(() -> jwtService.validateToken(token)); } @Test void testValidateTokenWhenJwtDisabled() { when(securityProperties.isJwtActive()).thenReturn(false); - - assertFalse(jwtService.validateToken("any-token")); + + assertThrows(IllegalStateException.class, () -> { + jwtService.validateToken("any-token"); + }); } @Test void testValidateTokenWithInvalidToken() { - assertFalse(jwtService.validateToken("invalid-token")); + assertThrows(AuthenticationFailureException.class, () -> { + jwtService.validateToken("invalid-token"); + }); } @Test @@ -134,7 +138,27 @@ class JWTServiceTest { Thread.currentThread().interrupt(); } - assertFalse(shortLivedJwtService.validateToken(token)); + assertThrows(AuthenticationFailureException.class, () -> { + shortLivedJwtService.validateToken(token); + }); + } + + @Test + void testValidateTokenWithMalformedToken() { + AuthenticationFailureException exception = assertThrows(AuthenticationFailureException.class, () -> { + jwtService.validateToken("malformed.token"); + }); + + assertTrue(exception.getMessage().contains("Invalid")); + } + + @Test + void testValidateTokenWithEmptyToken() { + AuthenticationFailureException exception = assertThrows(AuthenticationFailureException.class, () -> { + jwtService.validateToken(""); + }); + + assertTrue(exception.getMessage().contains("Claims are empty") || exception.getMessage().contains("Invalid")); } @Test @@ -145,6 +169,13 @@ class JWTServiceTest { assertEquals(username, jwtService.extractUsername(token)); } + @Test + void testExtractUsernameWithInvalidToken() { + assertThrows(AuthenticationFailureException.class, () -> { + jwtService.extractUsername("invalid-token"); + }); + } + @Test void testExtractAllClaims() { String username = "testuser"; @@ -162,11 +193,9 @@ class JWTServiceTest { } @Test - void testExtractAllClaimsWhenJwtDisabled() { - when(securityProperties.isJwtActive()).thenReturn(false); - - assertThrows(IllegalStateException.class, () -> { - jwtService.extractAllClaims("any-token"); + void testExtractAllClaimsWithInvalidToken() { + assertThrows(AuthenticationFailureException.class, () -> { + jwtService.extractAllClaims("invalid-token"); }); } @@ -175,17 +204,30 @@ class JWTServiceTest { String token = jwtService.generateToken("testuser", new HashMap<>()); assertFalse(jwtService.isTokenExpired(token)); + // Create a token that expires immediately when(jwtProperties.getExpiration()).thenReturn(1L); JWTService shortLivedJwtService = new JWTService(securityProperties); String expiredToken = shortLivedJwtService.generateToken("testuser", new HashMap<>()); + // Wait a bit to ensure expiration try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - assertThrows(ExpiredJwtException.class, () -> assertTrue(shortLivedJwtService.isTokenExpired(expiredToken))); + // Since expired tokens now throw exceptions in extractAllClaimsFromToken, + // isTokenExpired will also throw an exception + assertThrows(AuthenticationFailureException.class, () -> { + shortLivedJwtService.isTokenExpired(expiredToken); + }); + } + + @Test + void testIsTokenExpiredWithInvalidToken() { + assertThrows(AuthenticationFailureException.class, () -> { + jwtService.isTokenExpired("invalid-token"); + }); } @Test @@ -284,4 +326,4 @@ class JWTServiceTest { JWTService jwtServiceWithNullProps = new JWTService(securityProperties); assertFalse(jwtServiceWithNullProps.isJwtEnabled()); } -} +} \ No newline at end of file