Mock JWT with JwtDecoder in JUnit Test

1. 개요

이 튜토리얼에서는 JWT (JSON Web Token)를 효과적으로 모킹하여 JWT 인증을 사용하는 Spring Security 애플리케이션의 단위 테스트를 작성하는 방법을 살펴보겠습니다. JWT 보호 엔드포인트를 테스트하려면 실제 토큰 생성이나 검증에 의존하지 않고 다양한 JWT 시나리오를 시뮬레이션해야 합니다. 이 접근 방식을 통해 테스트 중에 실제 JWT 토큰을 관리하는 복잡성과 관계없이 견고한 단위 테스트를 작성할 수 있습니다.

JWT 디코딩을 모킹하는 것은 외부 종속성, 즉 토큰 생성 서비스나 서드파티 ID 제공자와 인증 로직을 분리할 수 있게 해주기 때문에 단위 테스트에서 중요합니다. 다양한 JWT 시나리오를 시뮬레이션함으로써 애플리케이션이 유효한 토큰, 사용자 정의 클레임, 잘못된 토큰 및 만료된 토큰을 올바르게 처리하는지 확인할 수 있습니다.

우리는 Mockito를 사용하여 JwtDecoder를 모킹하고, 사용자 정의 JWT 클레임을 생성하며, 다양한 시나리오를 테스트하는 방법을 배울 것입니다. 이 튜토리얼이 끝날 무렵에는 Spring Security JWT 기반 인증 로직에 대한 포괄적인 단위 테스트를 작성할 수 있게 될 것입니다.

2. 설정 및 구성

테스트를 작성하기 전에, 필요한 의존성으로 테스트 환경을 설정하겠습니다. 우리는 Spring Security OAuth2, Mockito,JUnit 5를 테스트에 사용할 것입니다.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
    <version>6.4.2</version>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.15.2</version>
    <scope>test</scope>
</dependency>

인증을 위한 JWT를 지원하는 spring-security-oauth2-jose 의존성과, 테스트에서 의존성을 모킹할 수 있게 해주는 mockito-core 의존성을 추가합니다. 이를 통해 UserController 단위가 외부 시스템으로부터 격리될 수 있습니다.

테스트 클래스 MockJwtDecoderJUnitTest를 만들고 Mockito를 사용해 JwtDecoder를 모킹합시다. 초기 설정은 다음과 같습니다.

@ExtendWith(MockitoExtension.class)
public class MockJwtDecoderJUnitTest {
    @Mock
    private JwtDecoder jwtDecoder;

    @InjectMocks
    private UserController userController;

    @BeforeEach
    void setUp() {
        SecurityContextHolder.clearContext();
    }
}

이 설정에서는 @ExtendWith(MockitoExtension.class)를 사용하여 JUnit 테스트에서 Mockito를 활성화합니다. @Mock을 사용해 JwtDecoder를 모킹하고, @InjectMocks를 통해 모킹된 JwtDecoderUserController에 주입합니다. 각 테스트 전에 SecurityContextHolder를 초기화하여 깨끗한 상태를 보장합니다.

3. JWT 디코딩 모킹

환경 설정이 완료되었으니, JWT 디코딩을 모킹하는 테스트를 작성해 보겠습니다. 먼저 유효한 JWT 토큰을 테스트하는 것부터 시작합니다.

3.1. 유효한 토큰 테스트

유효한 토큰이 제공되었을 때 애플리케이션은 사용자 정보를 반환해야 합니다. 이 시나리오를 테스트하는 방법은 다음과 같습니다.

@Test
void whenValidToken_thenReturnsUserInfo() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "john.doe");

    Jwt jwt = Jwt.withTokenValue("token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    ResponseEntity<String> response = userController.getUserInfo(jwt);

    assertEquals("Hello, john.doe", response.getBody());
    assertEquals(HttpStatus.OK, response.getStatusCode());
}

이 테스트에서는 sub (주제) 클레임이 있는 모의 JWT를 생성합니다. JwtAuthenticationToken을 사용하여 보안 컨텍스트를 설정하고, UserController가 토큰을 처리하여 응답을 반환합니다. 호출한 응답에 대해 검증합니다.

