반응형
💋 오늘 공부한 내용
- 장바구니 미션 테스트 코드를 드디어 작성했다! 어려운 점이 몇 가지 있었는데, 박스터랑 누누, 허브, 말랑, 주노에게 많이 물어봐서 대부분 해결할 수 있었다!
- 먼저 어려웠던 점 1: 로그인을 해서 인가를 받은 사용자만 할 수 있는 기능에 대해서 어떻게 테스트해야 하는지 궁금했다.
- 허브가 Authorization을 헤더에 늘 하던 것처럼 같이 보내라고 해서 해결했다. auth().preemtive()가 너무 무섭게 생겨서 막막했는데, auth는 Authorization 헤더에 설정한다는 내용을 좀 더 편하게 쓸 수 있는 것, preemtives는 잘 모르겠지만, 너무 마이너해지므로 일단 넘어가기로 했다.
- 뭔가 저번에 박스터랑 페어할 때부터 느낀건데, 박스터는 에러가 날 것을 알면서 피해가는 것 같을 때가 있었다. 테스트 코드를 작성하면서 필요한 빈들에 대해서 생성자 주입이 아니라 필드 주입을 하면서, 별다른 설명이 없어서 귀찮아서 그런가..? 싶었는데 알고보니 @SpringBootTest에서는 필드 주입 방식을 사용해야 한다고 한다. 구체적인 이유를 알려줬지만, 잘 모르겠다 ><
- 어려웠던 점 2: 아래 테스트가 단독으로는 되는데 자꾸 전체 테스트를 하면 혼자 깨졌다...
- 잘못하고 있던 부분은, auto increment를 통해서 받을 수 있는 id를 직접 설정해서 save 하고 있었던 것이었다!
- 여러 번 save하게 되면, 내가 원하는 아이디로 저장되지 않을 수 있다는 것....! 한 번에 여러 가지를 함께 돌리면 테스트가 깨지는 이유였다.
@Test
void updateTest() {
productDao.save(new Product(1L, "깃짱", "gitchan.img", 1000));
Product boxster = productDao.update(new Product(1L, "박스터", "boxster.img", 5000));
Product foundProduct = productDao.findById(boxster.getId());
assertTrue(boxster.getName().equals(foundProduct.getName()));
}
- 아래와 같이 수정했다. (박스터씨의 도움)
@Test
void updateTest() {
final Product gitchan = productDao.save(new Product("깃짱", "gitchan.img", 1000));
Product boxster = productDao.update(new Product(gitchan.getId(), "박스터", "boxster.img", 5000));
Product foundProduct = productDao.findById(boxster.getId());
assertTrue(boxster.getName().equals(foundProduct.getName()));
}
역시... 저장하고 나서 객체를 받아와야 제대로 id 값을 받을 수 있다...! ㅎㅎㅎㅎㅎㅎ
- 어려웠던 점 3: Argument Resolver를 통해서 request 헤더의 Authentication 내에 들어있는 인증과 관련된 부분을 처리했다.
- 로그인이 불가능한 경우에, 401 상태코드를 곧바로 반환하고 싶어서, 아래와 같이 코드를 썼었다. (주석만 슬쩍 보면 됨)
@Component
public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver {
private static final String BASIC_TYPE_PREFIX = "Basic";
private static final String DELIMITER = ":";
private final MemberDao memberDao;
public AuthenticatedMemberArgumentResolver(final MemberDao memberDao) {
this.memberDao = memberDao;
}
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthenticatedMember.class)
&& Member.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) throws Exception {
final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
final String authorization = request.getHeader("Authorization");
final boolean isBasicAuthentication = authorization != null && authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());
if (!isBasicAuthentication) {
return new ResponseEntity<>("Unauthorized", HttpStatus.UNAUTHORIZED); // 곧바로 401 상태코드를 포함한 채로 반환
}
final Member authenticatedMember = findMemberFromAuthentication(authorization);
if (authenticatedMember == null) {
return new ResponseEntity<>("Unauthorized", HttpStatus.UNAUTHORIZED); // 곧바로 401 상태코드를 포함한 채로 반환
}
return authenticatedMember;
}
private Member findMemberFromAuthentication(final String authorization) {
String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
String decodedString = new String(decodedBytes);
String[] credentials = decodedString.split(DELIMITER);
String email = credentials[0];
String password = credentials[1];
return memberDao.findByEmailAndPassword(email, password).orElse(null);
}
}
그리고 곧바로 접근 권한이 없는데 물건을 지우겠다거나 한다면 401 상태코드를 반환한다는 테스트 코드를 짰다.
@Test
public void deleteItemWhenNotLoggedInTest() {
RestAssured.given()
.log().all()
.when()
.delete("/carts/1")
.then()
.log().all()
.statusCode(HttpStatus.UNAUTHORIZED.value());
}
근데 이게 에러가 뜨는 것...!
- 이유는... ArgumentResolver에서 반환한 값이 그대로 파라미터로 @AuthenticatedMember Member 를 만나서 바인딩이 되어버리는데, ResponseEntity 형태는 Member 객체로 바인딩할 수 없어서 나타나는 (ClassCastException 비슷한) 것이었다...!
- 해결 방법은... ArgumentResolver에서 예외를 던지고, 그것을 ControllerAdvice에서 받아서 처리하는 식으로 해결했다...
@Component
public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumentResolver {
// ...
private final MemberDao memberDao;
public AuthenticatedMemberArgumentResolver(final MemberDao memberDao) {
this.memberDao = memberDao;
}
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthenticatedMember.class)
&& Member.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) throws Exception {
final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
final String authorization = request.getHeader("Authorization");
final boolean isBasicAuthentication = authorization != null && authorization.toLowerCase().startsWith(BASIC_TYPE_PREFIX.toLowerCase());
if (!isBasicAuthentication) {
throw new UnAuthenticatedMemberException("로그인 해 주세요!"); // 커스텀 예외 만들어서 던짐
}
final Member authenticatedMember = findMemberFromAuthentication(authorization);
if (authenticatedMember == null) {
throw new UnAuthenticatedMemberException("로그인 해 주세요!"); // 커스텀 예외 만들어서 던짐
}
return authenticatedMember;
}
private Member findMemberFromAuthentication(final String authorization) {
String authHeaderValue = authorization.substring(BASIC_TYPE_PREFIX.length()).trim();
byte[] decodedBytes = Base64.decodeBase64(authHeaderValue);
String decodedString = new String(decodedBytes);
String[] credentials = decodedString.split(DELIMITER);
String email = credentials[0];
String password = credentials[1];
return memberDao.findByEmailAndPassword(email, password).orElse(null);
}
}
그리고 이 커스텀 예외를 처리하는 코드
@ExceptionHandler(UnAuthenticatedMemberException.class)
public ResponseEntity<String> handleUnAuthenticatedMemberException(UnAuthenticatedMemberException e, HttpServletResponse response) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("Unauthorized");
}
💋 감정 회고
- 천재 박스터 최고... 전에 스프링을 좀 공부해서 나의 질문들에 잘 대답해줄 수 있는 크루들이 있어서 다행이다.
💋 오늘의 포스팅
반응형
'TIL > 2023' 카테고리의 다른 글
[TIL] 23.05.12 (5) | 2023.05.13 |
---|---|
[TIL] 23.05.08 (0) | 2023.05.08 |
[TIL] 23.05.04 (0) | 2023.05.04 |
[TIL] 23.05.03 (2) | 2023.05.03 |
[TIL] 23.04.28 (0) | 2023.04.29 |