주문
결제하기(주문 저장하기)
Controller
@PostMapping("/save") // /orders/save
public ResponseEntity<?> saveOrder() {
OrderRespDTO responseDTO = null;
//ItemInfo 담을 리스트 생성
List<ItemInfoDTO> itemInfoDTOList = new ArrayList<>();
//ItemInfo 리스트에 담기
ItemInfoDTO itemInfoDTO1 = ItemInfoDTO.builder()
.id(4)
.optionName("01. 슬라이딩 지퍼백 크리스마스에디션 4종")
.quantity(10)
.price(100000)
.build();
itemInfoDTOList.add(itemInfoDTO1);
ItemInfoDTO itemInfoDTO2 = ItemInfoDTO.builder()
.id(5)
.optionName("02. 슬라이딩 지퍼백 플라워에디션 5종")
.quantity(10)
.price(109000)
.build();
itemInfoDTOList.add(itemInfoDTO2);
//ProductItem 리스트 만들기
List<ProductItemDTO> productItemDTOList = new ArrayList<>();
ProductItemDTO productItemDTO1 = ProductItemDTO.builder()
.productName("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전")
.items(itemInfoDTOList)
.build();
//ProductItem 리스트에 담기
productItemDTOList.add(productItemDTO1);
//응답할 dto 생성
responseDTO = OrderRespDTO.builder()
.id(2)
.products(productItemDTOList)
.totalPrice(209000)
.build();
return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
✳️ 주문 결과 확인과 결제하기는 같은 응답구조를 가지므로 동일한 DTO를 사용했다.
주문 결과 확인 코드는 여기서 ⬇️
https://shout-to-my-mae.tistory.com/309
TIL[0706] : 2주차 과제 - Mock Controller 2
상품 상품 상세 보기 Mock GET http://localhost:8080/products/:id JSON { "success": true, "response": { "id": 1, "productName": "기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전", "description":
shout-to-my-mae.tistory.com
Mock Test
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DisplayName("결제하기(주문 인서트)")
class OrderRestControllerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser //인증된 사용자 생성
// 결제하기(주문 인서트)
public void saveOrder_test() throws Exception {
// when
ResultActions resultActions = mvc.perform(
post("/orders/save")
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response.id").value(2));
resultActions.andExpect(jsonPath("$.response.totalPrice").value(209000));
resultActions.andExpect(jsonPath("$.response.products[0].productName").value("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전"));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].id").value(4));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].price").value(100000));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].id").value(5));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].optionName").value("02. 슬라이딩 지퍼백 플라워에디션 5종"));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].price").value(109000));
}
}
유저
로그인
DTO
public class UserRequest {
@Getter
@Setter
public static class LoginDTO {
private String email;
private String password;
}
}
Controller
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserJPARepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestHeader(value = "Authorization", required = false) String authHeader, @RequestBody UserRequest.LoginDTO loginDTO) {
//검증 단계
String email = loginDTO.getEmail();
String password = loginDTO.getPassword();
//올바른 이메일 형식인지 확인
if (!email.contains("@"))
return ResponseEntity.badRequest().body(ApiUtils.error("이메일 형식으로 작성해주세요:email", HttpStatus.BAD_REQUEST));
//유효한 비밀번호인지 확인
if (!isValidPassword(password))
return ResponseEntity.badRequest().body(ApiUtils.error("영문, 숫자, 특수문자가 포함되어야하고 공백이 포함될 수 없습니다.:password", HttpStatus.BAD_REQUEST));
//인증 확인
if (authHeader == null || authHeader.isEmpty())
return ResponseEntity.badRequest().body(ApiUtils.error("인증되지 않았습니다", HttpStatus.UNAUTHORIZED));
//비밀번호 길이 검증
int passwordLength = loginDTO.getPassword().length();
if(!(passwordLength>=8 && passwordLength <= 20))
return ResponseEntity.badRequest().body(ApiUtils.error("8에서 20자 이내여야 합니다.:password", HttpStatus.BAD_REQUEST));
//로그인 수행
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(loginDTO.getEmail(), loginDTO.getPassword());
Authentication authentication;
//로그인 성공, 실패 여부 확인
try {
authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
}catch (Exception e){
System.out.println(e.getMessage());
return ResponseEntity.badRequest().body(ApiUtils.error("email 또는 password가 올바르지 않습니다", HttpStatus.BAD_REQUEST));
}
CustomUserDetails myUserDetails = (CustomUserDetails) authentication.getPrincipal();
String jwt = JWTProvider.create(myUserDetails.getUser());
//로그인 성공
return ResponseEntity.ok().header(JWTProvider.HEADER, jwt).body(ApiUtils.success(null));
}
//비밀번호 유효성 검사
private boolean isValidPassword(String password) {
boolean hasLetter = false; //문자 여부
boolean hasDigit = false; //숫자 여부
boolean hasSpecialCharacter = false; //특수문자 여부
for (char c:password.toCharArray()){
if(Character.isLetter(c)) hasLetter=true;
else if (Character.isDigit(c)) hasDigit = true;
else if (isSpecialCharacter(c)) hasSpecialCharacter = true;
if(hasLetter && hasDigit && hasSpecialCharacter) break;
}
//문자,숫자,특수문자가 있어야하고, 공백이 없어야한다.
return hasLetter && hasDigit && hasSpecialCharacter && !password.contains(" ");
}
//특수 문자 포함하는지 확인
private boolean isSpecialCharacter(char c) {
String specialCharacters = "!@#$%^&*()-_=+[]{};:'\"\\|<>,.?/~`";
return specialCharacters.contains(String.valueOf(c));
}
}
- 로그인 성공, 로그인 실패(형식 / 문자 / 인증 / 비밀번호 길이)의 경우를 구현했다.
- API 문서에는 기능이 구현되어있지만, 문자 포함 여부와 비밀번호 길이 검증은 회원가입시 이미 검증하므로 필요없을 것 같다.
- 로그인 시마다 jwt 토큰은 항상 달라지는데, 요청시 토큰을 필요로 하는 이유가 궁금하다.
Spring Security 로그인 Mock Test
@Transactional //테스트 후 rollback
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserRestControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private UserJPARepository userJPARepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private WebApplicationContext context;
//Spring Security 테스트 환경 구성
@BeforeEach
public void setup(){
mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
@WithMockUser
@DisplayName("로그인 성공(가입된 id와 비밀번호)")
public void login_success_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user1@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("user1@nate.com");
loginDTO.setPassword("user1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("true"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 -가입된 id와 잘못된 비밀번호")
public void login_fail_pw_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("user@nate.com");
loginDTO.setPassword("wrongpassword!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 존재하지 않는 id와 비밀번호 (미가입)")
public void login_fail_unregistered_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com"); //이미 존재하는 id
loginDTO.setPassword("fake1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 이메일 형식 검증")
public void login_fail_email_format_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newusernate.com"); //올바르지 않은 이메일 (@가 없음)
loginDTO.setPassword("user1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 비밀번호 글자 검증")
public void login_fail_password_character_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com");
loginDTO.setPassword("user1234"); //특수문자가 없는 비밀번호
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 인증되지 않은 유저")
public void login_fail_unauth_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com");
loginDTO.setPassword("user1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//when
mvc.perform( //토큰 보내지 않음
post("/login")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 비밀번호 글자수")
public void login_fail_password_length_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com");
loginDTO.setPassword("us4!"); //적은 글자수의 비밀번호
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//when
mvc.perform( //토큰 보내지 않음
post("/login")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
}
테스트 케이스는 다음과 같다.
- 로그인 성공(가입된 id와 비밀번호)
- 로그인 실패
- 가입된 id와 잘못된 비밀번호
- 존재하지 않는 id와 비밀번호 (미가입)
- 형식, 중복 검증
- 올바르지 않은 이메일
- 이메일 형식 검증 (@가 없음)
- 비밀번호 글자 검증(영문, 숫자, 특수문자 포함, 공백 포함X)
- 중복 이메일 검증
- 비밀번호 글자수 제한 검증회원가입
@Transactional 어노테이션을 붙여 테스트 후 rollback 되도록 하였다.
DTO
public class UserRequest {
@Getter
@Setter
public static class JoinDTO {
private String email;
private String password;
private String username;
}
}
Controller
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserJPARepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@PostMapping("/join")
public ResponseEntity<?> join(@RequestBody UserRequest.JoinDTO joinDTO) {
//검증
String email = joinDTO.getEmail();
String password = joinDTO.getPassword();
//올바른 이메일 형식인지 확인
if (!email.contains("@"))
return ResponseEntity.badRequest().body(ApiUtils.error("이메일 형식으로 작성해주세요:email", HttpStatus.BAD_REQUEST));
//유효한 비밀번호인지 확인
if (!isValidPassword(password))
return ResponseEntity.badRequest().body(ApiUtils.error("영문, 숫자, 특수문자가 포함되어야하고 공백이 포함될 수 없습니다.:password", HttpStatus.BAD_REQUEST));
//동일한 이메일이 존재하는지 확인
UserRequest.CheckEmailDTO checkEmailDTO = new UserRequest.CheckEmailDTO();
checkEmailDTO.setEmail(email);
ResponseEntity<?> responseEntity = check(checkEmailDTO); //check 메서드 사용
boolean isSuccessful = responseEntity.getStatusCode().is2xxSuccessful();
if (!isSuccessful)
return ResponseEntity.badRequest().body(ApiUtils.error("동일한 이메일이 존재합니다 : "+email, HttpStatus.BAD_REQUEST));
//비밀번호 길이 검증
int passwordLength = password.length();
if(!(passwordLength>=8 && passwordLength <= 20))
return ResponseEntity.badRequest().body(ApiUtils.error("8에서 20자 이내여야 합니다.:password", HttpStatus.BAD_REQUEST));
//회원가입 성공
//유저 생성
User user = User.builder()
.email(joinDTO.getEmail())
.password(passwordEncoder.encode(joinDTO.getPassword()))
.username(joinDTO.getUsername())
.roles("ROLE_USER")
.build();
//repo에 저장
userRepository.save(user);
return ResponseEntity.ok(ApiUtils.success(null));
}
private boolean isValidPassword(String password) {
boolean hasLetter = false; //문자 여부
boolean hasDigit = false; //숫자 여부
boolean hasSpecialCharacter = false; //특수문자 여부
for (char c:password.toCharArray()){
if(Character.isLetter(c)) hasLetter=true;
else if (Character.isDigit(c)) hasDigit = true;
else if (isSpecialCharacter(c)) hasSpecialCharacter = true;
if(hasLetter && hasDigit && hasSpecialCharacter) break;
}
//문자,숫자,특수문자가 있어야하고, 공백이 없어야한다.
return hasLetter && hasDigit && hasSpecialCharacter && !password.contains(" ");
}
//특수 문자 포함하는지 확인
private boolean isSpecialCharacter(char c) {
String specialCharacters = "!@#$%^&*()-_=+[]{};:'\"\\|<>,.?/~`";
return specialCharacters.contains(String.valueOf(c));
}
}
Controller
@Transactional //테스트 후 rollback
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserRestControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private UserJPARepository userJPARepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private WebApplicationContext context;
//Spring Security 테스트 환경 구성
@BeforeEach
public void setup(){
mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
//회원가입 요청 메서드
private ResultActions doPerform(String requestData) throws Exception {
return mvc.perform(
post("/join")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData));
}
@Test
@WithMockUser
@DisplayName("회원가입 성공(가입된 id와 비밀번호)")
public void join_success_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("newuser1234!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("true"));
}
@Test
@WithMockUser
@DisplayName("회원가입-올바르지않은 이메일")
public void join_fail_email_format_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newusernate.com"); //@가 없는 올바르지 않은 이메일
joinDTO.setPassword("newuser1234!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("회원가입-비밀번호 검증")
public void join_fail_password_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("newuser1234"); //특수문자가 없는 비밀번호
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("회원가입-중복 이메일 검증")
public void join_fail_email_duplicated_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("newuser1234!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andExpect(jsonPath("$.success").value("true"));
//중복 이메일
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("회원가입-글자수 검증")
public void join_fail_password_length_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("new12!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
//중복 이메일
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
}
Test Case
- 회원가입 성공(가입되지않은 id와 비밀번호)
- 회원가입 실패
- 이메일 형식 검증 (@가 없음)
- 비밀번호 글자 검증(영문, 숫자, 특수문자 포함, 공백 포함X)
- 중복 이메일 검증
- 비밀번호 글자수 제한 검증
이메일 중복 확인
DTO
public class UserRequest {
@Getter
@Setter
public static class CheckEmailDTO {
private String email;
}
}
Controller
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserJPARepository userRepository;
@PostMapping("/check")
public ResponseEntity<?> check(@RequestBody UserRequest.CheckEmailDTO emailDTO) {
//요청 email 얻기
String email = emailDTO.getEmail();
//repository에서 email이 존재하는지 확인
Optional<User> byEmail = userRepository.findByEmail(email);
if (byEmail.isPresent()) { //email이 이미 존재하면
return ResponseEntity.badRequest().body(ApiUtils.error("동일한 이메일이 존재합니다:email ", HttpStatus.BAD_REQUEST));
} else { //email 중복이 아님
if (email.contains("@")) //이메일 형식이면
return ResponseEntity.ok(ApiUtils.success(null));
else
return ResponseEntity.badRequest().body(ApiUtils.error("이메일 형식으로 작성해주세요:email", HttpStatus.BAD_REQUEST));
}
}
}
이메일 중복 확인, 형식 확인을 하는 API이다.
mock test
@Transactional //테스트 후 rollback
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserRestControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private UserJPARepository userJPARepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private WebApplicationContext context;
//Spring Security 테스트 환경 구성
@BeforeEach
public void setup(){
mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
@WithMockUser
@DisplayName("이메일 확인 테스트 - 이미 존재하는 email")
public void check_fail_duplicated_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user1@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.CheckEmailDTO checkEmailDTO = new UserRequest.CheckEmailDTO();
checkEmailDTO.setEmail("user1@nate.com"); //존재하는 email
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(checkEmailDTO);
//when
mvc.perform(
post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
// verify
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("이메일 확인 테스트 - 올바르지않은 형식")
public void check_fail_format_test() throws Exception {
//given
//요청 body
UserRequest.CheckEmailDTO checkEmailDTO = new UserRequest.CheckEmailDTO();
checkEmailDTO.setEmail("user1nate.com"); //올바르지않은 형식(@가 없음)
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(checkEmailDTO);
//when
mvc.perform(
post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
// verify
.andExpect(jsonPath("$.success").value("false"))
.andExpect(jsonPath("$.error.status").value(400)); //400번 에러
}
}
- Test Case
- 이메일 중복 (이미 가입된 email)
- 잘못된 이메일 형식 (@가 없는 email)
끝냈다....


Q&A
💙 Q1. API 테스트 방식
- 저는 API 테스트를 할 때 Postman이나 MockAPI를 주로 사용하는데, 현업에서 API 테스트를 할 때 주로 어떤 방식을 사용하고, 그 방식을 사용하는 이유는 무엇인가요?
멘토님 👨💻 :
현업에서는 인수 테스트라는 키워드를 찾아보면 좋을 것 같습니다. 저는 RestAssured을 사용합니다. 인수 테스트는 API의 모든 과정을 테스트하는 것을 의미하고, 인수 테스트를 만들면 테스트 자동화가 이루어지기 때문에 사용합니다.
💙 Q2. API 문서 작성 방식, 툴, 작성하는 사람
- 현업에서 API를 설계할 때는 백엔드 개발자의 입김이 더 세나요, 아니면 프론트엔드 개발자의 입김이 더 세나요?
- 현업에서는 어떤 툴로 API 문서를 작성하고, 어느 포지션의 개발자가 작성하나요?
멘토님 👨💻 :
백엔드 개발자가 입김이 더 세지만, 백엔드의 업무가 더 느리거나 특정 상황에서는 프론트 개발자가 맡습니다.
- API 문서는 Swagger같은 자동화 문서로 개발할 수 있습니다. 만약 백엔드 코드가 개발되기 전이라면, 노션으로 간략하게 API 구조를 문서화하는 편입니다.
💙 Q3. 스프링 시큐리티
- 스프링을 사용하는 백엔드 개발자라면 스프링 시큐리티에 대해서 어느정도까지 공부를 해야할까요?
- 스프링 시큐리티에서 유효한 JWT 토큰을 Request로 받았을 때 Controller에 Principle이라는 유저 객체를 넣어주는데, 해당 Principle은 언제 저장이 되어있어서 인자로 넘겨줄 수 있는 것이고, 누가 인자로 넘겨주는건가요?
멘토님 👨💻 :
스프링 시큐리티는 모든 백엔드 개발자에게 필요한 내용은 아닙니다. 스프링 시큐리티는 보안에 관련된 라이브러리인데, 회사마다 보안정책이 달라 커스텀이 어렵습니다. 시큐리티를 모르더라도 취업에 문제는 없지만 JWT에 대해서는 정확하게 알아두는 것이 필요합니다.
- 스프링에서는 ArgumentResolver가 컨트롤러의 인자들을 보고, 필요한 인자들을 넣어주는 객체입니다.
💙 Q4. DTO에 @Data 사용
- DTO에 @Data 어노테이션을 넣는게 Over Spec라는 말을 들은 적 있습니다. 이 때 Over Spec이 라는게 성능적으로 유의미한 영향을 끼친다는 말인지 아니면 객체에게 사용하지 않는 권한을 너무 많이 부여한 건지에 대한 말인지 궁금합니다!
멘토님 👨💻 :
후자(권한초과부여)에 가깝습니다. 그러나 setter를 제공하는게 지나친 권한을 주는 것 같다고 생각합니다.
💙 Q5. ? : 와일드카드
- ApiResult<?> error(String message, HttpStatus status) 코드에서 <?>는 무엇을 의미하는건가요?
멘토님 👨💻 :
자바의 와일드 카드입니다. 제네릭과 와일드카드를 공부해보면 좋을 것 같습니다.
- ? Extends Integer → 타입이 강제된 형태로 사용이 가능합니다.
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL [0714] : 3주차 과제 수행 (0) | 2023.07.14 |
---|---|
TIL[0712] : 3주차 강의 (DTO, 스트림, HTTP, JDBC, JPA) (0) | 2023.07.10 |
TIL[0706] : 2주차 과제 - Mock Controller 2 (0) | 2023.07.06 |
TIL [0705] : 2주차 과제 -1. Restful API, 2. Mock Controller 일부 (0) | 2023.07.05 |
카테캠 : 1주차 코드리뷰 (0) | 2023.07.04 |
주문
결제하기(주문 저장하기)
Controller
@PostMapping("/save") // /orders/save
public ResponseEntity<?> saveOrder() {
OrderRespDTO responseDTO = null;
//ItemInfo 담을 리스트 생성
List<ItemInfoDTO> itemInfoDTOList = new ArrayList<>();
//ItemInfo 리스트에 담기
ItemInfoDTO itemInfoDTO1 = ItemInfoDTO.builder()
.id(4)
.optionName("01. 슬라이딩 지퍼백 크리스마스에디션 4종")
.quantity(10)
.price(100000)
.build();
itemInfoDTOList.add(itemInfoDTO1);
ItemInfoDTO itemInfoDTO2 = ItemInfoDTO.builder()
.id(5)
.optionName("02. 슬라이딩 지퍼백 플라워에디션 5종")
.quantity(10)
.price(109000)
.build();
itemInfoDTOList.add(itemInfoDTO2);
//ProductItem 리스트 만들기
List<ProductItemDTO> productItemDTOList = new ArrayList<>();
ProductItemDTO productItemDTO1 = ProductItemDTO.builder()
.productName("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전")
.items(itemInfoDTOList)
.build();
//ProductItem 리스트에 담기
productItemDTOList.add(productItemDTO1);
//응답할 dto 생성
responseDTO = OrderRespDTO.builder()
.id(2)
.products(productItemDTOList)
.totalPrice(209000)
.build();
return ResponseEntity.ok(ApiUtils.success(responseDTO));
}
✳️ 주문 결과 확인과 결제하기는 같은 응답구조를 가지므로 동일한 DTO를 사용했다.
주문 결과 확인 코드는 여기서 ⬇️
https://shout-to-my-mae.tistory.com/309
TIL[0706] : 2주차 과제 - Mock Controller 2
상품 상품 상세 보기 Mock GET http://localhost:8080/products/:id JSON { "success": true, "response": { "id": 1, "productName": "기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전", "description":
shout-to-my-mae.tistory.com
Mock Test
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DisplayName("결제하기(주문 인서트)")
class OrderRestControllerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser //인증된 사용자 생성
// 결제하기(주문 인서트)
public void saveOrder_test() throws Exception {
// when
ResultActions resultActions = mvc.perform(
post("/orders/save")
);
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : " + responseBody);
// verify
resultActions.andExpect(jsonPath("$.success").value("true"));
resultActions.andExpect(jsonPath("$.response.id").value(2));
resultActions.andExpect(jsonPath("$.response.totalPrice").value(209000));
resultActions.andExpect(jsonPath("$.response.products[0].productName").value("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전"));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].id").value(4));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.products[0].items[0].price").value(100000));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].id").value(5));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].optionName").value("02. 슬라이딩 지퍼백 플라워에디션 5종"));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].quantity").value(10));
resultActions.andExpect(jsonPath("$.response.products[0].items[1].price").value(109000));
}
}
유저
로그인
DTO
public class UserRequest {
@Getter
@Setter
public static class LoginDTO {
private String email;
private String password;
}
}
Controller
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserJPARepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestHeader(value = "Authorization", required = false) String authHeader, @RequestBody UserRequest.LoginDTO loginDTO) {
//검증 단계
String email = loginDTO.getEmail();
String password = loginDTO.getPassword();
//올바른 이메일 형식인지 확인
if (!email.contains("@"))
return ResponseEntity.badRequest().body(ApiUtils.error("이메일 형식으로 작성해주세요:email", HttpStatus.BAD_REQUEST));
//유효한 비밀번호인지 확인
if (!isValidPassword(password))
return ResponseEntity.badRequest().body(ApiUtils.error("영문, 숫자, 특수문자가 포함되어야하고 공백이 포함될 수 없습니다.:password", HttpStatus.BAD_REQUEST));
//인증 확인
if (authHeader == null || authHeader.isEmpty())
return ResponseEntity.badRequest().body(ApiUtils.error("인증되지 않았습니다", HttpStatus.UNAUTHORIZED));
//비밀번호 길이 검증
int passwordLength = loginDTO.getPassword().length();
if(!(passwordLength>=8 && passwordLength <= 20))
return ResponseEntity.badRequest().body(ApiUtils.error("8에서 20자 이내여야 합니다.:password", HttpStatus.BAD_REQUEST));
//로그인 수행
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(loginDTO.getEmail(), loginDTO.getPassword());
Authentication authentication;
//로그인 성공, 실패 여부 확인
try {
authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
}catch (Exception e){
System.out.println(e.getMessage());
return ResponseEntity.badRequest().body(ApiUtils.error("email 또는 password가 올바르지 않습니다", HttpStatus.BAD_REQUEST));
}
CustomUserDetails myUserDetails = (CustomUserDetails) authentication.getPrincipal();
String jwt = JWTProvider.create(myUserDetails.getUser());
//로그인 성공
return ResponseEntity.ok().header(JWTProvider.HEADER, jwt).body(ApiUtils.success(null));
}
//비밀번호 유효성 검사
private boolean isValidPassword(String password) {
boolean hasLetter = false; //문자 여부
boolean hasDigit = false; //숫자 여부
boolean hasSpecialCharacter = false; //특수문자 여부
for (char c:password.toCharArray()){
if(Character.isLetter(c)) hasLetter=true;
else if (Character.isDigit(c)) hasDigit = true;
else if (isSpecialCharacter(c)) hasSpecialCharacter = true;
if(hasLetter && hasDigit && hasSpecialCharacter) break;
}
//문자,숫자,특수문자가 있어야하고, 공백이 없어야한다.
return hasLetter && hasDigit && hasSpecialCharacter && !password.contains(" ");
}
//특수 문자 포함하는지 확인
private boolean isSpecialCharacter(char c) {
String specialCharacters = "!@#$%^&*()-_=+[]{};:'\"\\|<>,.?/~`";
return specialCharacters.contains(String.valueOf(c));
}
}
- 로그인 성공, 로그인 실패(형식 / 문자 / 인증 / 비밀번호 길이)의 경우를 구현했다.
- API 문서에는 기능이 구현되어있지만, 문자 포함 여부와 비밀번호 길이 검증은 회원가입시 이미 검증하므로 필요없을 것 같다.
- 로그인 시마다 jwt 토큰은 항상 달라지는데, 요청시 토큰을 필요로 하는 이유가 궁금하다.
Spring Security 로그인 Mock Test
@Transactional //테스트 후 rollback
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserRestControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private UserJPARepository userJPARepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private WebApplicationContext context;
//Spring Security 테스트 환경 구성
@BeforeEach
public void setup(){
mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
@WithMockUser
@DisplayName("로그인 성공(가입된 id와 비밀번호)")
public void login_success_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user1@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("user1@nate.com");
loginDTO.setPassword("user1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("true"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 -가입된 id와 잘못된 비밀번호")
public void login_fail_pw_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("user@nate.com");
loginDTO.setPassword("wrongpassword!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 존재하지 않는 id와 비밀번호 (미가입)")
public void login_fail_unregistered_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com"); //이미 존재하는 id
loginDTO.setPassword("fake1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 이메일 형식 검증")
public void login_fail_email_format_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newusernate.com"); //올바르지 않은 이메일 (@가 없음)
loginDTO.setPassword("user1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 비밀번호 글자 검증")
public void login_fail_password_character_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com");
loginDTO.setPassword("user1234"); //특수문자가 없는 비밀번호
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//jwt Token
String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyMTIzMzZAbmF0ZS5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWQiOjEsImV4cCI6MTY4ODg5ODkxNn0.2ovT4QRQHAKFsjHZG1g_bFwC3RN9-3TxdgS_gMm3FKVstqrqPrw6C0VZEwmh5buZzz3ek3Ez_Z3IsNqiVnONcQ";
//when
mvc.perform(
post("/login")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 인증되지 않은 유저")
public void login_fail_unauth_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com");
loginDTO.setPassword("user1234!");
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//when
mvc.perform( //토큰 보내지 않음
post("/login")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("로그인 실패 - 비밀번호 글자수")
public void login_fail_password_length_test() throws Exception {
//given
//요청 body
UserRequest.LoginDTO loginDTO = new UserRequest.LoginDTO();
loginDTO.setEmail("newuser@nate.com");
loginDTO.setPassword("us4!"); //적은 글자수의 비밀번호
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(loginDTO);
//when
mvc.perform( //토큰 보내지 않음
post("/login")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
}
테스트 케이스는 다음과 같다.
- 로그인 성공(가입된 id와 비밀번호)
- 로그인 실패
- 가입된 id와 잘못된 비밀번호
- 존재하지 않는 id와 비밀번호 (미가입)
- 형식, 중복 검증
- 올바르지 않은 이메일
- 이메일 형식 검증 (@가 없음)
- 비밀번호 글자 검증(영문, 숫자, 특수문자 포함, 공백 포함X)
- 중복 이메일 검증
- 비밀번호 글자수 제한 검증회원가입
@Transactional 어노테이션을 붙여 테스트 후 rollback 되도록 하였다.
DTO
public class UserRequest {
@Getter
@Setter
public static class JoinDTO {
private String email;
private String password;
private String username;
}
}
Controller
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserJPARepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@PostMapping("/join")
public ResponseEntity<?> join(@RequestBody UserRequest.JoinDTO joinDTO) {
//검증
String email = joinDTO.getEmail();
String password = joinDTO.getPassword();
//올바른 이메일 형식인지 확인
if (!email.contains("@"))
return ResponseEntity.badRequest().body(ApiUtils.error("이메일 형식으로 작성해주세요:email", HttpStatus.BAD_REQUEST));
//유효한 비밀번호인지 확인
if (!isValidPassword(password))
return ResponseEntity.badRequest().body(ApiUtils.error("영문, 숫자, 특수문자가 포함되어야하고 공백이 포함될 수 없습니다.:password", HttpStatus.BAD_REQUEST));
//동일한 이메일이 존재하는지 확인
UserRequest.CheckEmailDTO checkEmailDTO = new UserRequest.CheckEmailDTO();
checkEmailDTO.setEmail(email);
ResponseEntity<?> responseEntity = check(checkEmailDTO); //check 메서드 사용
boolean isSuccessful = responseEntity.getStatusCode().is2xxSuccessful();
if (!isSuccessful)
return ResponseEntity.badRequest().body(ApiUtils.error("동일한 이메일이 존재합니다 : "+email, HttpStatus.BAD_REQUEST));
//비밀번호 길이 검증
int passwordLength = password.length();
if(!(passwordLength>=8 && passwordLength <= 20))
return ResponseEntity.badRequest().body(ApiUtils.error("8에서 20자 이내여야 합니다.:password", HttpStatus.BAD_REQUEST));
//회원가입 성공
//유저 생성
User user = User.builder()
.email(joinDTO.getEmail())
.password(passwordEncoder.encode(joinDTO.getPassword()))
.username(joinDTO.getUsername())
.roles("ROLE_USER")
.build();
//repo에 저장
userRepository.save(user);
return ResponseEntity.ok(ApiUtils.success(null));
}
private boolean isValidPassword(String password) {
boolean hasLetter = false; //문자 여부
boolean hasDigit = false; //숫자 여부
boolean hasSpecialCharacter = false; //특수문자 여부
for (char c:password.toCharArray()){
if(Character.isLetter(c)) hasLetter=true;
else if (Character.isDigit(c)) hasDigit = true;
else if (isSpecialCharacter(c)) hasSpecialCharacter = true;
if(hasLetter && hasDigit && hasSpecialCharacter) break;
}
//문자,숫자,특수문자가 있어야하고, 공백이 없어야한다.
return hasLetter && hasDigit && hasSpecialCharacter && !password.contains(" ");
}
//특수 문자 포함하는지 확인
private boolean isSpecialCharacter(char c) {
String specialCharacters = "!@#$%^&*()-_=+[]{};:'\"\\|<>,.?/~`";
return specialCharacters.contains(String.valueOf(c));
}
}
Controller
@Transactional //테스트 후 rollback
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserRestControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private UserJPARepository userJPARepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private WebApplicationContext context;
//Spring Security 테스트 환경 구성
@BeforeEach
public void setup(){
mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
//회원가입 요청 메서드
private ResultActions doPerform(String requestData) throws Exception {
return mvc.perform(
post("/join")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData));
}
@Test
@WithMockUser
@DisplayName("회원가입 성공(가입된 id와 비밀번호)")
public void join_success_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("newuser1234!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("true"));
}
@Test
@WithMockUser
@DisplayName("회원가입-올바르지않은 이메일")
public void join_fail_email_format_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newusernate.com"); //@가 없는 올바르지 않은 이메일
joinDTO.setPassword("newuser1234!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("회원가입-비밀번호 검증")
public void join_fail_password_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("newuser1234"); //특수문자가 없는 비밀번호
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("회원가입-중복 이메일 검증")
public void join_fail_email_duplicated_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("newuser1234!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
doPerform(requestData)
.andExpect(jsonPath("$.success").value("true"));
//중복 이메일
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("회원가입-글자수 검증")
public void join_fail_password_length_test() throws Exception {
//given
//유저 생성
UserRequest.JoinDTO joinDTO = new JoinDTO();
joinDTO.setUsername("newuser");
joinDTO.setEmail("newuser@nate.com");
joinDTO.setPassword("new12!");
//JSON 문자열로 변경
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(joinDTO);
//when
//중복 이메일
doPerform(requestData)
.andDo(print()) //결과 출력
//then
.andExpect(jsonPath("$.success").value("false"));
}
}
Test Case
- 회원가입 성공(가입되지않은 id와 비밀번호)
- 회원가입 실패
- 이메일 형식 검증 (@가 없음)
- 비밀번호 글자 검증(영문, 숫자, 특수문자 포함, 공백 포함X)
- 중복 이메일 검증
- 비밀번호 글자수 제한 검증
이메일 중복 확인
DTO
public class UserRequest {
@Getter
@Setter
public static class CheckEmailDTO {
private String email;
}
}
Controller
@RestController
@RequiredArgsConstructor
public class UserRestController {
private final UserJPARepository userRepository;
@PostMapping("/check")
public ResponseEntity<?> check(@RequestBody UserRequest.CheckEmailDTO emailDTO) {
//요청 email 얻기
String email = emailDTO.getEmail();
//repository에서 email이 존재하는지 확인
Optional<User> byEmail = userRepository.findByEmail(email);
if (byEmail.isPresent()) { //email이 이미 존재하면
return ResponseEntity.badRequest().body(ApiUtils.error("동일한 이메일이 존재합니다:email ", HttpStatus.BAD_REQUEST));
} else { //email 중복이 아님
if (email.contains("@")) //이메일 형식이면
return ResponseEntity.ok(ApiUtils.success(null));
else
return ResponseEntity.badRequest().body(ApiUtils.error("이메일 형식으로 작성해주세요:email", HttpStatus.BAD_REQUEST));
}
}
}
이메일 중복 확인, 형식 확인을 하는 API이다.
mock test
@Transactional //테스트 후 rollback
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserRestControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private UserJPARepository userJPARepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private WebApplicationContext context;
//Spring Security 테스트 환경 구성
@BeforeEach
public void setup(){
mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
@WithMockUser
@DisplayName("이메일 확인 테스트 - 이미 존재하는 email")
public void check_fail_duplicated_test() throws Exception {
//given
//user 생성
User user = User.builder()
.email("user1@nate.com")
.password(passwordEncoder.encode("user1234!"))
.username("user")
.roles("ROLE_USER")
.build();
//저장
userJPARepository.save(user);
//요청 body
UserRequest.CheckEmailDTO checkEmailDTO = new UserRequest.CheckEmailDTO();
checkEmailDTO.setEmail("user1@nate.com"); //존재하는 email
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(checkEmailDTO);
//when
mvc.perform(
post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
// verify
.andExpect(jsonPath("$.success").value("false"));
}
@Test
@WithMockUser
@DisplayName("이메일 확인 테스트 - 올바르지않은 형식")
public void check_fail_format_test() throws Exception {
//given
//요청 body
UserRequest.CheckEmailDTO checkEmailDTO = new UserRequest.CheckEmailDTO();
checkEmailDTO.setEmail("user1nate.com"); //올바르지않은 형식(@가 없음)
ObjectMapper objectMapper = new ObjectMapper();
String requestData = objectMapper.writeValueAsString(checkEmailDTO);
//when
mvc.perform(
post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(requestData))
.andDo(print()) //결과 출력
// verify
.andExpect(jsonPath("$.success").value("false"))
.andExpect(jsonPath("$.error.status").value(400)); //400번 에러
}
}
- Test Case
- 이메일 중복 (이미 가입된 email)
- 잘못된 이메일 형식 (@가 없는 email)
끝냈다....


Q&A
💙 Q1. API 테스트 방식
- 저는 API 테스트를 할 때 Postman이나 MockAPI를 주로 사용하는데, 현업에서 API 테스트를 할 때 주로 어떤 방식을 사용하고, 그 방식을 사용하는 이유는 무엇인가요?
멘토님 👨💻 :
현업에서는 인수 테스트라는 키워드를 찾아보면 좋을 것 같습니다. 저는 RestAssured을 사용합니다. 인수 테스트는 API의 모든 과정을 테스트하는 것을 의미하고, 인수 테스트를 만들면 테스트 자동화가 이루어지기 때문에 사용합니다.
💙 Q2. API 문서 작성 방식, 툴, 작성하는 사람
- 현업에서 API를 설계할 때는 백엔드 개발자의 입김이 더 세나요, 아니면 프론트엔드 개발자의 입김이 더 세나요?
- 현업에서는 어떤 툴로 API 문서를 작성하고, 어느 포지션의 개발자가 작성하나요?
멘토님 👨💻 :
백엔드 개발자가 입김이 더 세지만, 백엔드의 업무가 더 느리거나 특정 상황에서는 프론트 개발자가 맡습니다.
- API 문서는 Swagger같은 자동화 문서로 개발할 수 있습니다. 만약 백엔드 코드가 개발되기 전이라면, 노션으로 간략하게 API 구조를 문서화하는 편입니다.
💙 Q3. 스프링 시큐리티
- 스프링을 사용하는 백엔드 개발자라면 스프링 시큐리티에 대해서 어느정도까지 공부를 해야할까요?
- 스프링 시큐리티에서 유효한 JWT 토큰을 Request로 받았을 때 Controller에 Principle이라는 유저 객체를 넣어주는데, 해당 Principle은 언제 저장이 되어있어서 인자로 넘겨줄 수 있는 것이고, 누가 인자로 넘겨주는건가요?
멘토님 👨💻 :
스프링 시큐리티는 모든 백엔드 개발자에게 필요한 내용은 아닙니다. 스프링 시큐리티는 보안에 관련된 라이브러리인데, 회사마다 보안정책이 달라 커스텀이 어렵습니다. 시큐리티를 모르더라도 취업에 문제는 없지만 JWT에 대해서는 정확하게 알아두는 것이 필요합니다.
- 스프링에서는 ArgumentResolver가 컨트롤러의 인자들을 보고, 필요한 인자들을 넣어주는 객체입니다.
💙 Q4. DTO에 @Data 사용
- DTO에 @Data 어노테이션을 넣는게 Over Spec라는 말을 들은 적 있습니다. 이 때 Over Spec이 라는게 성능적으로 유의미한 영향을 끼친다는 말인지 아니면 객체에게 사용하지 않는 권한을 너무 많이 부여한 건지에 대한 말인지 궁금합니다!
멘토님 👨💻 :
후자(권한초과부여)에 가깝습니다. 그러나 setter를 제공하는게 지나친 권한을 주는 것 같다고 생각합니다.
💙 Q5. ? : 와일드카드
- ApiResult<?> error(String message, HttpStatus status) 코드에서 <?>는 무엇을 의미하는건가요?
멘토님 👨💻 :
자바의 와일드 카드입니다. 제네릭과 와일드카드를 공부해보면 좋을 것 같습니다.
- ? Extends Integer → 타입이 강제된 형태로 사용이 가능합니다.
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL [0714] : 3주차 과제 수행 (0) | 2023.07.14 |
---|---|
TIL[0712] : 3주차 강의 (DTO, 스트림, HTTP, JDBC, JPA) (0) | 2023.07.10 |
TIL[0706] : 2주차 과제 - Mock Controller 2 (0) | 2023.07.06 |
TIL [0705] : 2주차 과제 -1. Restful API, 2. Mock Controller 일부 (0) | 2023.07.05 |
카테캠 : 1주차 코드리뷰 (0) | 2023.07.04 |