3.2. 사용자 정의 클레임 테스트

때로는 JWT에 역할(role)이나 이메일 주소와 같은 사용자 정의 클레임이 포함될 수 있습니다. 애플리케이션이 이러한 사용자 정의 클레임을 어떻게 처리하는지 테스트합니다.

@Test
void whenTokenHasCustomClaims_thenProcessesCorrectly() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "john.doe");
    claims.put("roles", Arrays.asList("ROLE_USER", "ROLE_ADMIN"));
    claims.put("email", "john.doe@example.com");

    Jwt jwt = Jwt.withTokenValue("token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    ResponseEntity<String> response = userController.getUserInfo(jwt);

    assertEquals("Hello, john.doe", response.getBody());
    assertEquals(HttpStatus.OK, response.getStatusCode());
}

이 테스트에서는 JWT에 사용자 정의 클레임(역할 및 이메일)을 추가합니다. 컨트롤러는 토큰을 처리하고 예상 응답을 반환합니다.

4. 다양한 시나리오 처리

4.1. 잘못된 토큰 테스트

잘못된 토큰이 제공될 때 애플리케이션은 JwtValidationException을 발생시켜야 합니다. 이 시나리오를 테스트하는 방법은 다음과 같습니다.

@Test
void whenInvalidToken_thenThrowsException() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "invalid.user");

    Jwt invalidJwt = Jwt.withTokenValue("invalid_token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    when(jwtDecoder.decode("invalid_token"))
      .thenThrow(new JwtValidationException(
        "Invalid token", 
        Arrays.asList(new OAuth2Error("invalid_token"))
      ));

    JwtValidationException thrown = assertThrows(
      JwtValidationException.class,
      () -> jwtDecoder.decode("invalid_token")
    );

    assertEquals("Invalid token", thrown.getMessage());
}

이 테스트에서는 JwtDecoder를 모킹하여 잘못된 토큰을 해독할 때 JwtValidationException을 발생시킵니다. 제기된 예외의 메시지가 정확한지 검증합니다.

4.2. 만료된 토큰 테스트

이 시나리오에서는 만료된 토큰이 제공되었을 때 애플리케이션 역시 JwtValidationException을 발생시켜야 합니다. 이 시나리오를 테스트하는 방법은 다음과 같습니다.

@Test
void whenTokenExpired_thenThrowsException() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", "expired.user");
    claims.put("exp", Instant.now().minusSeconds(3600));
    claims.put("iat", Instant.now().minusSeconds(7200));

    Jwt expiredJwt = Jwt.withTokenValue("expired_token")
      .header("alg", "none")
      .claims(existingClaims -> existingClaims.putAll(claims))
      .build();

    when(jwtDecoder.decode("expired_token"))
      .thenThrow(new JwtValidationException(
        "Token expired", 
        Arrays.asList(new OAuth2Error("invalid_token"))
      ));

    JwtValidationException thrown = assertThrows(
      JwtValidationException.class,
      () -> jwtDecoder.decode("expired_token")
    );

    assertEquals("Token expired", thrown.getMessage());
}

이 테스트에서는 과거의 만료 시간을 가진 JWT를 생성합니다. JwtDecoder를 모킹하여 만료된 토큰을 해독할 때 JwtValidationException을 발생시킵니다. 제기된 예외의 메시지가 정확한지 검증합니다.

5. 결론

이 튜토리얼에서는 Mockito를 사용하여 JUnit 테스트에서 JWT 디코딩을 모킹하는 방법을 배웠습니다. 유효한 토큰 테스트, 사용자 정의 클레임 처리, 잘못된 토큰 및 만료된 토큰 관리 등 다양한 시나리오를 다루었습니다. JWT 디코딩을 모킹함으로써 외부 토큰 생성이나 검증 서비스에 의존하지 않고 Spring Security 애플리케이션에 대한 단위 테스트를 작성할 수 있습니다. 이러한 접근 방식을 통해 우리의 테스트는 빠르고 신뢰할 수 있으며 외부 종속성에 독립적입니다.

이 문서의 전체 소스 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다