diff --git a/src/main/java/nextstep/auth/AuthConfig.java b/src/main/java/nextstep/auth/AuthConfig.java index e17edc191..90d9ad340 100644 --- a/src/main/java/nextstep/auth/AuthConfig.java +++ b/src/main/java/nextstep/auth/AuthConfig.java @@ -18,15 +18,15 @@ @RequiredArgsConstructor @Configuration public class AuthConfig implements WebMvcConfigurer { - private final UserDetailService loginMemberService; + private final UserDetailService userDetailService; private final JwtTokenProvider jwtTokenProvider; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityContextPersistenceFilter()); - registry.addInterceptor(new UsernamePasswordAuthFilter(loginMemberService)).addPathPatterns("/login/form"); - registry.addInterceptor(new TokenAuthInterceptor(loginMemberService, jwtTokenProvider)).addPathPatterns("/login/token"); - registry.addInterceptor(new BasicAuthFilter(loginMemberService)); + registry.addInterceptor(new UsernamePasswordAuthFilter(userDetailService)).addPathPatterns("/login/form"); + registry.addInterceptor(new TokenAuthInterceptor(userDetailService, jwtTokenProvider)).addPathPatterns("/login/token"); + registry.addInterceptor(new BasicAuthFilter(userDetailService)); registry.addInterceptor(new BearerTokenAuthFilter(jwtTokenProvider)); } diff --git a/src/main/java/nextstep/auth/authentication/BasicAuthFilter.java b/src/main/java/nextstep/auth/authentication/BasicAuthFilter.java index 52400bc11..f4ef7d39d 100644 --- a/src/main/java/nextstep/auth/authentication/BasicAuthFilter.java +++ b/src/main/java/nextstep/auth/authentication/BasicAuthFilter.java @@ -2,19 +2,19 @@ import lombok.RequiredArgsConstructor; import nextstep.auth.context.Authentication; -import nextstep.auth.context.SecurityContextHolder; +import nextstep.auth.user.User; import nextstep.auth.user.UserDetail; import nextstep.auth.user.UserDetailService; -import nextstep.auth.interceptor.AuthChainInterceptor; +import nextstep.auth.interceptor.AuthContextChainInterceptor; import org.apache.tomcat.util.codec.binary.Base64; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; @RequiredArgsConstructor -public class BasicAuthFilter extends AuthChainInterceptor { +public class BasicAuthFilter extends AuthContextChainInterceptor { private final UserDetailService userDetailService; + @Override protected void checkValidAuth(final AuthenticationToken token) { UserDetail loginMember = userDetailService.loadUserByUsername(token.getPrincipal()); if (loginMember == null) { @@ -25,12 +25,14 @@ protected void checkValidAuth(final AuthenticationToken token) { } } + @Override protected Authentication getAuthentication(final AuthenticationToken token) { UserDetail loginMember = userDetailService.loadUserByUsername(token.getPrincipal()); return new Authentication(loginMember.getEmail(), loginMember.getAuthorities()); } - protected AuthenticationToken getAuthenticationToken(final HttpServletRequest request) { + @Override + protected AuthenticationToken createAuthToken(final HttpServletRequest request) { String authCredentials = AuthorizationExtractor.extract(request, AuthorizationType.BASIC); String authHeader = new String(Base64.decodeBase64(authCredentials)); diff --git a/src/main/java/nextstep/auth/authentication/BearerTokenAuthFilter.java b/src/main/java/nextstep/auth/authentication/BearerTokenAuthFilter.java index 66b7ea163..03f842432 100644 --- a/src/main/java/nextstep/auth/authentication/BearerTokenAuthFilter.java +++ b/src/main/java/nextstep/auth/authentication/BearerTokenAuthFilter.java @@ -3,21 +3,23 @@ import lombok.RequiredArgsConstructor; import nextstep.auth.context.Authentication; import nextstep.auth.token.JwtTokenProvider; -import nextstep.auth.interceptor.AuthChainInterceptor; +import nextstep.auth.interceptor.AuthContextChainInterceptor; import javax.servlet.http.HttpServletRequest; import java.util.List; @RequiredArgsConstructor -public class BearerTokenAuthFilter extends AuthChainInterceptor { +public class BearerTokenAuthFilter extends AuthContextChainInterceptor { private final JwtTokenProvider jwtTokenProvider; + @Override protected void checkValidAuth(final AuthenticationToken token) { if (!jwtTokenProvider.validateToken(token.getPrincipal())) { throw new AuthenticationException(); } } + @Override protected Authentication getAuthentication(final AuthenticationToken token) { String principal = jwtTokenProvider.getPrincipal(token.getPrincipal()); List roles = jwtTokenProvider.getRoles(token.getPrincipal()); @@ -26,7 +28,8 @@ protected Authentication getAuthentication(final AuthenticationToken token) { return authentication; } - protected AuthenticationToken getAuthenticationToken(final HttpServletRequest request) { + @Override + protected AuthenticationToken createAuthToken(final HttpServletRequest request) { String authCredentials = AuthorizationExtractor.extract(request, AuthorizationType.BEARER); return new AuthenticationToken(authCredentials, authCredentials); } diff --git a/src/main/java/nextstep/auth/authentication/UsernamePasswordAuthFilter.java b/src/main/java/nextstep/auth/authentication/UsernamePasswordAuthFilter.java index 1c8e7bfab..1d276be95 100644 --- a/src/main/java/nextstep/auth/authentication/UsernamePasswordAuthFilter.java +++ b/src/main/java/nextstep/auth/authentication/UsernamePasswordAuthFilter.java @@ -2,8 +2,9 @@ import nextstep.auth.context.Authentication; import nextstep.auth.context.SecurityContextHolder; -import nextstep.auth.user.UserDetail; import nextstep.auth.interceptor.AuthNotChainInterceptor; +import nextstep.auth.user.User; +import nextstep.auth.user.UserDetail; import nextstep.auth.user.UserDetailService; import javax.servlet.http.HttpServletRequest; @@ -27,8 +28,8 @@ protected AuthenticationToken createAuthToken(final HttpServletRequest request) } @Override - protected void afterSuccessUserCheck(final HttpServletRequest request, final HttpServletResponse response, UserDetail userDetail) { - Authentication authentication = new Authentication(userDetail.getEmail(), userDetail.getAuthorities()); + protected void afterSuccessUserCheck(final HttpServletRequest request, final HttpServletResponse response, UserDetail user) { + Authentication authentication = new Authentication(user.getEmail(), user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } diff --git a/src/main/java/nextstep/auth/authorization/AuthenticationPrincipalArgumentResolver.java b/src/main/java/nextstep/auth/authorization/AuthenticationPrincipalArgumentResolver.java index 10317cab0..741767dea 100644 --- a/src/main/java/nextstep/auth/authorization/AuthenticationPrincipalArgumentResolver.java +++ b/src/main/java/nextstep/auth/authorization/AuthenticationPrincipalArgumentResolver.java @@ -2,7 +2,8 @@ import nextstep.auth.context.Authentication; import nextstep.auth.context.SecurityContextHolder; -import nextstep.member.domain.LoginMember; +import nextstep.auth.user.User; +import nextstep.auth.user.UserDetail; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -16,12 +17,12 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public LoginMember resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + public UserDetail resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { - return LoginMember.guest(); + return User.guest(); } - return LoginMember.of(authentication.getPrincipal().toString(), authentication.getAuthorities()); + return User.of(authentication.getPrincipal().toString(), authentication.getAuthorities()); } } diff --git a/src/main/java/nextstep/auth/interceptor/AuthChainInterceptor.java b/src/main/java/nextstep/auth/interceptor/AuthContextChainInterceptor.java similarity index 80% rename from src/main/java/nextstep/auth/interceptor/AuthChainInterceptor.java rename to src/main/java/nextstep/auth/interceptor/AuthContextChainInterceptor.java index 639ce4e37..1745be99e 100644 --- a/src/main/java/nextstep/auth/interceptor/AuthChainInterceptor.java +++ b/src/main/java/nextstep/auth/interceptor/AuthContextChainInterceptor.java @@ -8,12 +8,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public abstract class AuthChainInterceptor implements HandlerInterceptor { +public abstract class AuthContextChainInterceptor implements HandlerInterceptor { @Override public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler){ try { - AuthenticationToken token = getAuthenticationToken(request); + AuthenticationToken token = createAuthToken(request); checkValidAuth(token); saveAuthContext(token); return true; @@ -26,7 +26,7 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp protected abstract Authentication getAuthentication(final AuthenticationToken token); - protected abstract AuthenticationToken getAuthenticationToken(final HttpServletRequest request); + protected abstract AuthenticationToken createAuthToken(final HttpServletRequest request); private void saveAuthContext(final AuthenticationToken token) { Authentication authentication = getAuthentication(token); diff --git a/src/main/java/nextstep/auth/interceptor/AuthNotChainInterceptor.java b/src/main/java/nextstep/auth/interceptor/AuthNotChainInterceptor.java index 43f1a1b90..39107123d 100644 --- a/src/main/java/nextstep/auth/interceptor/AuthNotChainInterceptor.java +++ b/src/main/java/nextstep/auth/interceptor/AuthNotChainInterceptor.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import nextstep.auth.authentication.AuthenticationException; import nextstep.auth.authentication.AuthenticationToken; +import nextstep.auth.user.User; import nextstep.auth.user.UserDetail; import nextstep.auth.user.UserDetailService; import org.springframework.web.servlet.HandlerInterceptor; @@ -19,9 +20,9 @@ public abstract class AuthNotChainInterceptor implements HandlerInterceptor { public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler){ try { AuthenticationToken authToken = createAuthToken(request); - checkValidUser(authToken); - UserDetail userDetail = getUserDetail(authToken); - afterSuccessUserCheck(request, response, userDetail); + checkValidAuth(authToken); + UserDetail user = getUserDetail(authToken); + afterSuccessUserCheck(request, response, user); return false; } catch (Exception e) { return true; @@ -30,10 +31,10 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp protected abstract AuthenticationToken createAuthToken(final HttpServletRequest request) throws IOException; - protected abstract void afterSuccessUserCheck(final HttpServletRequest request, final HttpServletResponse response, UserDetail userDetail) + protected abstract void afterSuccessUserCheck(final HttpServletRequest request, final HttpServletResponse response, UserDetail user) throws IOException; - protected void checkValidUser(AuthenticationToken authToken){ + protected void checkValidAuth(AuthenticationToken authToken){ UserDetail loginMember = getUserDetail(authToken); if (loginMember == null) { diff --git a/src/main/java/nextstep/auth/secured/SecuredAnnotationChecker.java b/src/main/java/nextstep/auth/secured/SecuredAnnotationChecker.java index 755955a04..fe191f042 100644 --- a/src/main/java/nextstep/auth/secured/SecuredAnnotationChecker.java +++ b/src/main/java/nextstep/auth/secured/SecuredAnnotationChecker.java @@ -2,8 +2,8 @@ import nextstep.auth.context.Authentication; import nextstep.auth.context.SecurityContextHolder; -import nextstep.common.exception.CustomException; -import nextstep.common.exception.code.CommonCode; +import nextstep.common.exception.AuthException; +import nextstep.common.exception.code.AuthCode; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @@ -27,11 +27,11 @@ public void checkAuthorities(JoinPoint joinPoint) { List values = Arrays.stream(secured.value()).map(Enum::name).collect(Collectors.toList()); Authentication authentication = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .orElseThrow(() -> new CustomException(CommonCode.AUTH_INVALID)); + .orElseThrow(() -> new AuthException(AuthCode.AUTH_INVALID)); authentication.getAuthorities().stream() .filter(values::contains) .findFirst() - .orElseThrow(() -> new CustomException(CommonCode.AUTH_INVALID)); + .orElseThrow(() -> new AuthException(AuthCode.AUTH_INVALID)); } } diff --git a/src/main/java/nextstep/auth/token/TokenAuthInterceptor.java b/src/main/java/nextstep/auth/token/TokenAuthInterceptor.java index 480a2e499..5b568a864 100644 --- a/src/main/java/nextstep/auth/token/TokenAuthInterceptor.java +++ b/src/main/java/nextstep/auth/token/TokenAuthInterceptor.java @@ -2,8 +2,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import nextstep.auth.authentication.AuthenticationToken; -import nextstep.auth.user.UserDetail; +import nextstep.auth.user.User; import nextstep.auth.interceptor.AuthNotChainInterceptor; +import nextstep.auth.user.UserDetail; import nextstep.auth.user.UserDetailService; import org.springframework.http.MediaType; @@ -30,8 +31,8 @@ protected AuthenticationToken createAuthToken(final HttpServletRequest request) @Override protected void afterSuccessUserCheck(final HttpServletRequest request, final HttpServletResponse response, - final UserDetail userDetail) throws IOException { - String token = jwtTokenProvider.createToken(userDetail.getEmail(), userDetail.getAuthorities()); + final UserDetail user) throws IOException { + String token = jwtTokenProvider.createToken(user.getEmail(), user.getAuthorities()); TokenResponse tokenResponse = new TokenResponse(token); String responseToClient = new ObjectMapper().writeValueAsString(tokenResponse); diff --git a/src/main/java/nextstep/auth/user/User.java b/src/main/java/nextstep/auth/user/User.java new file mode 100644 index 000000000..0f39cb09a --- /dev/null +++ b/src/main/java/nextstep/auth/user/User.java @@ -0,0 +1,34 @@ +package nextstep.auth.user; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class User implements UserDetail{ + private String email; + private String password; + private List authorities; + + public static User of(String email, List authorities) { + return new User(email, null, authorities); + } + + public static User of(String email, String password, List authorities) { + return new User(email, password, authorities); + } + + public static UserDetail guest() { + return new User(); + } + + @Override + public boolean checkPassword(final String password) { + return this.password.equals(password); + } +} diff --git a/src/main/java/nextstep/auth/user/UserDetail.java b/src/main/java/nextstep/auth/user/UserDetail.java index 410ac0753..5459ef036 100644 --- a/src/main/java/nextstep/auth/user/UserDetail.java +++ b/src/main/java/nextstep/auth/user/UserDetail.java @@ -3,11 +3,11 @@ import java.util.List; public interface UserDetail { + boolean checkPassword(final String password); + String getEmail(); String getPassword(); List getAuthorities(); - - boolean checkPassword(String password); } diff --git a/src/main/java/nextstep/common/CommonResponse.java b/src/main/java/nextstep/common/CommonResponse.java index 4c0131bdb..2349fbbeb 100644 --- a/src/main/java/nextstep/common/CommonResponse.java +++ b/src/main/java/nextstep/common/CommonResponse.java @@ -13,7 +13,7 @@ public class CommonResponse { private int code; private String message; - T data; + private T data; public CommonResponse(ResponseCode responseCode) { this.code = responseCode.getCode(); diff --git a/src/main/java/nextstep/common/exception/AuthException.java b/src/main/java/nextstep/common/exception/AuthException.java new file mode 100644 index 000000000..d176cf8a9 --- /dev/null +++ b/src/main/java/nextstep/common/exception/AuthException.java @@ -0,0 +1,9 @@ +package nextstep.common.exception; + +import nextstep.common.exception.code.AuthCode; + +public class AuthException extends CustomException{ + public AuthException(final AuthCode authCode) { + super(authCode); + } +} diff --git a/src/main/java/nextstep/common/exception/GlobalExceptionHandler.java b/src/main/java/nextstep/common/exception/GlobalExceptionHandler.java index 1cfb56cf7..5e0e70545 100644 --- a/src/main/java/nextstep/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/nextstep/common/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import nextstep.common.CommonResponse; +import nextstep.common.exception.code.AuthCode; import nextstep.common.exception.code.CommonCode; import nextstep.common.exception.code.ResponseCode; import org.springframework.dao.DataIntegrityViolationException; @@ -28,6 +29,16 @@ public ResponseEntity handleCustomException(CustomException ex) { return ResponseEntity.status(HttpStatus.OK).body(response); } + /** + * 인증 exception 처리 + */ + @ExceptionHandler(value = {AuthException.class}) + public ResponseEntity handleAuthException(AuthException ex) { + ResponseCode responseCode = ex.getResponseCode(); + CommonResponse response = new CommonResponse<>(responseCode); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } + /** * 파라미터 유효성관련 exception 처리 */ diff --git a/src/main/java/nextstep/common/exception/code/AuthCode.java b/src/main/java/nextstep/common/exception/code/AuthCode.java new file mode 100644 index 000000000..b7cb80db1 --- /dev/null +++ b/src/main/java/nextstep/common/exception/code/AuthCode.java @@ -0,0 +1,17 @@ +package nextstep.common.exception.code; + +import lombok.Getter; + +@Getter +public enum AuthCode implements ResponseCode { + AUTH_INVALID(1002, "올바르지 않는 사용자입니다."); + + private final int code; + + private final String message; + + AuthCode(int code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/nextstep/common/exception/code/CommonCode.java b/src/main/java/nextstep/common/exception/code/CommonCode.java index c296fa80a..3b0a1458d 100644 --- a/src/main/java/nextstep/common/exception/code/CommonCode.java +++ b/src/main/java/nextstep/common/exception/code/CommonCode.java @@ -5,8 +5,7 @@ @Getter public enum CommonCode implements ResponseCode { ETC(1000, "알 수 없는 오류입니다."), - PARAM_INVALID(1001, "올바르지 않은 파라미터입니다."), - AUTH_INVALID(1002, "올바르지 않는 사용자입니다."); + PARAM_INVALID(1001, "올바르지 않은 파라미터입니다."); private final int code; diff --git a/src/main/java/nextstep/member/application/LoginMemberService.java b/src/main/java/nextstep/member/application/LoginMemberService.java index 90442b38a..515d09ec4 100644 --- a/src/main/java/nextstep/member/application/LoginMemberService.java +++ b/src/main/java/nextstep/member/application/LoginMemberService.java @@ -1,7 +1,8 @@ package nextstep.member.application; +import nextstep.auth.user.User; +import nextstep.auth.user.UserDetail; import nextstep.auth.user.UserDetailService; -import nextstep.member.domain.LoginMember; import nextstep.member.domain.Member; import nextstep.member.domain.MemberRepository; import org.springframework.stereotype.Service; @@ -14,8 +15,8 @@ public LoginMemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } - public LoginMember loadUserByUsername(String email) { + public UserDetail loadUserByUsername(String email) { Member member = memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); - return LoginMember.of(member); + return User.of(member.getEmail(), member.getPassword(), member.getRoles()); } } diff --git a/src/main/java/nextstep/member/domain/LoginMember.java b/src/main/java/nextstep/member/domain/LoginMember.java deleted file mode 100644 index b847fe96d..000000000 --- a/src/main/java/nextstep/member/domain/LoginMember.java +++ /dev/null @@ -1,35 +0,0 @@ -package nextstep.member.domain; - - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import nextstep.auth.user.UserDetail; - -import java.util.List; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@Getter -public class LoginMember implements UserDetail { - private String email; - private String password; - private List authorities; - - public static LoginMember of(Member member) { - return new LoginMember(member.getEmail(), member.getPassword(), member.getRoles()); - } - - public static LoginMember of(String email, List authorities) { - return new LoginMember(email, null, authorities); - } - - public static LoginMember guest() { - return new LoginMember(); - } - - public boolean checkPassword(String password) { - return this.password.equals(password); - } -} diff --git a/src/main/java/nextstep/member/ui/MemberController.java b/src/main/java/nextstep/member/ui/MemberController.java index 742acdb17..ac61d6ace 100644 --- a/src/main/java/nextstep/member/ui/MemberController.java +++ b/src/main/java/nextstep/member/ui/MemberController.java @@ -1,10 +1,10 @@ package nextstep.member.ui; import nextstep.auth.authorization.AuthenticationPrincipal; +import nextstep.auth.user.User; import nextstep.member.application.MemberService; import nextstep.member.application.dto.MemberRequest; import nextstep.member.application.dto.MemberResponse; -import nextstep.member.domain.LoginMember; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -43,19 +43,19 @@ public ResponseEntity deleteMember(@PathVariable Long id) { } @GetMapping("/members/me") - public ResponseEntity findMemberOfMine(@AuthenticationPrincipal LoginMember loginMember) { + public ResponseEntity findMemberOfMine(@AuthenticationPrincipal User loginMember) { MemberResponse member = memberService.findMember(loginMember.getEmail()); return ResponseEntity.ok().body(member); } @PutMapping("/members/me") - public ResponseEntity updateMemberOfMine(@AuthenticationPrincipal LoginMember loginMember, @RequestBody MemberRequest param) { + public ResponseEntity updateMemberOfMine(@AuthenticationPrincipal User loginMember, @RequestBody MemberRequest param) { memberService.updateMember(loginMember.getEmail(), param); return ResponseEntity.ok().build(); } @DeleteMapping("/members/me") - public ResponseEntity deleteMemberOfMine(@AuthenticationPrincipal LoginMember loginMember) { + public ResponseEntity deleteMemberOfMine(@AuthenticationPrincipal User loginMember) { memberService.deleteMember(loginMember.getEmail()); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/nextstep/subway/applicaion/FavoritesService.java b/src/main/java/nextstep/subway/applicaion/FavoritesService.java new file mode 100644 index 000000000..8fed8ab4c --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/FavoritesService.java @@ -0,0 +1,44 @@ +package nextstep.subway.applicaion; + +import lombok.RequiredArgsConstructor; +import nextstep.subway.applicaion.dto.FavoriteCreateRequest; +import nextstep.subway.applicaion.dto.FavoriteResponse; +import nextstep.subway.domain.Favorite; +import nextstep.subway.domain.FavoritesRepository; +import nextstep.subway.domain.Station; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Transactional +@Service +public class FavoritesService { + private final FavoritesRepository favoritesRepository; + private final StationService stationService; + + public FavoriteResponse createFavorite(final FavoriteCreateRequest request) { + Station source = stationService.findById(request.getSource()); + Station target = stationService.findById(request.getTarget()); + + Favorite favorite = new Favorite(source, target); + favoritesRepository.save(favorite); + + return new FavoriteResponse(favorite); + } + + @Transactional(readOnly = true) + public List getFavorites() { + List favorites = favoritesRepository.findAll(); + + return favorites.stream() + .map(FavoriteResponse::new) + .collect(Collectors.toList()); + } + + public void deleteFavorite(final Long favoriteId) { + favoritesRepository.deleteById(favoriteId); + } +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/FavoriteCreateRequest.java b/src/main/java/nextstep/subway/applicaion/dto/FavoriteCreateRequest.java new file mode 100644 index 000000000..a4d1e4c9b --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/FavoriteCreateRequest.java @@ -0,0 +1,11 @@ +package nextstep.subway.applicaion.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class FavoriteCreateRequest { + private Long source; + private Long target; +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/FavoriteResponse.java b/src/main/java/nextstep/subway/applicaion/dto/FavoriteResponse.java new file mode 100644 index 000000000..37b7e5147 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/FavoriteResponse.java @@ -0,0 +1,32 @@ +package nextstep.subway.applicaion.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import nextstep.subway.domain.Favorite; +import nextstep.subway.domain.Station; + +@AllArgsConstructor +@Getter +public class FavoriteResponse { + Long id; + StationDTO source; + StationDTO target; + + public FavoriteResponse(Favorite favorite) { + this.id = favorite.getId(); + this.source = new StationDTO(favorite.getSource()); + this.target = new StationDTO(favorite.getTarget()); + } + + @Getter + @AllArgsConstructor + public static class StationDTO { + private Long id; + private String name; + + public StationDTO(Station station){ + this.id = station.getId(); + this.name = station.getName(); + } + } +} diff --git a/src/main/java/nextstep/subway/domain/Favorite.java b/src/main/java/nextstep/subway/domain/Favorite.java new file mode 100644 index 000000000..849320f29 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/Favorite.java @@ -0,0 +1,32 @@ +package nextstep.subway.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class Favorite { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne + @JoinColumn(name = "source_id") + private Station source; + @ManyToOne + @JoinColumn(name = "target_id") + private Station target; + + public Favorite(final Station source, final Station target) { + this.source = source; + this.target = target; + } +} diff --git a/src/main/java/nextstep/subway/domain/FavoritesRepository.java b/src/main/java/nextstep/subway/domain/FavoritesRepository.java new file mode 100644 index 000000000..805774160 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/FavoritesRepository.java @@ -0,0 +1,6 @@ +package nextstep.subway.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FavoritesRepository extends JpaRepository { +} diff --git a/src/main/java/nextstep/subway/domain/Station.java b/src/main/java/nextstep/subway/domain/Station.java index 79e394179..b54745b36 100644 --- a/src/main/java/nextstep/subway/domain/Station.java +++ b/src/main/java/nextstep/subway/domain/Station.java @@ -1,5 +1,7 @@ package nextstep.subway.domain; +import lombok.Builder; + import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -15,6 +17,12 @@ public class Station { public Station() { } + @Builder + public Station(final Long id, final String name) { + this.id = id; + this.name = name; + } + public Station(String name) { this.name = name; } diff --git a/src/main/java/nextstep/subway/ui/FavoritesController.java b/src/main/java/nextstep/subway/ui/FavoritesController.java new file mode 100644 index 000000000..05ad6a7c9 --- /dev/null +++ b/src/main/java/nextstep/subway/ui/FavoritesController.java @@ -0,0 +1,47 @@ +package nextstep.subway.ui; + +import lombok.RequiredArgsConstructor; +import nextstep.auth.secured.Secured; +import nextstep.member.domain.RoleType; +import nextstep.subway.applicaion.FavoritesService; +import nextstep.subway.applicaion.dto.FavoriteCreateRequest; +import nextstep.subway.applicaion.dto.FavoriteResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/favorites") +public class FavoritesController { + private final FavoritesService favoritesService; + + @Secured(RoleType.ROLE_ADMIN) + @PostMapping + public ResponseEntity createFavorite(@RequestBody FavoriteCreateRequest request){ + FavoriteResponse favorite = favoritesService.createFavorite(request); + return ResponseEntity.created(URI.create("/favorites/" + favorite.getId())).build(); + } + + @Secured(RoleType.ROLE_ADMIN) + @GetMapping + public ResponseEntity> getFavorites(){ + List response = favoritesService.getFavorites(); + return ResponseEntity.ok(response); + } + + @Secured(RoleType.ROLE_ADMIN) + @DeleteMapping("/{favoriteId}") + public ResponseEntity deleteFavorite(@PathVariable Long favoriteId){ + favoritesService.deleteFavorite(favoriteId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/FavoritesAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/FavoritesAcceptanceTest.java new file mode 100644 index 000000000..6d9f0f26b --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/FavoritesAcceptanceTest.java @@ -0,0 +1,176 @@ +package nextstep.subway.acceptance; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.support.AcceptanceTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static nextstep.subway.acceptance.support.FavoritesSteps.미로그인_즐겨찾기_생성_요청; +import static nextstep.subway.acceptance.support.FavoritesSteps.즐겨찾기_삭제_요청; +import static nextstep.subway.acceptance.support.FavoritesSteps.즐겨찾기_생성_요청; +import static nextstep.subway.acceptance.support.FavoritesSteps.즐겨찾기_조회_요청; +import static nextstep.subway.acceptance.support.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("즐겨찾기 기능") +public class FavoritesAcceptanceTest extends AcceptanceTest { + + private Long 강남역; + private Long 양재역; + + /** + * Given 지하철역을 생성하고 + */ + @BeforeEach + public void setUp() { + super.setUp(); + + 강남역 = 지하철역_생성_요청(관리자, "강남역").jsonPath().getLong("id"); + 양재역 = 지하철역_생성_요청(관리자, "양재역").jsonPath().getLong("id"); + } + + @DisplayName("즐겨찾기 생성") + @Nested + class CreateFavorite{ + /** + * When 서로 다른 두개의 역에 대해서 즐겨찾기를 등록하면 + * Then 즐겨찾기가 등록된다. + */ + @Test + void success(){ + // when + ExtractableResponse response = 즐겨찾기_생성_요청(관리자, 강남역, 양재역); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + /** + * When 유효하지 않은 사용자가 즐겨찾기를 등록하면 + * Then 등록에 실패한다. + */ + @Test + void unauthorized(){ + // when + final String notAdmin = "9999"; + ExtractableResponse response = 즐겨찾기_생성_요청(notAdmin, 강남역, 양재역); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(response.jsonPath().getLong("code")).isEqualTo(1002); + } + + /** + * When 로그인 하지 않은 사용자가 즐겨찾기 등록하면 + * Then 등록에 실패한다. + */ + @Test + void unauthorized_notlogin(){ + // when + ExtractableResponse response = 미로그인_즐겨찾기_생성_요청(강남역, 양재역); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(response.jsonPath().getLong("code")).isEqualTo(1002); + } + + /** + * When 일반 회원이 즐겨찾기를 등록하면 + * Then 등록에 실패한다. + */ + @Test + void unauthorized_when_member(){ + // when + ExtractableResponse response = 즐겨찾기_생성_요청(일반회원, 강남역, 양재역); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(response.jsonPath().getLong("code")).isEqualTo(1002); + } + } + + @DisplayName("즐겨찾기 조회") + @Nested + class GetFavorite{ + /** + * Given 서로 다른 두개의 역에 대해서 즐겨찾기를 등록한다. + * When 즐겨 찾기를 조회하면 + * Then 즐겨찾기 정보를 얻을 수 있다. + */ + @Test + void success(){ + // given + 즐겨찾기_생성_요청(관리자, 강남역, 양재역); + + // when + ExtractableResponse response = 즐겨찾기_조회_요청(관리자); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getLong("[0].id")).isNotNull(); + assertThat(response.jsonPath().getString("[0].source.name")).contains("강남역"); + assertThat(response.jsonPath().getString("[0].target.name")).contains("양재역"); + } + + /** + * When 유효하지 않은 사용자가 즐겨찾기를 조회하면 + * Then 조회에 실패한다. + */ + @Test + void unauthorized(){ + // when + final String notAdmin = "9999"; + ExtractableResponse response = 즐겨찾기_조회_요청(notAdmin); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(response.jsonPath().getLong("code")).isEqualTo(1002); + } + } + + @DisplayName("즐겨찾기 삭제") + @Nested + class DeleteFavorite{ + /** + * Given 서로 다른 두개의 역에 대해서 즐겨찾기를 등록한다. + * When 즐겨찾기를 삭제하면 + * Then 즐겨찾기 삭제가 된다. + */ + @Test + void success(){ + // given + 즐겨찾기_생성_요청(관리자, 강남역, 양재역); + Long 등록된_즐겨찾기 = 즐겨찾기_조회_요청(관리자).jsonPath().getLong("[0].id"); + + // when + ExtractableResponse response = 즐겨찾기_삭제_요청(관리자, 등록된_즐겨찾기); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + /** + * Given 서로 다른 두개의 역에 대해서 즐겨찾기를 등록한다. + * When 유효하지 않은 사용자가 즐겨찾기를 조회하면 + * Then 조회에 실패한다. + */ + @Test + void unauthorized(){ + // given + 즐겨찾기_생성_요청(관리자, 강남역, 양재역); + Long 등록된_즐겨찾기 = 즐겨찾기_조회_요청(관리자).jsonPath().getLong("[0].id"); + + // when + final String 유효하지않은_사용자 = "9999"; + ExtractableResponse response = 즐겨찾기_삭제_요청(유효하지않은_사용자, 등록된_즐겨찾기); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(response.jsonPath().getLong("code")).isEqualTo(1002); + } + } +} diff --git a/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java index 0cf5258eb..214454a5c 100644 --- a/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java +++ b/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java @@ -49,7 +49,7 @@ void throwExceptionWhenNotAdmin() { ExtractableResponse response = 지하철_노선_생성_요청(notAdmin, "2호선", "green"); // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); assertThat(response.jsonPath().getLong("code")).isEqualTo(1002); } } diff --git a/src/test/java/nextstep/subway/acceptance/support/AcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/support/AcceptanceTest.java index e4e6f1be5..5bcd69139 100644 --- a/src/test/java/nextstep/subway/acceptance/support/AcceptanceTest.java +++ b/src/test/java/nextstep/subway/acceptance/support/AcceptanceTest.java @@ -14,8 +14,11 @@ @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class AcceptanceTest { - private static final String EMAIL = "admin@email.com"; - private static final String PASSWORD = "password"; + private static final String ADMIN_EMAIL = "admin@email.com"; + private static final String ADMIN_PASSWORD = "password"; + + private static final String MEMBER_EMAIL = "member@email.com"; + private static final String MEMBER_PASSWORD = "password"; @LocalServerPort int port; @@ -27,6 +30,7 @@ public class AcceptanceTest { private DataLoader dataLoader; protected String 관리자; + protected String 일반회원; @BeforeEach public void setUp() { @@ -34,6 +38,7 @@ public void setUp() { databaseCleanup.execute(); dataLoader.loadData(); - 관리자 = 로그인_되어_있음(EMAIL, PASSWORD); + 관리자 = 로그인_되어_있음(ADMIN_EMAIL, ADMIN_PASSWORD); + 일반회원 = 로그인_되어_있음(MEMBER_EMAIL, MEMBER_PASSWORD); } } diff --git a/src/test/java/nextstep/subway/acceptance/support/AcceptanceTestSteps.java b/src/test/java/nextstep/subway/acceptance/support/AcceptanceTestSteps.java index f6a483981..c30956dea 100644 --- a/src/test/java/nextstep/subway/acceptance/support/AcceptanceTestSteps.java +++ b/src/test/java/nextstep/subway/acceptance/support/AcceptanceTestSteps.java @@ -4,12 +4,12 @@ import io.restassured.specification.RequestSpecification; public class AcceptanceTestSteps { - static RequestSpecification given() { + public static RequestSpecification given() { return RestAssured .given().log().all(); } - static RequestSpecification given(String token) { + public static RequestSpecification given(String token) { return RestAssured .given().log().all() .auth().oauth2(token); diff --git a/src/test/java/nextstep/subway/acceptance/support/FavoritesSteps.java b/src/test/java/nextstep/subway/acceptance/support/FavoritesSteps.java new file mode 100644 index 000000000..0e1596a39 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/support/FavoritesSteps.java @@ -0,0 +1,47 @@ +package nextstep.subway.acceptance.support; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.support.AcceptanceTestSteps.given; + +public class FavoritesSteps { + public static ExtractableResponse 즐겨찾기_생성_요청(String token, Long sourceId, Long targetId) { + Map params = new HashMap<>(); + params.put("source", sourceId); + params.put("target", targetId); + return given(token) + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/favorites") + .then().log().all().extract(); + } + + public static ExtractableResponse 미로그인_즐겨찾기_생성_요청(Long sourceId, Long targetId) { + Map params = new HashMap<>(); + params.put("source", sourceId); + params.put("target", targetId); + return given() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/favorites") + .then().log().all().extract(); + } + + public static ExtractableResponse 즐겨찾기_조회_요청(String token) { + return given(token) + .when().get("/favorites") + .then().log().all().extract(); + } + + public static ExtractableResponse 즐겨찾기_삭제_요청(String token, Long favoritesId) { + return given(token) + .delete("/favorites/{favoritesId}", favoritesId) + .then().log().all() + .extract(); + } +} diff --git a/src/test/java/nextstep/subway/unit/FavoritesServiceTest.java b/src/test/java/nextstep/subway/unit/FavoritesServiceTest.java new file mode 100644 index 000000000..38510c5a9 --- /dev/null +++ b/src/test/java/nextstep/subway/unit/FavoritesServiceTest.java @@ -0,0 +1,56 @@ +package nextstep.subway.unit; + +import nextstep.subway.applicaion.FavoritesService; +import nextstep.subway.applicaion.StationService; +import nextstep.subway.applicaion.dto.FavoriteCreateRequest; +import nextstep.subway.applicaion.dto.FavoriteResponse; +import nextstep.subway.domain.Favorite; +import nextstep.subway.domain.FavoritesRepository; +import nextstep.subway.domain.Station; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static nextstep.subway.unit.support.StationMockFactory.station; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + + +@ExtendWith(MockitoExtension.class) +class FavoritesServiceTest { + @Mock + private StationService stationService; + @Mock + private FavoritesRepository favoritesRepository; + @InjectMocks + private FavoritesService favoritesService; + + @Test + void createFavorite() { + // given + Station source = station(1L); + Station target = station(2L); + FavoriteCreateRequest request = new FavoriteCreateRequest(source.getId(), target.getId()); + given(stationService.findById(source.getId())).willReturn(source); + given(stationService.findById(target.getId())).willReturn(target); + + // when + FavoriteResponse response = favoritesService.createFavorite(request); + + // then + assertAll( + () -> assertThat(response.getSource().getId()).isEqualTo(source.getId()), + () -> assertThat(response.getSource().getName()).isEqualTo(source.getName()), + () -> assertThat(response.getTarget().getId()).isEqualTo(target.getId()), + () -> assertThat(response.getTarget().getName()).isEqualTo(target.getName()) + ); + verify(favoritesRepository).save(any(Favorite.class)); + } + + +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/unit/support/StationMockFactory.java b/src/test/java/nextstep/subway/unit/support/StationMockFactory.java new file mode 100644 index 000000000..a06043c8c --- /dev/null +++ b/src/test/java/nextstep/subway/unit/support/StationMockFactory.java @@ -0,0 +1,11 @@ +package nextstep.subway.unit.support; + +import nextstep.subway.domain.Station; + +public class StationMockFactory { + public static Station station(long id) { + return Station.builder() + .id(id) + .name("station1").build(); + } +}