테스트할 컨트롤러 메서드는 다음과 같다.
파라미터에 @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와 같은 어노테이션 부착등이 있었다. 하지만, 두 가지 측면에서 문제가 있었다.
@AuthenticationPrincipal
에 PK 값을 넣을 수 없다.saveDiaryService.saveDiaryOfWriterById()
를 호출할 때, 테스트 db에 해당 id가 없어서 null 예외가 발생한다.따라서 위 문제를 해결하기 위해
TestUserDetailsService
라는 테스트용 UserDetailsService
를 만들어 사용한다. loadUserByUsername
에서 리턴되는 것을 principalDetails
로 받는다. 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());
}
}
TestUserDetailsService
는 UserDetailsService
구현체이므로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;
}
}