2.스프링 시큐리티 [@AuthenticationPrincipal 테스트하기]

dasd412·2022년 1월 31일
2

포트폴리오

목록 보기
22/41

컨트롤러 코드

테스트할 컨트롤러 메서드는 다음과 같다.

파라미터에 @AuthenticationPrincipal 이 부착되어 있기 때문에 현재 세션에 담긴 사용자 정보를 얻을 수 있다.

해당 사용자 정보 중, PK값은 일지, 식단, 음식 저장에 필수적으로 들어간다. (복합키 식별 관계로 만들었기 때문이다.)

어떻게 테스트 코드를 작성할 수 있을까?

@RestController
public class SecurityDiaryRestController {
    //시큐리티에서는 인증이 이미 되있기 때문에 기존 url 은 관리자만 진입가능하게 바꿨다.
    private final SaveDiaryService saveDiaryService;
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public SecurityDiaryRestController(SaveDiaryService saveDiaryService) {
        this.saveDiaryService = saveDiaryService;
    }

    @PostMapping("/api/diary/user/diabetes-diary")
    public void postDiary(@AuthenticationPrincipal PrincipalDetails principalDetails, @RequestBody @Valid SecurityDiaryPostRequestDTO dto) {
        logger.info("post diary with authenticated user");

        //JSON 직렬화가 LocalDateTime 에는 적용이 안되서 작성한 코드.
        String date = dto.getYear() + "-" + dto.getMonth() + "-" + dto.getDay() + " " + dto.getHour() + ":" + dto.getMinute() + ":" + dto.getSecond();
        LocalDateTime writtenTime = LocalDateTime.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        DiabetesDiary diary=saveDiaryService.saveDiaryOfWriterById(EntityId.of(Writer.class,principalDetails.getWriter().getId()),dto.getFastingPlasmaGlucose(),dto.getRemark(),writtenTime);


    }
}

테스트 클래스 코드

시도했던 방법 중, WithMockUser와 같은 어노테이션 부착등이 있었다. 하지만, 두 가지 측면에서 문제가 있었다.

  1. 해당 어노테이션만으론 @AuthenticationPrincipal 에 PK 값을 넣을 수 없다.
  2. saveDiaryService.saveDiaryOfWriterById() 를 호출할 때, 테스트 db에 해당 id가 없어서 null 예외가 발생한다.

따라서 위 문제를 해결하기 위해

  1. 먼저 리포지토리에 테스트용 유저 엔티티를 직접 만든다.
  2. 그리고 TestUserDetailsService 라는 테스트용 UserDetailsService 를 만들어 사용한다. loadUserByUsername 에서 리턴되는 것을 principalDetails 로 받는다.
  3. 마지막으로 테스트 코드에 mockMvc.perform(post(url).with(user(principalDetails)) 과 같이 org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() 메서드 안에 파라미터로 principalDetails 를 넣어주면 된다.

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
public class SecurityDiaryRestControllerTest {

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private WriterRepository writerRepository;

    @Autowired
    private SaveDiaryService saveDiaryService;

    private final TestUserDetailsService testUserDetailsService = new TestUserDetailsService();

    private PrincipalDetails principalDetails;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
        //해결책 1. 단계
        Writer entity = Writer.builder()
                .writerEntityId(EntityId.of(Writer.class, 1L))
                .name(TestUserDetailsService.USERNAME)
                .email(TestUserDetailsService.USERNAME)
                .password("test")
                .role(Role.User)
                .provider(null)
                .providerId(null)
                .build();
        writerRepository.save(entity);

				//해결책 2. 단계
        principalDetails = (PrincipalDetails) testUserDetailsService.loadUserByUsername(TestUserDetailsService.USERNAME);
    }

    @Test
    public void methodAccessTest_PostDiaryWithSecurity() throws Exception {
        //given
        String url = "/api/diary/user/diabetes-diary";

        List<SecurityFoodDTO> breakFast = IntStream.rangeClosed(1, 3).mapToObj(i -> new SecurityFoodDTO("breakFast" + i, i))
                .collect(Collectors.toList());
        List<SecurityFoodDTO> lunch = IntStream.rangeClosed(1, 3).mapToObj(i -> new SecurityFoodDTO("lunch" + i, i))
                .collect(Collectors.toList());
        List<SecurityFoodDTO> dinner = IntStream.rangeClosed(1, 1).mapToObj(i -> new SecurityFoodDTO("dinner" + i, i))
                .collect(Collectors.toList());

        SecurityDiaryPostRequestDTO dto = SecurityDiaryPostRequestDTO.builder().fastingPlasmaGlucose(100).remark("test")
                .year("2021").month("12").day("22").hour("00").minute("00").second("00")
                .breakFastSugar(110).lunchSugar(120).dinnerSugar(130)
                .breakFastFoods(breakFast).lunchFoods(lunch).dinnerFoods(dinner).build();

        //when and then
				//해결책 3.단계
        mockMvc.perform(post(url).with(user(principalDetails))
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(dto)))
                .andExpect(status().isOk());
    }

}

테스트용 UserDetailsService클래스 코드

TestUserDetailsServiceUserDetailsService 구현체이므로loadUserByUsername 을 구현해서 가짜 사용자 세션을 만들어낼 수 있다.

하지만, 주의할 것이 하나 있다.

바로 Bean 으로 등록해서는 안된다는 것이다. 스프링 빈으로 등록할 때 TestUserDetailsService 말고도 다른 UserDetailsService 구현체 (실제로 사용되는 구현체)가 이미 있기 때문에 빈 생성 예외가 던져진다.

따라서 빈으로 등록하지 말고 테스트 클래스에서 new 로 생성해서 사용하자.

private final TestUserDetailsService testUserDetailsService = new TestUserDetailsService();

@Profile("test")
public class TestUserDetailsService implements UserDetailsService {

    public static final String USERNAME = "user@example.com";

    private Writer getUser() {
        return Writer.builder()
                .writerEntityId(EntityId.of(Writer.class, 1L))
                .name(USERNAME)
                .email(USERNAME)
                .password("test")
                .provider(null)
                .providerId(null)
                .role(Role.User)
                .build();
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        if (s.equals(USERNAME)) {
            return new PrincipalDetails(getUser());
        }
        return null;
    }
}
profile
시드 레벨 스타트업의 2호 직원으로서 백엔드 시스템의 70%를 설계 및 개발하였고, TIPS 5억 투자 유치에 기여한 서버 개발자입니다. (Go/Python/MSA/Spring)

0개의 댓글