SecurityContext를 통하여 얻어낸 유저 정보를 글 , 댓글과 연결하고, 이를 테스트하는 과정을 기록했습니다.
(CommentService, PostsService, UserDetailService)
CommentService의 CommentSave 기능
@Transactional
public Long commentSave(CommentSaveRequestDto requestDto, Long id) {
Posts post = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다"));
requestDto.setPosts(post);
User user = userDetailService.returnUser();
requestDto.setUser(user);
return commentRepository.save(requestDto.toEntity()).getId();
}
PostsService의 save 기능
@Transactional
public Long save(PostsSaveRequestDto requestDto) {
User user = userDetailService.returnUser();
requestDto.setUser(user);
return postsRepository.save(requestDto.toEntity()).getId();
}
UserDetailService의 returnUser 기능
public User returnUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userName;
if (principal instanceof UserDetails) {
userName = ((UserDetails) principal).getUsername();
} else {
userName = principal.toString();
}
int start = userName.indexOf("email")+6;
int end = userName.indexOf(".com,")+4;
String email = userName.substring(start, end);
User user = userRepository.findByEmail(email).orElse(null);
return user;
}
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class, setupBefore = TestExecutionEvent.TEST_EXECUTION)
public @interface WithMockCustomUser {
String name() default "testName";
String email() default "testEmail@naver.com";
String role() default "USER";
}
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
CustomUserDetails principal = new CustomUserDetails();
Authentication auth =
new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());
context.setAuthentication(auth);
return context;
}
}
CustomUserDetails principal = new CustomUserDetails();
@Getter
class CustomUserDetails implements UserDetails {
public String name = "fakeUser";
public String email = "1park5@naver.com";
public Role role = Role.USER;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
auth.add(new SimpleGrantedAuthority((role.toString())));
return auth;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return "email="+email+",";
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
@Override
public String getUsername() {
return "email="+email+",";
}
UserDetails로 형변환 principal이 email=이메일, 의 형태로 이메일을 리턴하기 때문에, 이와 구조를 맞춰주었습니다.
@Test
@WithMockCustomUser
public void posts_등록() throws Exception {
//given
String title = "postTitle";
String content = "postContent";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.build();
String url = "http://localhost:" + port + "api/v1/posts";
//when
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andDo(print())
.andExpect(status().isOk());
//then
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
java.lang.AssertionError: Status
Expected :200
Actual :403
<Click to see difference>
MockMvc를 사용하고 있었는데, andDo() 로 프린트 찍어보면
Body = {"title":"postTitle","content":"postContent","user":null}
유저 자체도 null이 뜹니다.
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
int start = userName.indexOf("email")+6;
int end = userName.indexOf(".com,")+4;
String email = userName.substring(start, end);
System.out.println("이메일 : " + email);
이렇게 sout찍어가면서 어디까지 가다 막히는지 확인했는데, requestDto에 setUser하는 부분까지는 적용이 되다가, mvc.perform 부분에서 갑자기 403이 떴었습니다.
@Test
@WithMockCustomUser
public void posts_등록() throws Exception {
//given
String title = "postTitle";
String content = "postContent";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.build();
// String url = "http://localhost:" + port + "api/v1/posts";
//when
postsApiController.save(requestDto);
// mvc.perform(post(url)
// .contentType(MediaType.APPLICATION_JSON_UTF8)
// .content(new ObjectMapper().writeValueAsString(requestDto)))
// .andDo(print())
// .andExpect(status().isOk());
//then
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
CommentsApiControllerTest도 변경해줍니다
@Test
@WithMockCustomUser
@Transactional // 프록시 객체에 실제 데이터를 불러올 수 있게 영속성 컨텍스트에서 관리
public void comment_등록() throws Exception {
// given
String title = "title";
String content = "content";
User user = userRepository.save(User.builder()
.name("name")
.email("fake@naver.com")
.picture("fakePic.com")
.role(Role.USER)
.build());
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.user(user)
.build();
postsRepository.save(requestDto.toEntity());
String comment = "comment";
Posts posts = postsRepository.findAll().get(0);
CommentSaveRequestDto saveRequestDto = CommentSaveRequestDto.builder()
.comment(comment)
.posts(posts)
.build();
Long id = posts.getId();
// String url = "http://localhost:"+ port + "/api/v1/posts/" + id + "/comments";
//when
commentsApiController.save(saveRequestDto,id);
//then
assertThat(commentRepository.findAll().get(0).getComment()).isEqualTo("comment");
// mvc.perform(post(url)
// .contentType(MediaType.APPLICATION_JSON_UTF8)
// .content(objectMapper.writeValueAsString(saveRequestDto)))
// .andDo(print())
// .andExpect(status().isOk());
}
UserDetailService에서 유저를 찾는 코드
User user = userRepository.findByEmail(email).orElse(null);
@Before
public void setUser() {
User user = new User("fakeUser","1park5@naver.com","fakePic.com",Role.USER);
// 유저가 있어야 UserDetailService의 returnUser가 유저 가져올 수 있음
userRepository.deleteAll();
userRepository.save(user);
}
기존에 보고 따라하던 책에서 MockMvc사용하여 테스트를 진행해서 이를 활용해서 테스트를 작성하다보니, 이걸 사용하는데 있어서 경각심을 못느꼈습니다.
MockMvc는 말그대로 테스트용(mock) mvc환경을 만들어서, 애플리케이션 서버에 배포하지 않고도 mvc동작을 재현할 수 있는 객체입니다.
그리고 이를 위해 @Before로 생성하던 MockMvc 인스턴스는
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.apply(sharedHttpSession())
.build();
}
SpringSecurityContext에 대해 알지못하고, 자체적인 mvc환경에서 테스트를 하다보니 계속 403(거절) 이 떴던듯 합니다.
더이상 @WithMockUser 를 사용하지 않으니, 인증에 대한 허가를 받을 수 없었던 것입니다.