Spring security Test
- SpringSecurity의 테스트에서는 User가 로그인한 상태를 가정하고 테스트해야 하는 경우가 많다.
- 인증을 받지 않은 상태로 테스트를 하면 SpringSecurity에서 요청 자체를 막기 때문에 테스트가 제대로 동작조차 하지 못하기 때문에 spring-security-test를 사용해서 해결
- Spring-security-test를 사용하면 테스트 직전에 Mock User를 인증시켜놓고 테스트를 구동시킬수 있다.
의존성 추가
testImplementation 'org.springframework.security:spring-security-test'
mockMVC
- Test 실행 전 MockMvc에 springSecurity (static 메소드)를 설정
@BeforeEach
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.build();
BDDAssertions
- then절로 테스트 결과를 검증
MockMvc
- perform
- 요청을 전송하는 역할
- 결과로 ResultActions 반환
- get, post, put, delete
- 요청할 http method 를 perform()안에 넣어서 결정, 인자로 경로를 사용
- perform(get("/hello"))
- params
- Key value 파라미터를 전달
- 여러개 일때는 params, 단일은 param
- andExpect
- 응답을 검증
- andExpect(status().isBadRequest())
- status()
- 상태 검증
- isOk(200)
- view()
- 응답으로 받은 뷰 이름 검증
- redirect()
- 응답으로 받은 redirect 검증
- content()
- 응답 body 검증
- andDo
- 해야할 일 표현
- andDo(print()) : 결과를 출력
가짜 유저 세팅하여 테스트하는 방법
- @WithMockUser
- 특정 사용자가 존재하는 것처럼 테스트 진행
- Mock(가짜) User를 생성하고 Authentication을 만든다.
- User : org.springframework.security.core.userdetails.User
- 내부에서 UserDetails를 직접 구현해서 Custom User를 만들어 사용하는 경우에는 WithMockUser를 사용하면 문제가 발생할 수 있다.
- WithMockUser는 org.springframework.security.core.userdetails.User를 만들어 주지만 우리가 필요한 User는 Custom User이기 때문에 인증에는 문제가 없지 형변환 에러(class cast)가 발생 할 수도 있다.
- @WithUserDetails
- 테스트를 위해 별도로 구현한 UserDetailsService를 참고해서 사용자를 가짜로 로그인 할 수 있다.
- WithMockUser와 마찬가지로 Mock(가짜) User를 생성하고 Authentication을 생성
- 가짜 User를 가져올 때 UserDetailsService의 Bean 이름을 넣어줘서userDetailsService.loadUserByUsername(String username)을 통해 User를 가져온다.
- @WithAnonymousUser
- WithMockUser와 동일하지만 인증된 유저 대신에 익명(Anonymous)유저를 Authentication에서 사용
- 익명이기 때문에 멤버변수에 유저와 관련된 값이 없다.
- @WithSecurityContext
- 다른 방식들은 Authentication을 가짜로 만들었다고 한다면 WithSecurityContext는 아예 SecurityContext를 만든다.
- WithSecurityContextFactory를 Implement한 Class를 넣어준다
public interface WithSecurityContextFactory<A extends Annotation> {
SecurityContext createSecurityContext(A annotation);
}
- with(user( ))
- 다른 방식은 어노테이션 기반인 반면에 이 방식은 직접 User를 MockMvc에 주입하는 방법
- WithMockUser와 마찬가지로 유저를 생성해서 Principal에 넣고 Authentication을 생성
- org.springframework.security.test.web.servlet.request.user를 사용
mockMvc.perform(get("/admin")
.with(user(user))) // 유저 추가
// AdminControllerTest
@SpringBootTest // 통합 테스트를 위한 애플리케이션 컨텍스트를 로드합니다.
@ActiveProfiles(profiles = "test") // "test" 프로파일이 활성화되도록 설정
@Transactional // 각각의 테스트 메소드 실행 후 롤백하여 데이터베이스의 상태를 변경하지 않고 테스트를 수행
class AdminControllerTest {
@Autowired // 스프링에 의해 자동 주입되는 필드를 나타냅니다. 이 경우에는 userRepository가 자동으로 주입됩니다.
private UserRepository userRepository;
private MockMvc mockMvc;
private User user;
private User admin;
@BeforeEach // 각 테스트 메소드가 실행되기 전에 실행되는 메소드입니다. 여기서는 테스트에 필요한 MockMvc 객체를 설정하고, 테스트용 사용자 데이터를 데이터베이스에 저장합니다.
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(SecurityMockMvcConfigurers.springSecurity()) // Spring Security 설정 적용
.alwaysDo(print())
.build();
// ROLE_USER 권한이 있는 유저 생성
user = userRepository.save(new User("user", "user", "ROLE_USER"));
// ROLE_ADMIN 권한이 있는 관리자 생성
admin = userRepository.save(new User("admin", "admin", "ROLE_ADMIN"));
}
// 특정 URL에 대한 테스트 케이스: 인증 없이 접근하는 경우
@Test
void getNoteForAdmin_인증없음() throws Exception {
mockMvc.perform(get("/admin").with(csrf())) // CSRF 토큰 추가
.andExpect(redirectedUrlPattern("**/login")) // 로그인 페이지로 리다이렉션되는지 확인
.andExpect(status().is3xxRedirection()); // 3xx 리다이렉션 상태코드 반환 여부 확인
}
// 특정 URL에 대한 테스트 케이스: 어드민 권한으로 접근하는 경우
@Test
void getNoteForAdmin_어드민인증있음() throws Exception {
mockMvc.perform(get("/admin").with(csrf()).with(user(admin))) // 어드민 유저로 요청
.andExpect(status().is2xxSuccessful()); // 2xx 성공 상태코드 반환 여부 확인
}
// 특정 URL에 대한 테스트 케이스: 유저 권한으로 접근하는 경우
@Test
void getNoteForAdmin_유저인증있음() throws Exception {
mockMvc.perform(get("/admin").with(csrf()).with(user(user))) // 유저로 요청
.andExpect(status().isForbidden()); // 403 Forbidden 상태코드 반환 여부 확인
}
}
// NoteControllerTest
@SpringBootTest
@ActiveProfiles(profiles = "test")
@Transactional
class NoteControllerTest {
@Autowired
private UserRepository userRepository;
@Autowired
private NoteRepository noteRepository;
private MockMvc mockMvc;
private User user;
private User admin;
@BeforeEach
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.alwaysDo(print())
.build();
// 유저와 어드민 데이터 준비
user = userRepository.save(new User("user123", "user", "ROLE_USER"));
admin = userRepository.save(new User("admin123", "admin", "ROLE_ADMIN"));
}
// 인증 없이 노트에 접근하는 경우를 테스트합니다.
@Test
void getNote_인증없음() throws Exception {
mockMvc.perform(get("/note"))
.andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
// 인증된 사용자가 노트에 접근하는 경우를 테스트합니다.
@Test
@WithUserDetails(
value = "user123", // 사용자명
userDetailsServiceBeanName = "userDetailsService", // UserDetailsService 구현체의 Bean
setupBefore = TestExecutionEvent.TEST_EXECUTION // 테스트 실행 직전에 유저를 가져온다.
)
void getNote_인증있음() throws Exception {
mockMvc.perform(
get("/note")
).andExpect(status().isOk()) // 성공적인 응답 여부 확인
.andExpect(view().name("note/index")) // 정상적인 뷰 반환 여부 확인
.andDo(print()); // 결과를 출력
}
// 인증 없이 노트를 작성하는 경우를 테스트합니다.
@Test
void postNote_인증없음() throws Exception {
mockMvc.perform(
post("/note").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(redirectedUrlPattern("**/login")) // 로그인 페이지로 리다이렉션 여부 확인
.andExpect(status().is3xxRedirection()); // 3xx 리다이렉션 상태코드 반환 여부 확인
}
// 어드민 권한으로 노트를 작성하는 경우를 테스트합니다.
@Test
@WithUserDetails(
value = "admin123",
userDetailsServiceBeanName = "userDetailsService",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void postNote_어드민인증있음() throws Exception {
mockMvc.perform(
post("/note").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(status().isForbidden()); // 접근 거부
}
// 유저 권한으로 노트를 작성하는 경우를 테스트합니다.
@Test
@WithUserDetails(
value = "user123",
userDetailsServiceBeanName = "userDetailsService",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void postNote_유저인증있음() throws Exception {
mockMvc.perform(
post("/note").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(redirectedUrl("note")).andExpect(status().is3xxRedirection());
}
// 인증 없이 노트를 삭제하는 경우를 테스트합니다.
@Test
void deleteNote_인증없음() throws Exception {
Note note = noteRepository.save(new Note("제목", "내용", user));
mockMvc.perform(
delete("/note?id=" + note.getId()).with(csrf())
).andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
// 유저 권한으로 노트를 삭제하는 경우를 테스트합니다.
@Test
@WithUserDetails(
value = "user123",
userDetailsServiceBeanName = "userDetailsService",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void deleteNote_유저인증있음() throws Exception {
Note note = noteRepository.save(new Note("제목", "내용", user));
mockMvc.perform(
delete("/note?id=" + note.getId()).with(csrf())
).andExpect(redirectedUrl("note")).andExpect(status().is3xxRedirection());
}
// 어드민 권한으로 노트를 삭제하는 경우를 테스트합니다.
@Test
@WithUserDetails(
value = "admin123",
userDetailsServiceBeanName = "userDetailsService",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void deleteNote_어드민인증있음() throws Exception {
Note note = noteRepository.save(new Note("제목", "내용", user));
mockMvc.perform(
delete("/note?id=" + note.getId()).with(csrf()).with(user(admin))
).andExpect(status().isForbidden()); // 접근 거부
}
}
// NoteServiceTest
@SpringBootTest // 스프링 부트 애플리케이션 컨텍스트를 로드하여 테스트 환경을 설정합니다.
@ActiveProfiles(profiles = "test") // "test" 프로파일을 활성화하여 테스트 시 환경설정을 구성합니다.
@Transactional // 각 테스트 메소드가 실행된 후 롤백되어 테스트 간 데이터 오염을 방지합니다.
class NoteServiceTest {
@Autowired
private NoteService noteService; // 테스트할 노트 서비스를 주입합니다.
@Autowired
private UserRepository userRepository; // 사용자 레포지토리를 주입합니다.
@Autowired
private NoteRepository noteRepository; // 노트 레포지토리를 주입합니다.
// 사용자가 게시한 노트를 조회하는 경우를 테스트합니다.
@Test
void findByUser_유저가_게시물조회() {
// given
User user = userRepository.save(new User("username", "password", "ROLE_USER"));
noteRepository.save(new Note("title1", "content1", user));
noteRepository.save(new Note("title2", "content2", user));
// when
List<Note> notes = noteService.findByUser(user);
// then
then(notes.size()).isEqualTo(2); // 사용자에 의해 생성된 노트 수를 확인
Note note1 = notes.get(0);
Note note2 = notes.get(1);
// note1 = title2
then(note1.getUser().getUsername()).isEqualTo("username");
then(note1.getTitle()).isEqualTo("title2"); // 최신 노트가 먼저 조회되어야 함
then(note1.getContent()).isEqualTo("content2");
// note2 = title1
then(note2.getUser().getUsername()).isEqualTo("username");
then(note2.getTitle()).isEqualTo("title1");
then(note2.getContent()).isEqualTo("content1");
}
// 어드민이 모든 사용자의 게시물을 조회하는 경우를 테스트합니다.
@Test
void findByUser_어드민이_조회() {
// given
User admin = userRepository.save(new User("admin", "password", "ROLE_ADMIN"));
User user1 = userRepository.save(new User("username", "password", "ROLE_USER"));
User user2 = userRepository.save(new User("username2", "password", "ROLE_USER"));
noteRepository.save(new Note("title1", "content1", user1));
noteRepository.save(new Note("title2", "content2", user1));
noteRepository.save(new Note("title3", "content3", user2));
// when
List<Note> notes = noteService.findByUser(admin);
// then
then(notes.size()).isEqualTo(3); // 모든 노트를 조회해야 함
Note note1 = notes.get(0);
Note note2 = notes.get(1);
Note note3 = notes.get(2);
// note1 = title3
then(note1.getUser().getUsername()).isEqualTo("username2");
then(note1.getTitle()).isEqualTo("title3"); // 최신 노트가 먼저 조회되어야 함
then(note1.getContent()).isEqualTo("content3");
// note2 = title2
then(note2.getUser().getUsername()).isEqualTo("username");
then(note2.getTitle()).isEqualTo("title2");
then(note2.getContent()).isEqualTo("content2");
// note3 = title1
then(note3.getUser().getUsername()).isEqualTo("username");
then(note3.getTitle()).isEqualTo("title1");
then(note3.getContent()).isEqualTo("content1");
}
// 노트를 저장하는 경우를 테스트합니다.
@Test
void saveNote() {
// given
User user = userRepository.save(new User("username", "password", "ROLE_USER"));
// when
noteService.saveNote(user, "title1", "content1");
// then
then(noteRepository.count()).isOne(); // 노트가 저장되었는지 확인
}
// 노트를 삭제하는 경우를 테스트합니다.
@Test
void deleteNote() {
User user = userRepository.save(new User("username", "password", "ROLE_USER"));
Note note = noteRepository.save(new Note("title1", "content1", user));
noteService.deleteNote(user, note.getId());
// then
then(noteRepository.count()).isZero(); // 노트가 삭제되었는지 확인
}
}
// NoticeControllerTest
@SpringBootTest // 스프링 부트 애플리케이션 컨텍스트를 로드하여 테스트 환경을 설정합니다.
@Transactional // 각 테스트 메소드가 실행된 후 롤백되어 테스트 간 데이터 오염을 방지합니다.
class NoticeControllerTest {
@Autowired
private NoticeRepository noticeRepository; // NoticeRepository를 주입합니다.
private MockMvc mockMvc; // MockMvc 객체를 선언합니다.
@BeforeEach
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity()) // Spring Security를 적용합니다.
.alwaysDo(print()) // 모든 요청과 응답을 콘솔에 출력합니다.
.build();
}
// 인증 없이 공지사항을 조회하는 경우를 테스트합니다.
@Test
void getNotice_인증없음() throws Exception {
mockMvc.perform(get("/notice"))
.andExpect(redirectedUrlPattern("**/login")) // 로그인 페이지로 리다이렉트되는지 확인합니다.
.andExpect(status().is3xxRedirection()); // HTTP 상태 코드가 리다이렉션인지 확인합니다.
}
// 인증된 사용자가 공지사항을 조회하는 경우를 테스트합니다.
@Test
@WithMockUser // 인증된 가짜 사용자를 사용하여 테스트합니다.
void getNotice_인증있음() throws Exception {
mockMvc.perform(get("/notice"))
.andExpect(status().isOk()) // HTTP 상태 코드가 200인지 확인합니다.
.andExpect(view().name("notice/index")); // 뷰 이름이 "notice/index"인지 확인합니다.
}
// 인증 없이 공지사항을 게시하는 경우를 테스트합니다.
@Test
void postNotice_인증없음() throws Exception {
mockMvc.perform(
post("/notice")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(status().isForbidden()); // 접근 거부
}
// 유저 권한으로 공지사항을 게시하는 경우를 테스트합니다.
@Test
@WithMockUser(roles = {"USER"}, username = "admin", password = "admin")
void postNotice_유저인증있음() throws Exception {
mockMvc.perform(
post("/notice").with(csrf()) // CSRF 토큰을 함께 전송합니다.
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(status().isForbidden()); // 접근 거부
}
// 어드민 권한으로 공지사항을 게시하는 경우를 테스트합니다.
@Test
@WithMockUser(roles = {"ADMIN"}, username = "admin", password = "admin")
void postNotice_어드민인증있음() throws Exception {
mockMvc.perform(
post("/notice").with(csrf()) // CSRF 토큰을 함께 전송합니다.
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(redirectedUrl("notice")) // 공지사항 페이지로 리다이렉트되는지 확인합니다.
.andExpect(status().is3xxRedirection()); // HTTP 상태 코드가 리다이렉션인지 확인합니다.
}
// 인증 없이 공지사항을 삭제하는 경우를 테스트합니다.
@Test
void deleteNotice_인증없음() throws Exception {
Notice notice = noticeRepository.save(new Notice("제목", "내용"));
mockMvc.perform(
delete("/notice?id=" + notice.getId())
).andExpect(status().isForbidden()); // 접근 거부
}
// 유저 권한으로 공지사항을 삭제하는 경우를 테스트합니다.
@Test
@WithMockUser(roles = {"USER"}, username = "admin", password = "admin")
void deleteNotice_유저인증있음() throws Exception {
Notice notice = noticeRepository.save(new Notice("제목", "내용"));
mockMvc.perform(
delete("/notice?id=" + notice.getId()).with(csrf()) // CSRF 토큰을 함께 전송합니다.
).andExpect(status().isForbidden()); // 접근 거부
}
// 어드민 권한으로 공지사항을 삭제하는 경우를 테스트합니다.
@Test
@WithMockUser(roles = {"ADMIN"}, username = "admin", password = "admin")
void deleteNotice_어드민인증있음() throws Exception {
Notice notice = noticeRepository.save(new Notice("제목", "내용"));
mockMvc.perform(
delete("/notice?id=" + notice.getId()).with(csrf()) // CSRF 토큰을 함께 전송합니다.
).andExpect(redirectedUrl("notice")) // 공지사항 페이지로 리다이렉트되는지 확인합니다.
.andExpect(status().is3xxRedirection()); // HTTP 상태 코드가 리다이렉션인지 확인합니다.
}
}
// SignUpControllerTest
@SpringBootTest // 스프링 부트 애플리케이션 컨텍스트를 로드하여 테스트 환경을 설정합니다.
@Transactional // 각 테스트 메소드가 실행된 후 롤백되어 테스트 간 데이터 오염을 방지합니다.
class SignUpControllerTest {
private MockMvc mockMvc; // MockMvc 객체를 선언합니다.
@BeforeEach
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity()) // Spring Security를 적용합니다.
.alwaysDo(print()) // 모든 요청과 응답을 콘솔에 출력합니다.
.build();
}
// 회원가입을 테스트합니다.
@Test
void signup() throws Exception {
mockMvc.perform(
post("/signup").with(csrf()) // CSRF 토큰을 함께 전송합니다.
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "user123") // 회원 정보를 파라미터로 전송합니다.
.param("password", "password")
).andExpect(redirectedUrl("login")) // 로그인 페이지로 리다이렉트되는지 확인합니다.
.andExpect(status().is3xxRedirection()); // HTTP 상태 코드가 리다이렉션인지 확인합니다.
}
}
// UserServiceTest
@SpringBootTest // 스프링 부트 애플리케이션 컨텍스트를 로드하여 테스트 환경을 설정합니다.
@ActiveProfiles(profiles = "test") // 활성화된 프로파일이 "test"인 경우를 지정합니다.
@Transactional // 각 테스트 메소드가 실행된 후 롤백되어 테스트 간 데이터 오염을 방지합니다.
class UserServiceTest {
@Autowired
private UserService userService; // UserService를 주입합니다.
@Autowired
private UserRepository userRepository; // UserRepository를 주입합니다.
// 회원 가입을 테스트합니다.
@Test
void signup() {
// given
String username = "user123";
String password = "password";
// when
User user = userService.signup(username, password);
// then
then(user.getId()).isNotNull(); // id가 NotNull인지 검증
then(user.getUsername()).isEqualTo("user123"); // 유저명이 user123인지 검증
then(user.getPassword()).startsWith("{bcrypt}"); // 패스워드가 {bcrypt}로 시작하는지 검증
then(user.getAuthorities()).hasSize(1); // Authorities가 1개인지 검증
then(user.getAuthorities().stream().findFirst().get().getAuthority()).isEqualTo("ROLE_USER");
then(user.isAdmin()).isFalse(); // 어드민 여부가 False인지 검증
then(user.isAccountNonExpired()).isTrue();
then(user.isAccountNonLocked()).isTrue();
then(user.isEnabled()).isTrue();
then(user.isCredentialsNonExpired()).isTrue();
}
// 어드민 회원 가입을 테스트합니다.
@Test
void signupAdmin() {
// given
String username = "admin123";
String password = "password";
// when
User user = userService.signupAdmin(username, password);
// then
then(user.getId()).isNotNull();
then(user.getUsername()).isEqualTo("admin123");
then(user.getPassword()).startsWith("{bcrypt}");
then(user.getAuthorities()).hasSize(1);
then(user.getAuthorities().stream().findFirst().get().getAuthority()).isEqualTo("ROLE_ADMIN");
then(user.isAdmin()).isTrue();
then(user.isAccountNonExpired()).isTrue();
then(user.isAccountNonLocked()).isTrue();
then(user.isEnabled()).isTrue();
then(user.isCredentialsNonExpired()).isTrue();
}
// 유저명으로 유저를 조회하는 테스트를 수행합니다.
@Test
void findByUsername() {
// given
userRepository.save(new User("user123", "password", "ROLE_USER"));
// when
User user = userService.findByUsername("user123");
// then
then(user.getId()).isNotNull();
}
}
결과
'개발 > Spring' 카테고리의 다른 글
Spring Security Config (1) | 2024.03.01 |
---|---|
Spring security Architecture, Filter (0) | 2024.02.29 |
Spring Security 구현 (0) | 2024.02.27 |
Spring Security (0) | 2024.02.27 |
Querydsl (1) | 2024.02.27 |