포스트 컨트롤러의 write 메서드를 테스트 해보자.
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PostControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context) // spring의 환경을 알고있어야 set
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@WithMockUser
@Test
public void write_테스트() throws Exception {
// given
PostWriteReqDto postWriteReqDto = PostWriteReqDto.builder()
.categoryId(1) // 이거 분명히 터짐 FK 연결 안되어있어서!
.title("스프링1강")
.content("재밌음")
.build();
// when
ResultActions resultActions = mockMvc.perform(post("/s/post"));
// then
resultActions
.andDo(MockMvcResultHandlers.print());
}
}
1. postWriteReqDto 체크
실제 컨트롤러 주소에 요청했을 때 우선 해당 메서드로 잘 들어가는지 확인하자.
@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
// postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
// return "redirect:/user/" + loginUser.getUser().getId() + "/post";
return "1";
}
@WithMockUser가 있기 때문에 인증에 성공하여 잘들어간다.
@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
// postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
// return "redirect:/user/" + loginUser.getUser().getId() + "/post";
postWriteReqDto.getTitle();
return "1";
}
컨트롤러로 전송되는 postWriteReqDto의 값이 null이라 무조건 터질줄 알았는데 터지지 않는다.
값에 null이 들어가긴했는데 터지진않네. 직접 터뜨려주자.
@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
// postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
// return "redirect:/user/" + loginUser.getUser().getId() + "/post";
if (postWriteReqDto.getTitle() == null) {
throw new NullPointerException();
}
return "1";
}
null이 아닌 값이 들어오게 먼저 잡아주자.
얘는 json 타입으로 받고있지 않기 때문에 기본전략인 x-www-form-urlencoded 타입으로 전송해줘야한다.
x-www-form-urlencoded 타입으로 전송할 때는 param을 사용해야한다.
json 넣을 때는 content 사용
@WithMockUser // username="username" password="password"
@Test
public void write_테스트() throws Exception {
// given
PostWriteReqDto postWriteReqDto = PostWriteReqDto.builder()
.categoryId(1) // 이거 분명히 터짐 FK 연결 안되어있어서!
.title("스프링1강")
.content("재밌음")
.build();
// when
ResultActions resultActions = mockMvc.perform(
post("/s/post").param("title", postWriteReqDto.getTitle()));
// then
resultActions
.andDo(MockMvcResultHandlers.print());
}
터지지 않고 잘 들어간다. param 사용!
param에는 무조건 문자열이 들어감에 주의하자.
통신이니까!
ResultActions resultActions = mockMvc.perform(
post("/s/post")
.param("title", postWriteReqDto.getTitle())
.param("content", postWriteReqDto.getContent())
.param("categoryId", postWriteReqDto.getCategoryId() + ""));
@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
// postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
// return "redirect:/user/" + loginUser.getUser().getId() + "/post";
if (postWriteReqDto.getTitle() == null) {
throw new NullPointerException("title이 없습니다.");
}
if (postWriteReqDto.getContent() == null) {
throw new NullPointerException("content가 없습니다.");
}
if (postWriteReqDto.getCategoryId() == null) {
throw new NullPointerException("categoryId가 없습니다.");
}
return "1";
}
다시 테스트해보니 통과하고 값이 다 잘 들어왔다.
2. loginUser 값 체크
@WithMockUser 체크
줄이 한줄만 그이고 80번 라인에서 loginUser를 받아오지 못해서 오류가 터졌다.
실제로 @AuthenticationPrincipal은 시큐리티 컨텍스트에서 가져오는데
Mock로 띄운건 가져오지 않는다.
클라이언트의 요청이 들어오면 풀링기법으로 관리되는 request 객체를 가져오고
이 객체들은 모두 하나의 세션 영역을 공유한다.
세션에서 클라이언트를 구분하는 방법은 JSESSIONID로 구분했다.
스프링에서는 이 세션 영역 내부에 SecurityContextHolder라는 지갑을 하나 만들어둔다.
보안의 모든 것을 다 알고있는 지갑인 것이다.
시큐리티 컨텍스트 홀더 내부에 SecurityContext들이 모여있다.
이 시큐리티 컨텍스트 내부에는 각각 Authentication 객체가 들어있다.
객체 내부에 원래는 User 오브젝트를 넣어주면 되는데
그냥 User 오브젝트에는 내 마음대로 만들어서 시큐리티가 원하는 정보가 없을 수도 있기 때문에
강제성을 부여하여 오브젝트 타입을 받는게 아닌 UserDetails 객체를 받는다.
이 Authentication 객체는 로그인 성공시에 만들어진다.
Authentication은 UserDetails를 감싸고 있는 래핑 클래스일 뿐이다.
로그인 시에 UserDetailsService의 loadUserByUsername이 호출되며 인증 성공 시에
UserDetails 객체가 Authentication에 담기게 되는 것이다.
그럼 인증이 필요한 주소(/s)를 어떻게 통과했을까?
@WithMockUser를 붙여주면
username="username", password="password" 값이 담긴 가짜 UserDetails 객체를
Authentication 객체에 담겨서 만들어진다.
그래서 세션이 존재하기 때문에 인증에 통과하여 진입이 가능한 것이다.
진입에는 성공했지만 loginUser에 접근할 때 null이라고 오류가 발생하게 된다.
@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
return "redirect:/user/" + loginUser.getUser().getId() + "/post";
}
@WithMockUser가 만들어준 UserDetails에는 User 오브젝트가 없기 때문이다.
이는 우리가 UserDetails 타입을 구현한(impl) LoginUser에 필요에 의해 만들어둔 User 오브젝트이기 때문에
당연히 @WithMockUser가 만든 UserDetails에는 User가 존재하지 않는다.
그래서 터진것이다.
그러면 내가 LoginUser를 만들어줘야 하는데 @WithMockUser로는 불가능하다.
얘는 User를 만들어주지 않으니까!
왜 우리가 getUser가 필요한가?
게시글쓰기 할 때 getUser로 받아온 principal 객체로 포스트 save 할 때 필요하기 때문이다.
그래서 User를 넣어둔것이다.
개념적으로만 봤을 때 UserDetails 오브젝트 내부에 User 오브젝트만 넣어주면 끝난다.
단순하다.
@WithMockUser로 감싸서 인증 필터를 통과만 시켜준거지 값을 가져올 수는 없다.
간단한 인증만 통과하고 싶을 때는 @WithMockUser를 사용하고,
세션에 있는 값을 내부에서 사용해야 한다면 이 가짜 객체로는 불가능하다.
핵심은 시큐리티 컨텍스트 홀더(SCH)에 우리가 시큐리티 컨텍스트를 하나 만들어서 끼워 넣어줘야 한다.
https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/
@WithUserDetails
내가 커스텀한 UserDetailsService를 하나 더 만들건데 이 때는 프로파일 설정이 필요하다.
테스트용 TestUserDetailsService를 하나 더만들면 UserDetailsService가
IoC 컨테이너에 2개가 뜨게되어 충돌나기 때문에
시큐리티는 어떤 서비스로 가야하는지 구분하지 못한다.
TestUserDetailsService에는 @Profile("test")
실제 UserDetailsService에는 @Profile("dev")
SCH에다가 직접 만든 UserDetails 타입을 넣어주면 끝난다.
package site.metacoding.blogv3.config.auth;
import java.time.LocalDateTime;
import org.springframework.context.annotation.Profile;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import site.metacoding.blogv3.domain.user.User;
@Profile("test")
@Service
public class TestUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = User.builder()
.id(1)
.username("ssar")
.password("1234")
.email("ssar@nate.com")
.createDate(LocalDateTime.now())
.updateDate(LocalDateTime.now())
.build();
return new LoginUser(user);
}
}
// WithMockUser는 간단한 인증만 통과하고 싶을 때 사용!!
// 만약에 세션에 있는 값을 내부에서 사용해야 하면? -> 다른 것을 써야 함.
// @WithMockUser // username="username" password="password"
@WithUserDetails("ssar")
@Test
public void write_테스트() throws Exception {
// given
PostWriteReqDto postWriteReqDto = PostWriteReqDto.builder()
.categoryId(1) // 이거 분명히 터짐
.title("스프링1강")
.content("재밌음")
.build();
// when
ResultActions resultActions = mockMvc.perform(
post("/s/post")
.param("title", postWriteReqDto.getTitle())
.param("content", postWriteReqDto.getContent())
.param("categoryId", postWriteReqDto.getCategoryId() + "")
.param("thumnailFile", ""));
// then
resultActions
.andDo(MockMvcResultHandlers.print());
}
[출처]
https://cafe.naver.com/metacoding
메타 코딩 유튜브
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9