diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 6519fda8c..285c6c3f7 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,4 +11,4 @@ === 경로 조회 -operation::path[snippets='http-request,http-response'] \ No newline at end of file +operation::path[snippets='http-request,http-response,request-parameters,response-body,response-fields'] \ No newline at end of file diff --git a/src/main/java/nextstep/auth/AuthConfig.java b/src/main/java/nextstep/auth/AuthConfig.java new file mode 100644 index 000000000..f740cd2de --- /dev/null +++ b/src/main/java/nextstep/auth/AuthConfig.java @@ -0,0 +1,22 @@ +package nextstep.auth; + +import nextstep.auth.principal.AuthenticationPrincipalArgumentResolver; +import nextstep.auth.token.JwtTokenProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class AuthConfig implements WebMvcConfigurer { + private JwtTokenProvider jwtTokenProvider; + + public AuthConfig(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new AuthenticationPrincipalArgumentResolver(jwtTokenProvider)); + } +} diff --git a/src/main/java/nextstep/auth/AuthenticationException.java b/src/main/java/nextstep/auth/AuthenticationException.java new file mode 100644 index 000000000..2865ceca3 --- /dev/null +++ b/src/main/java/nextstep/auth/AuthenticationException.java @@ -0,0 +1,8 @@ +package nextstep.auth; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class AuthenticationException extends RuntimeException { +} diff --git a/src/main/java/nextstep/auth/principal/AuthenticationPrincipal.java b/src/main/java/nextstep/auth/principal/AuthenticationPrincipal.java new file mode 100644 index 000000000..7f595cd19 --- /dev/null +++ b/src/main/java/nextstep/auth/principal/AuthenticationPrincipal.java @@ -0,0 +1,11 @@ +package nextstep.auth.principal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthenticationPrincipal { +} diff --git a/src/main/java/nextstep/auth/principal/AuthenticationPrincipalArgumentResolver.java b/src/main/java/nextstep/auth/principal/AuthenticationPrincipalArgumentResolver.java new file mode 100644 index 000000000..0cdb02744 --- /dev/null +++ b/src/main/java/nextstep/auth/principal/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,36 @@ +package nextstep.auth.principal; + +import nextstep.auth.AuthenticationException; +import nextstep.auth.token.JwtTokenProvider; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private JwtTokenProvider jwtTokenProvider; + + public AuthenticationPrincipalArgumentResolver(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthenticationPrincipal.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String authorization = webRequest.getHeader("Authorization"); + if (!"bearer".equalsIgnoreCase(authorization.split(" ")[0])) { + throw new AuthenticationException(); + } + String token = authorization.split(" ")[1]; + + String username = jwtTokenProvider.getPrincipal(token); + String role = jwtTokenProvider.getRoles(token); + + return new UserPrincipal(username, role); + } +} diff --git a/src/main/java/nextstep/auth/principal/UserPrincipal.java b/src/main/java/nextstep/auth/principal/UserPrincipal.java new file mode 100644 index 000000000..b7f62dec5 --- /dev/null +++ b/src/main/java/nextstep/auth/principal/UserPrincipal.java @@ -0,0 +1,19 @@ +package nextstep.auth.principal; + +public class UserPrincipal { + private String username; + private String role; + + public UserPrincipal(String username, String role) { + this.username = username; + this.role = role; + } + + public String getUsername() { + return username; + } + + public String getRole() { + return role; + } +} diff --git a/src/main/java/nextstep/auth/token/JwtTokenProvider.java b/src/main/java/nextstep/auth/token/JwtTokenProvider.java new file mode 100644 index 000000000..50a518674 --- /dev/null +++ b/src/main/java/nextstep/auth/token/JwtTokenProvider.java @@ -0,0 +1,48 @@ +package nextstep.auth.token; + +import io.jsonwebtoken.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtTokenProvider { + @Value("${security.jwt.token.secret-key}") + private String secretKey; + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + public String createToken(String principal, String role) { + Claims claims = Jwts.claims().setSubject(principal); + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .claim("role", role) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + public String getPrincipal(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + public String getRoles(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("role", String.class); + } + + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + + return !claims.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} + diff --git a/src/main/java/nextstep/auth/token/TokenController.java b/src/main/java/nextstep/auth/token/TokenController.java new file mode 100644 index 000000000..459410c05 --- /dev/null +++ b/src/main/java/nextstep/auth/token/TokenController.java @@ -0,0 +1,30 @@ +package nextstep.auth.token; + +import nextstep.auth.token.oauth2.github.GithubTokenRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TokenController { + private TokenService tokenService; + + public TokenController(TokenService tokenService) { + this.tokenService = tokenService; + } + + @PostMapping("/login/token") + public ResponseEntity createToken(@RequestBody TokenRequest request) { + TokenResponse response = tokenService.createToken(request.getEmail(), request.getPassword()); + + return ResponseEntity.ok(response); + } + + @PostMapping("/login/github") + public ResponseEntity createTokenByGithub(@RequestBody GithubTokenRequest request) { + TokenResponse response = tokenService.createTokenFromGithub(request.getCode()); + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/nextstep/auth/token/TokenRequest.java b/src/main/java/nextstep/auth/token/TokenRequest.java new file mode 100644 index 000000000..e27687e41 --- /dev/null +++ b/src/main/java/nextstep/auth/token/TokenRequest.java @@ -0,0 +1,22 @@ +package nextstep.auth.token; + +public class TokenRequest { + private String email; + private String password; + + public TokenRequest() { + } + + public TokenRequest(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/nextstep/auth/token/TokenResponse.java b/src/main/java/nextstep/auth/token/TokenResponse.java new file mode 100644 index 000000000..69a72a148 --- /dev/null +++ b/src/main/java/nextstep/auth/token/TokenResponse.java @@ -0,0 +1,16 @@ +package nextstep.auth.token; + +public class TokenResponse { + private String accessToken; + + public TokenResponse() { + } + + public TokenResponse(String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } +} diff --git a/src/main/java/nextstep/auth/token/TokenService.java b/src/main/java/nextstep/auth/token/TokenService.java new file mode 100644 index 000000000..fe6dc7b56 --- /dev/null +++ b/src/main/java/nextstep/auth/token/TokenService.java @@ -0,0 +1,52 @@ +package nextstep.auth.token; + +import nextstep.auth.AuthenticationException; +import nextstep.auth.token.oauth2.OAuth2User; +import nextstep.auth.token.oauth2.OAuth2UserService; +import nextstep.auth.token.oauth2.github.GithubClient; +import nextstep.auth.token.oauth2.github.GithubProfileResponse; +import nextstep.auth.userdetails.UserDetails; +import nextstep.auth.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +@Service +public class TokenService { + private UserDetailsService userDetailsService; + private OAuth2UserService oAuth2UserService; + private JwtTokenProvider jwtTokenProvider; + private GithubClient githubClient; + + public TokenService( + UserDetailsService userDetailsService, + OAuth2UserService oAuth2UserService, + JwtTokenProvider jwtTokenProvider, + GithubClient githubClient + ) { + this.userDetailsService = userDetailsService; + this.oAuth2UserService = oAuth2UserService; + this.jwtTokenProvider = jwtTokenProvider; + this.githubClient = githubClient; + } + + public TokenResponse createToken(String email, String password) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + if (!userDetails.getPassword().equals(password)) { + throw new AuthenticationException(); + } + + String token = jwtTokenProvider.createToken(userDetails.getUsername(), userDetails.getRole()); + + return new TokenResponse(token); + } + + public TokenResponse createTokenFromGithub(String code) { + String accessTokenFromGithub = githubClient.getAccessTokenFromGithub(code); + GithubProfileResponse githubProfile = githubClient.getGithubProfileFromGithub(accessTokenFromGithub); + + OAuth2User oAuth2User = oAuth2UserService.loadUser(githubProfile); + + String token = jwtTokenProvider.createToken(oAuth2User.getUsername(), oAuth2User.getRole()); + + return new TokenResponse(token); + } +} diff --git a/src/main/java/nextstep/auth/token/oauth2/OAuth2User.java b/src/main/java/nextstep/auth/token/oauth2/OAuth2User.java new file mode 100644 index 000000000..a61c547f2 --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/OAuth2User.java @@ -0,0 +1,7 @@ +package nextstep.auth.token.oauth2; + +public interface OAuth2User { + String getUsername(); + + String getRole(); +} diff --git a/src/main/java/nextstep/auth/token/oauth2/OAuth2UserRequest.java b/src/main/java/nextstep/auth/token/oauth2/OAuth2UserRequest.java new file mode 100644 index 000000000..4a9e37faa --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/OAuth2UserRequest.java @@ -0,0 +1,7 @@ +package nextstep.auth.token.oauth2; + +public interface OAuth2UserRequest { + String getUsername(); + + Integer getAge(); +} diff --git a/src/main/java/nextstep/auth/token/oauth2/OAuth2UserService.java b/src/main/java/nextstep/auth/token/oauth2/OAuth2UserService.java new file mode 100644 index 000000000..586555ebf --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/OAuth2UserService.java @@ -0,0 +1,5 @@ +package nextstep.auth.token.oauth2; + +public interface OAuth2UserService { + OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest); +} diff --git a/src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenRequest.java b/src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenRequest.java new file mode 100644 index 000000000..5df9965d8 --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenRequest.java @@ -0,0 +1,29 @@ +package nextstep.auth.token.oauth2.github; + +public class GithubAccessTokenRequest { + + private String code; + private String client_id; + private String client_secret; + + public GithubAccessTokenRequest(String code, String client_id, String client_secret) { + this.code = code; + this.client_id = client_id; + this.client_secret = client_secret; + } + + public GithubAccessTokenRequest() { + } + + public String getCode() { + return code; + } + + public String getClient_id() { + return client_id; + } + + public String getClient_secret() { + return client_secret; + } +} diff --git a/src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenResponse.java b/src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenResponse.java new file mode 100644 index 000000000..6397d2c7d --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenResponse.java @@ -0,0 +1,43 @@ +package nextstep.auth.token.oauth2.github; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GithubAccessTokenResponse { + + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("token_type") + private String tokenType; + private String scope; + private String bearer; + + public GithubAccessTokenResponse() { + + } + + public GithubAccessTokenResponse(String accessToken, + String tokenType, + String scope, + String bearer) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.scope = scope; + this.bearer = bearer; + } + + public String getAccessToken() { + return accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public String getScope() { + return scope; + } + + public String getBearer() { + return bearer; + } +} diff --git a/src/main/java/nextstep/auth/token/oauth2/github/GithubClient.java b/src/main/java/nextstep/auth/token/oauth2/github/GithubClient.java new file mode 100644 index 000000000..ae41ec48b --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/github/GithubClient.java @@ -0,0 +1,64 @@ +package nextstep.auth.token.oauth2.github; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +@Component +public class GithubClient { + + @Value("${github.client.id}") + private String clientId; + @Value("${github.client.secret}") + private String clientSecret; + @Value("${github.url.access-token}") + private String tokenUrl; + @Value("${github.url.profile}") + private String profileUrl; + + public String getAccessTokenFromGithub(String code) { + GithubAccessTokenRequest githubAccessTokenRequest = new GithubAccessTokenRequest( + code, + clientId, + clientSecret + ); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); + + HttpEntity> httpEntity = new HttpEntity( + githubAccessTokenRequest, headers); + RestTemplate restTemplate = new RestTemplate(); + + String accessToken = restTemplate + .exchange(tokenUrl, HttpMethod.POST, httpEntity, GithubAccessTokenResponse.class) + .getBody() + .getAccessToken(); + if (accessToken == null) { + throw new RuntimeException(); + } + return accessToken; + } + + public GithubProfileResponse getGithubProfileFromGithub(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "token " + accessToken); + + HttpEntity httpEntity = new HttpEntity<>(headers); + RestTemplate restTemplate = new RestTemplate(); + + try { + return restTemplate + .exchange(profileUrl, HttpMethod.GET, httpEntity, GithubProfileResponse.class) + .getBody(); + } catch (HttpClientErrorException e) { + throw new RuntimeException(); + } + } +} diff --git a/src/main/java/nextstep/auth/token/oauth2/github/GithubProfileResponse.java b/src/main/java/nextstep/auth/token/oauth2/github/GithubProfileResponse.java new file mode 100644 index 000000000..cd802526e --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/github/GithubProfileResponse.java @@ -0,0 +1,31 @@ +package nextstep.auth.token.oauth2.github; + +import nextstep.auth.token.oauth2.OAuth2UserRequest; + +public class GithubProfileResponse implements OAuth2UserRequest { + + private String email; + private Integer age; + + public GithubProfileResponse() { + } + + public GithubProfileResponse(String email, Integer age) { + this.email = email; + this.age = age; + } + + public String getEmail() { + return email; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public Integer getAge() { + return age; + } +} diff --git a/src/main/java/nextstep/auth/token/oauth2/github/GithubTokenRequest.java b/src/main/java/nextstep/auth/token/oauth2/github/GithubTokenRequest.java new file mode 100644 index 000000000..a4acdc74b --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/github/GithubTokenRequest.java @@ -0,0 +1,16 @@ +package nextstep.auth.token.oauth2.github; + +public class GithubTokenRequest { + private String code; + + public GithubTokenRequest() { + } + + public GithubTokenRequest(String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/src/main/java/nextstep/auth/userdetails/UserDetails.java b/src/main/java/nextstep/auth/userdetails/UserDetails.java new file mode 100644 index 000000000..d3de46901 --- /dev/null +++ b/src/main/java/nextstep/auth/userdetails/UserDetails.java @@ -0,0 +1,9 @@ +package nextstep.auth.userdetails; + +public interface UserDetails { + String getUsername(); + + String getPassword(); + + String getRole(); +} diff --git a/src/main/java/nextstep/auth/userdetails/UserDetailsService.java b/src/main/java/nextstep/auth/userdetails/UserDetailsService.java new file mode 100644 index 000000000..0157c0e7f --- /dev/null +++ b/src/main/java/nextstep/auth/userdetails/UserDetailsService.java @@ -0,0 +1,5 @@ +package nextstep.auth.userdetails; + +public interface UserDetailsService { + UserDetails loadUserByUsername(String username); +} diff --git a/src/main/java/nextstep/favorite/application/FavoriteService.java b/src/main/java/nextstep/favorite/application/FavoriteService.java new file mode 100644 index 000000000..f1919cf81 --- /dev/null +++ b/src/main/java/nextstep/favorite/application/FavoriteService.java @@ -0,0 +1,79 @@ +package nextstep.favorite.application; + +import nextstep.favorite.application.dto.FavoriteRequest; +import nextstep.favorite.application.dto.FavoriteResponse; +import nextstep.favorite.domain.Favorite; +import nextstep.favorite.domain.FavoriteRepository; +import nextstep.member.application.MemberService; +import nextstep.member.application.dto.MemberResponse; +import nextstep.subway.applicaion.PathService; +import nextstep.subway.applicaion.StationService; +import nextstep.subway.applicaion.dto.StationResponse; +import nextstep.subway.domain.Station; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FavoriteService { + private FavoriteRepository favoriteRepository; + private MemberService memberService; + private StationService stationService; + private PathService pathService; + + public FavoriteService(FavoriteRepository favoriteRepository, MemberService memberService, StationService stationService, PathService pathService) { + this.favoriteRepository = favoriteRepository; + this.memberService = memberService; + this.stationService = stationService; + this.pathService = pathService; + } + + public void createFavorite(String email, FavoriteRequest request) { + pathService.findPath(request.getSource(), request.getTarget()); + MemberResponse member = memberService.findMemberByEmail(email); + Favorite favorite = new Favorite(member.getId(), request.getSource(), request.getTarget()); + favoriteRepository.save(favorite); + } + + public List findFavorites(String email) { + MemberResponse member = memberService.findMemberByEmail(email); + List favorites = favoriteRepository.findByMemberId(member.getId()); + Map stations = extractStations(favorites); + + return favorites.stream() + .map(it -> FavoriteResponse.of( + it, + StationResponse.of(stations.get(it.getSourceStationId())), + StationResponse.of(stations.get(it.getTargetStationId())))) + .collect(Collectors.toList()); + } + + public void deleteFavorite(String email, Long id) { + MemberResponse member = memberService.findMemberByEmail(email); + Favorite favorite = favoriteRepository.findById(id).orElseThrow(RuntimeException::new); + if (!favorite.isCreatedBy(member.getId())) { + throw new RuntimeException(); + } + favoriteRepository.deleteById(id); + } + + private Map extractStations(List favorites) { + Set stationIds = extractStationIds(favorites); + return stationService.findAllStationsById(stationIds).stream() + .collect(Collectors.toMap(Station::getId, Function.identity())); + } + + private Set extractStationIds(List favorites) { + Set stationIds = new HashSet<>(); + for (Favorite favorite : favorites) { + stationIds.add(favorite.getSourceStationId()); + stationIds.add(favorite.getTargetStationId()); + } + return stationIds; + } +} diff --git a/src/main/java/nextstep/favorite/application/dto/FavoriteRequest.java b/src/main/java/nextstep/favorite/application/dto/FavoriteRequest.java new file mode 100644 index 000000000..e6be669aa --- /dev/null +++ b/src/main/java/nextstep/favorite/application/dto/FavoriteRequest.java @@ -0,0 +1,22 @@ +package nextstep.favorite.application.dto; + +public class FavoriteRequest { + private Long source; + private Long target; + + public FavoriteRequest() { + } + + public FavoriteRequest(Long source, Long target) { + this.source = source; + this.target = target; + } + + public Long getSource() { + return source; + } + + public Long getTarget() { + return target; + } +} diff --git a/src/main/java/nextstep/favorite/application/dto/FavoriteResponse.java b/src/main/java/nextstep/favorite/application/dto/FavoriteResponse.java new file mode 100644 index 000000000..d90b7e376 --- /dev/null +++ b/src/main/java/nextstep/favorite/application/dto/FavoriteResponse.java @@ -0,0 +1,35 @@ +package nextstep.favorite.application.dto; + +import nextstep.favorite.domain.Favorite; +import nextstep.subway.applicaion.dto.StationResponse; + +public class FavoriteResponse { + private Long id; + private StationResponse source; + private StationResponse target; + + public FavoriteResponse() { + } + + public FavoriteResponse(Long id, StationResponse source, StationResponse target) { + this.id = id; + this.source = source; + this.target = target; + } + + public static FavoriteResponse of(Favorite favorite, StationResponse source, StationResponse target) { + return new FavoriteResponse(favorite.getId(), source, target); + } + + public Long getId() { + return id; + } + + public StationResponse getSource() { + return source; + } + + public StationResponse getTarget() { + return target; + } +} diff --git a/src/main/java/nextstep/favorite/domain/Favorite.java b/src/main/java/nextstep/favorite/domain/Favorite.java new file mode 100644 index 000000000..ee8d58901 --- /dev/null +++ b/src/main/java/nextstep/favorite/domain/Favorite.java @@ -0,0 +1,45 @@ +package nextstep.favorite.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Favorite { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private Long memberId; + private Long sourceStationId; + private Long targetStationId; + + public Favorite() { + } + + public Favorite(Long memberId, Long sourceStationId, Long targetStationId) { + this.memberId = memberId; + this.sourceStationId = sourceStationId; + this.targetStationId = targetStationId; + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public Long getSourceStationId() { + return sourceStationId; + } + + public Long getTargetStationId() { + return targetStationId; + } + + public boolean isCreatedBy(Long memberId) { + return this.memberId == memberId; + } +} diff --git a/src/main/java/nextstep/favorite/domain/FavoriteRepository.java b/src/main/java/nextstep/favorite/domain/FavoriteRepository.java new file mode 100644 index 000000000..71c84b46c --- /dev/null +++ b/src/main/java/nextstep/favorite/domain/FavoriteRepository.java @@ -0,0 +1,9 @@ +package nextstep.favorite.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FavoriteRepository extends JpaRepository { + List findByMemberId(Long memberId); +} diff --git a/src/main/java/nextstep/favorite/ui/FavoriteController.java b/src/main/java/nextstep/favorite/ui/FavoriteController.java new file mode 100644 index 000000000..5ca60ad55 --- /dev/null +++ b/src/main/java/nextstep/favorite/ui/FavoriteController.java @@ -0,0 +1,41 @@ +package nextstep.favorite.ui; + +import nextstep.auth.principal.AuthenticationPrincipal; +import nextstep.auth.principal.UserPrincipal; +import nextstep.favorite.application.FavoriteService; +import nextstep.favorite.application.dto.FavoriteRequest; +import nextstep.favorite.application.dto.FavoriteResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.List; + +@RestController +public class FavoriteController { + private FavoriteService favoriteService; + + public FavoriteController(FavoriteService favoriteService) { + this.favoriteService = favoriteService; + } + + @PostMapping("/favorites") + public ResponseEntity createFavorite(@AuthenticationPrincipal UserPrincipal userPrincipal, @RequestBody FavoriteRequest request) { + favoriteService.createFavorite(userPrincipal.getUsername(), request); + return ResponseEntity + .created(URI.create("/favorites/" + 1L)) + .build(); + } + + @GetMapping("/favorites") + public ResponseEntity> getFavorites(@AuthenticationPrincipal UserPrincipal userPrincipal) { + List favorites = favoriteService.findFavorites(userPrincipal.getUsername()); + return ResponseEntity.ok().body(favorites); + } + + @DeleteMapping("/favorites/{id}") + public ResponseEntity deleteFavorite(@AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable Long id) { + favoriteService.deleteFavorite(userPrincipal.getUsername(), id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/nextstep/member/application/CustomOAuth2UserService.java b/src/main/java/nextstep/member/application/CustomOAuth2UserService.java new file mode 100644 index 000000000..963f1b60f --- /dev/null +++ b/src/main/java/nextstep/member/application/CustomOAuth2UserService.java @@ -0,0 +1,26 @@ +package nextstep.member.application; + +import nextstep.auth.token.oauth2.OAuth2User; +import nextstep.auth.token.oauth2.OAuth2UserRequest; +import nextstep.auth.token.oauth2.OAuth2UserService; +import nextstep.member.domain.CustomOAuth2User; +import nextstep.member.domain.Member; +import nextstep.member.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class CustomOAuth2UserService implements OAuth2UserService { + private MemberRepository memberRepository; + + public CustomOAuth2UserService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) { + Member member = memberRepository.findByEmail(oAuth2UserRequest.getUsername()) + .orElseGet(() -> memberRepository.save(new Member(oAuth2UserRequest.getUsername(), "", oAuth2UserRequest.getAge()))); + + return new CustomOAuth2User(member.getEmail(), member.getRole()); + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/member/application/CustomUserDetailsService.java b/src/main/java/nextstep/member/application/CustomUserDetailsService.java new file mode 100644 index 000000000..b7f74c39d --- /dev/null +++ b/src/main/java/nextstep/member/application/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package nextstep.member.application; + +import nextstep.auth.AuthenticationException; +import nextstep.auth.userdetails.UserDetails; +import nextstep.auth.userdetails.UserDetailsService; +import nextstep.member.domain.CustomUserDetails; +import nextstep.member.domain.Member; +import nextstep.member.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private MemberRepository memberRepository; + + public CustomUserDetailsService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) { + Member member = memberRepository.findByEmail(username).orElseThrow(AuthenticationException::new); + return new CustomUserDetails(member.getEmail(), member.getPassword(), member.getRole()); + } +} diff --git a/src/main/java/nextstep/member/application/MemberService.java b/src/main/java/nextstep/member/application/MemberService.java new file mode 100644 index 000000000..f007954e8 --- /dev/null +++ b/src/main/java/nextstep/member/application/MemberService.java @@ -0,0 +1,41 @@ +package nextstep.member.application; + +import nextstep.member.application.dto.MemberRequest; +import nextstep.member.application.dto.MemberResponse; +import nextstep.member.domain.Member; +import nextstep.member.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class MemberService { + private MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public MemberResponse createMember(MemberRequest request) { + Member member = memberRepository.save(request.toMember()); + return MemberResponse.of(member); + } + + public MemberResponse findMember(Long id) { + Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); + return MemberResponse.of(member); + } + + public void updateMember(Long id, MemberRequest param) { + Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); + member.update(param.toMember()); + } + + public void deleteMember(Long id) { + memberRepository.deleteById(id); + } + + public MemberResponse findMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .map(MemberResponse::of) + .orElseThrow(RuntimeException::new); + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/member/application/dto/MemberRequest.java b/src/main/java/nextstep/member/application/dto/MemberRequest.java new file mode 100644 index 000000000..c7ce4ff12 --- /dev/null +++ b/src/main/java/nextstep/member/application/dto/MemberRequest.java @@ -0,0 +1,34 @@ +package nextstep.member.application.dto; + +import nextstep.member.domain.Member; + +public class MemberRequest { + private String email; + private String password; + private Integer age; + + public MemberRequest() { + } + + public MemberRequest(String email, String password, Integer age) { + this.email = email; + this.password = password; + this.age = age; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public Integer getAge() { + return age; + } + + public Member toMember() { + return new Member(email, password, age); + } +} diff --git a/src/main/java/nextstep/member/application/dto/MemberResponse.java b/src/main/java/nextstep/member/application/dto/MemberResponse.java new file mode 100644 index 000000000..c9d36d06e --- /dev/null +++ b/src/main/java/nextstep/member/application/dto/MemberResponse.java @@ -0,0 +1,34 @@ +package nextstep.member.application.dto; + +import nextstep.member.domain.Member; + +public class MemberResponse { + private Long id; + private String email; + private Integer age; + + public MemberResponse() { + } + + public MemberResponse(Long id, String email, Integer age) { + this.id = id; + this.email = email; + this.age = age; + } + + public static MemberResponse of(Member member) { + return new MemberResponse(member.getId(), member.getEmail(), member.getAge()); + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public Integer getAge() { + return age; + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/member/domain/CustomOAuth2User.java b/src/main/java/nextstep/member/domain/CustomOAuth2User.java new file mode 100644 index 000000000..00e4fadd9 --- /dev/null +++ b/src/main/java/nextstep/member/domain/CustomOAuth2User.java @@ -0,0 +1,23 @@ +package nextstep.member.domain; + +import nextstep.auth.token.oauth2.OAuth2User; + +public class CustomOAuth2User implements OAuth2User { + private String email; + private String role; + + public CustomOAuth2User(String email, String role) { + this.email = email; + this.role = role; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getRole() { + return role; + } +} diff --git a/src/main/java/nextstep/member/domain/CustomUserDetails.java b/src/main/java/nextstep/member/domain/CustomUserDetails.java new file mode 100644 index 000000000..b5a222daf --- /dev/null +++ b/src/main/java/nextstep/member/domain/CustomUserDetails.java @@ -0,0 +1,30 @@ +package nextstep.member.domain; + +import nextstep.auth.userdetails.UserDetails; + +public class CustomUserDetails implements UserDetails { + private String email; + private String password; + private String role; + + public CustomUserDetails(String email, String password, String role) { + this.email = email; + this.password = password; + this.role = role; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getRole() { + return role; + } +} diff --git a/src/main/java/nextstep/member/domain/Member.java b/src/main/java/nextstep/member/domain/Member.java new file mode 100644 index 000000000..6264129ab --- /dev/null +++ b/src/main/java/nextstep/member/domain/Member.java @@ -0,0 +1,63 @@ +package nextstep.member.domain; + +import javax.persistence.*; +import java.util.Objects; + +@Entity +public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(unique = true) + private String email; + private String password; + private Integer age; + private String role; + + public Member() { + } + + public Member(String email, String password, Integer age) { + this.email = email; + this.password = password; + this.age = age; + this.role = RoleType.ROLE_MEMBER.name(); + } + + public Member(String email, String password, Integer age, String role) { + this.email = email; + this.password = password; + this.age = age; + this.role = role; + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public Integer getAge() { + return age; + } + + public String getRole() { + return role; + } + + public void update(Member member) { + this.email = member.email; + this.password = member.password; + this.age = member.age; + } + + public boolean checkPassword(String password) { + return Objects.equals(this.password, password); + } +} diff --git a/src/main/java/nextstep/member/domain/MemberRepository.java b/src/main/java/nextstep/member/domain/MemberRepository.java new file mode 100644 index 000000000..1ec8adee8 --- /dev/null +++ b/src/main/java/nextstep/member/domain/MemberRepository.java @@ -0,0 +1,11 @@ +package nextstep.member.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); + + void deleteByEmail(String email); +} diff --git a/src/main/java/nextstep/member/domain/RoleType.java b/src/main/java/nextstep/member/domain/RoleType.java new file mode 100644 index 000000000..9d9f24de8 --- /dev/null +++ b/src/main/java/nextstep/member/domain/RoleType.java @@ -0,0 +1,6 @@ +package nextstep.member.domain; + +public enum RoleType { + ROLE_ADMIN, + ROLE_MEMBER +} diff --git a/src/main/java/nextstep/member/ui/MemberController.java b/src/main/java/nextstep/member/ui/MemberController.java new file mode 100644 index 000000000..21252c6ce --- /dev/null +++ b/src/main/java/nextstep/member/ui/MemberController.java @@ -0,0 +1,51 @@ +package nextstep.member.ui; + +import nextstep.auth.principal.AuthenticationPrincipal; +import nextstep.auth.principal.UserPrincipal; +import nextstep.member.application.MemberService; +import nextstep.member.application.dto.MemberRequest; +import nextstep.member.application.dto.MemberResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@RestController +public class MemberController { + private MemberService memberService; + + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + @PostMapping("/members") + public ResponseEntity createMember(@RequestBody MemberRequest request) { + MemberResponse member = memberService.createMember(request); + return ResponseEntity.created(URI.create("/members/" + member.getId())).build(); + } + + @GetMapping("/members/{id}") + public ResponseEntity findMember(@PathVariable Long id) { + MemberResponse member = memberService.findMember(id); + return ResponseEntity.ok().body(member); + } + + @PutMapping("/members/{id}") + public ResponseEntity updateMember(@PathVariable Long id, @RequestBody MemberRequest param) { + memberService.updateMember(id, param); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/members/{id}") + public ResponseEntity deleteMember(@PathVariable Long id) { + memberService.deleteMember(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/members/me") + public ResponseEntity findMemberOfMine(@AuthenticationPrincipal UserPrincipal userPrincipal) { + MemberResponse member = memberService.findMemberByEmail(userPrincipal.getUsername()); + return ResponseEntity.ok().body(member); + } +} + diff --git a/src/main/java/nextstep/subway/applicaion/LineService.java b/src/main/java/nextstep/subway/applicaion/LineService.java new file mode 100644 index 000000000..b359fc433 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/LineService.java @@ -0,0 +1,89 @@ +package nextstep.subway.applicaion; + +import nextstep.subway.applicaion.dto.LineRequest; +import nextstep.subway.applicaion.dto.LineResponse; +import nextstep.subway.applicaion.dto.SectionRequest; +import nextstep.subway.applicaion.dto.StationResponse; +import nextstep.subway.domain.Line; +import nextstep.subway.domain.LineRepository; +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; + +@Service +@Transactional(readOnly = true) +public class LineService { + private LineRepository lineRepository; + private StationService stationService; + + public LineService(LineRepository lineRepository, StationService stationService) { + this.lineRepository = lineRepository; + this.stationService = stationService; + } + + @Transactional + public LineResponse saveLine(LineRequest request) { + Line line = lineRepository.save(new Line(request.getName(), request.getColor())); + if (request.getUpStationId() != null && request.getDownStationId() != null && request.getDistance() != 0) { + Station upStation = stationService.findById(request.getUpStationId()); + Station downStation = stationService.findById(request.getDownStationId()); + line.addSection(upStation, downStation, request.getDistance()); + } + return LineResponse.of(line); + } + + public List findLines() { + return lineRepository.findAll(); + } + + public List findLineResponses() { + return lineRepository.findAll().stream() + .map(LineResponse::of) + .collect(Collectors.toList()); + } + + public LineResponse findLineResponseById(Long id) { + return LineResponse.of(findById(id)); + } + + public Line findById(Long id) { + return lineRepository.findById(id).orElseThrow(IllegalArgumentException::new); + } + + @Transactional + public void updateLine(Long id, LineRequest lineRequest) { + Line line = findById(id); + line.update(lineRequest.getName(), lineRequest.getColor()); + } + + @Transactional + public void deleteLine(Long id) { + lineRepository.deleteById(id); + } + + @Transactional + public void addSection(Long lineId, SectionRequest sectionRequest) { + Station upStation = stationService.findById(sectionRequest.getUpStationId()); + Station downStation = stationService.findById(sectionRequest.getDownStationId()); + Line line = findById(lineId); + + line.addSection(upStation, downStation, sectionRequest.getDistance()); + } + + private List createStationResponses(Line line) { + return line.getStations().stream() + .map(it -> stationService.createStationResponse(it)) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteSection(Long lineId, Long stationId) { + Line line = findById(lineId); + Station station = stationService.findById(stationId); + + line.deleteSection(station); + } +} diff --git a/src/main/java/nextstep/subway/applicaion/PathService.java b/src/main/java/nextstep/subway/applicaion/PathService.java new file mode 100644 index 000000000..442439095 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/PathService.java @@ -0,0 +1,31 @@ +package nextstep.subway.applicaion; + +import nextstep.subway.applicaion.dto.PathResponse; +import nextstep.subway.domain.Line; +import nextstep.subway.domain.Path; +import nextstep.subway.domain.Station; +import nextstep.subway.domain.SubwayMap; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PathService { + private LineService lineService; + private StationService stationService; + + public PathService(LineService lineService, StationService stationService) { + this.lineService = lineService; + this.stationService = stationService; + } + + public PathResponse findPath(Long source, Long target) { + Station upStation = stationService.findById(source); + Station downStation = stationService.findById(target); + List lines = lineService.findLines(); + SubwayMap subwayMap = new SubwayMap(lines); + Path path = subwayMap.findPath(upStation, downStation); + + return PathResponse.of(path); + } +} diff --git a/src/main/java/nextstep/subway/applicaion/StationService.java b/src/main/java/nextstep/subway/applicaion/StationService.java new file mode 100644 index 000000000..c9773399b --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/StationService.java @@ -0,0 +1,54 @@ +package nextstep.subway.applicaion; + +import nextstep.subway.applicaion.dto.StationRequest; +import nextstep.subway.applicaion.dto.StationResponse; +import nextstep.subway.domain.Station; +import nextstep.subway.domain.StationRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Transactional(readOnly = true) +public class StationService { + private StationRepository stationRepository; + + public StationService(StationRepository stationRepository) { + this.stationRepository = stationRepository; + } + + @Transactional + public StationResponse saveStation(StationRequest stationRequest) { + Station station = stationRepository.save(new Station(stationRequest.getName())); + return StationResponse.of(station); + } + + public List findAllStations() { + return stationRepository.findAll().stream() + .map(StationResponse::of) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteStationById(Long id) { + stationRepository.deleteById(id); + } + + public StationResponse createStationResponse(Station station) { + return new StationResponse( + station.getId(), + station.getName() + ); + } + + public Station findById(Long id) { + return stationRepository.findById(id).orElseThrow(IllegalArgumentException::new); + } + + public List findAllStationsById(Set stationIds) { + return stationRepository.findAllById(stationIds); + } +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/LineRequest.java b/src/main/java/nextstep/subway/applicaion/dto/LineRequest.java new file mode 100644 index 000000000..737a70914 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/LineRequest.java @@ -0,0 +1,29 @@ +package nextstep.subway.applicaion.dto; + +public class LineRequest { + private String name; + private String color; + private Long upStationId; + private Long downStationId; + private int distance; + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public Long getUpStationId() { + return upStationId; + } + + public Long getDownStationId() { + return downStationId; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/LineResponse.java b/src/main/java/nextstep/subway/applicaion/dto/LineResponse.java new file mode 100644 index 000000000..87fc496ab --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/LineResponse.java @@ -0,0 +1,44 @@ +package nextstep.subway.applicaion.dto; + +import nextstep.subway.domain.Line; + +import java.util.List; +import java.util.stream.Collectors; + +public class LineResponse { + private Long id; + private String name; + private String color; + private List stations; + + public static LineResponse of(Line line) { + List stations = line.getStations().stream() + .map(StationResponse::of) + .collect(Collectors.toList()); + return new LineResponse(line.getId(), line.getName(), line.getColor(), stations); + } + + public LineResponse(Long id, String name, String color, List stations) { + this.id = id; + this.name = name; + this.color = color; + this.stations = stations; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public List getStations() { + return stations; + } +} + diff --git a/src/main/java/nextstep/subway/applicaion/dto/PathResponse.java b/src/main/java/nextstep/subway/applicaion/dto/PathResponse.java new file mode 100644 index 000000000..64065369e --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/PathResponse.java @@ -0,0 +1,33 @@ +package nextstep.subway.applicaion.dto; + +import nextstep.subway.domain.Path; + +import java.util.List; +import java.util.stream.Collectors; + +public class PathResponse { + private List stations; + private int distance; + + public PathResponse(List stations, int distance) { + this.stations = stations; + this.distance = distance; + } + + public static PathResponse of(Path path) { + List stations = path.getStations().stream() + .map(StationResponse::of) + .collect(Collectors.toList()); + int distance = path.extractDistance(); + + return new PathResponse(stations, distance); + } + + public List getStations() { + return stations; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/SectionRequest.java b/src/main/java/nextstep/subway/applicaion/dto/SectionRequest.java new file mode 100644 index 000000000..39341d85b --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/SectionRequest.java @@ -0,0 +1,28 @@ +package nextstep.subway.applicaion.dto; + +public class SectionRequest { + private Long upStationId; + private Long downStationId; + private int distance; + + public SectionRequest() { + } + + public SectionRequest(Long upStationId, Long downStationId, int distance) { + this.upStationId = upStationId; + this.downStationId = downStationId; + this.distance = distance; + } + + public Long getUpStationId() { + return upStationId; + } + + public Long getDownStationId() { + return downStationId; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/StationRequest.java b/src/main/java/nextstep/subway/applicaion/dto/StationRequest.java new file mode 100644 index 000000000..b29928d41 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/StationRequest.java @@ -0,0 +1,9 @@ +package nextstep.subway.applicaion.dto; + +public class StationRequest { + private String name; + + public String getName() { + return name; + } +} diff --git a/src/main/java/nextstep/subway/applicaion/dto/StationResponse.java b/src/main/java/nextstep/subway/applicaion/dto/StationResponse.java new file mode 100644 index 000000000..ca9b5a099 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/StationResponse.java @@ -0,0 +1,37 @@ +package nextstep.subway.applicaion.dto; + +import nextstep.subway.domain.Station; + +import java.util.List; +import java.util.stream.Collectors; + +public class StationResponse { + private Long id; + private String name; + + public static StationResponse of(Station station) { + return new StationResponse(station.getId(), station.getName()); + } + + public static List listOf(List stations) { + return stations.stream() + .map(StationResponse::of) + .collect(Collectors.toList()); + } + + public StationResponse() { + } + + public StationResponse(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/nextstep/subway/domain/Line.java b/src/main/java/nextstep/subway/domain/Line.java new file mode 100644 index 000000000..85bd61c4c --- /dev/null +++ b/src/main/java/nextstep/subway/domain/Line.java @@ -0,0 +1,61 @@ +package nextstep.subway.domain; + +import javax.persistence.*; +import java.util.List; + +@Entity +public class Line { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String color; + + @Embedded + private Sections sections = new Sections(); + + public Line() { + } + + public Line(String name, String color) { + this.name = name; + this.color = color; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public List
getSections() { + return sections.getSections(); + } + + public void update(String name, String color) { + if (name != null) { + this.name = name; + } + if (color != null) { + this.color = color; + } + } + + public void addSection(Station upStation, Station downStation, int distance) { + sections.add(new Section(this, upStation, downStation, distance)); + } + + public List getStations() { + return sections.getStations(); + } + + public void deleteSection(Station station) { + sections.delete(station); + } +} diff --git a/src/main/java/nextstep/subway/domain/LineRepository.java b/src/main/java/nextstep/subway/domain/LineRepository.java new file mode 100644 index 000000000..aff3bde80 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/LineRepository.java @@ -0,0 +1,10 @@ +package nextstep.subway.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LineRepository extends JpaRepository { + @Override + List findAll(); +} \ No newline at end of file diff --git a/src/main/java/nextstep/subway/domain/Path.java b/src/main/java/nextstep/subway/domain/Path.java new file mode 100644 index 000000000..3a0ba8e3a --- /dev/null +++ b/src/main/java/nextstep/subway/domain/Path.java @@ -0,0 +1,23 @@ +package nextstep.subway.domain; + +import java.util.List; + +public class Path { + private Sections sections; + + public Path(Sections sections) { + this.sections = sections; + } + + public Sections getSections() { + return sections; + } + + public int extractDistance() { + return sections.totalDistance(); + } + + public List getStations() { + return sections.getStations(); + } +} diff --git a/src/main/java/nextstep/subway/domain/Section.java b/src/main/java/nextstep/subway/domain/Section.java new file mode 100644 index 000000000..450af16f0 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/Section.java @@ -0,0 +1,70 @@ +package nextstep.subway.domain; + +import org.jgrapht.graph.DefaultWeightedEdge; + +import javax.persistence.*; + +@Entity +public class Section extends DefaultWeightedEdge { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(cascade = CascadeType.PERSIST) + @JoinColumn(name = "line_id") + private Line line; + + @ManyToOne(cascade = CascadeType.PERSIST) + @JoinColumn(name = "up_station_id") + private Station upStation; + + @ManyToOne(cascade = CascadeType.PERSIST) + @JoinColumn(name = "down_station_id") + private Station downStation; + + private int distance; + + public Section() { + + } + + public Section(Line line, Station upStation, Station downStation, int distance) { + this.line = line; + this.upStation = upStation; + this.downStation = downStation; + this.distance = distance; + } + + public Long getId() { + return id; + } + + public Line getLine() { + return line; + } + + public Station getUpStation() { + return upStation; + } + + public Station getDownStation() { + return downStation; + } + + public int getDistance() { + return distance; + } + + public boolean isSameUpStation(Station station) { + return this.upStation == station; + } + + public boolean isSameDownStation(Station station) { + return this.downStation == station; + } + + public boolean hasDuplicateSection(Station upStation, Station downStation) { + return (this.upStation == upStation && this.downStation == downStation) + || (this.upStation == downStation && this.downStation == upStation); + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/subway/domain/SectionEdge.java b/src/main/java/nextstep/subway/domain/SectionEdge.java new file mode 100644 index 000000000..2ce68c71f --- /dev/null +++ b/src/main/java/nextstep/subway/domain/SectionEdge.java @@ -0,0 +1,19 @@ +package nextstep.subway.domain; + +import org.jgrapht.graph.DefaultWeightedEdge; + +public class SectionEdge extends DefaultWeightedEdge { + private Section section; + + public static SectionEdge of(Section section) { + return new SectionEdge(section); + } + + public SectionEdge(Section section) { + this.section = section; + } + + public Section getSection() { + return section; + } +} diff --git a/src/main/java/nextstep/subway/domain/Sections.java b/src/main/java/nextstep/subway/domain/Sections.java new file mode 100644 index 000000000..03b4e1933 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/Sections.java @@ -0,0 +1,153 @@ +package nextstep.subway.domain; + +import javax.persistence.CascadeType; +import javax.persistence.Embeddable; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Embeddable +public class Sections { + @OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) + private List
sections = new ArrayList<>(); + + public Sections() { + } + + public Sections(List
sections) { + this.sections = sections; + } + + public List
getSections() { + return sections; + } + + public void add(Section section) { + if (this.sections.isEmpty()) { + this.sections.add(section); + return; + } + + checkDuplicateSection(section); + + rearrangeSectionWithUpStation(section); + rearrangeSectionWithDownStation(section); + + sections.add(section); + } + + public void delete(Station station) { + if (this.sections.size() <= 1) { + throw new IllegalArgumentException(); + } + + Optional
upSection = findSectionAsUpStation(station); + Optional
downSection = findSectionAsDownStation(station); + + addNewSectionForDelete(upSection, downSection); + + upSection.ifPresent(it -> this.sections.remove(it)); + downSection.ifPresent(it -> this.sections.remove(it)); + } + + public List getStations() { + if (this.sections.isEmpty()) { + return Collections.emptyList(); + } + + Station upStation = findFirstUpStation(); + List result = new ArrayList<>(); + result.add(upStation); + + while (true) { + Station finalUpStation = upStation; + Optional
section = findSectionAsUpStation(finalUpStation); + + if (!section.isPresent()) { + break; + } + + upStation = section.get().getDownStation(); + result.add(upStation); + } + + return result; + } + + private void checkDuplicateSection(Section section) { + sections.stream() + .filter(it -> it.hasDuplicateSection(section.getUpStation(), section.getDownStation())) + .findFirst() + .ifPresent(it -> { + throw new IllegalArgumentException(); + }); + } + + private void rearrangeSectionWithDownStation(Section section) { + sections.stream() + .filter(it -> it.isSameDownStation(section.getDownStation())) + .findFirst() + .ifPresent(it -> { + // 신규 구간의 상행역과 기존 구간의 상행역에 대한 구간을 추가한다. + sections.add(new Section(section.getLine(), it.getUpStation(), section.getUpStation(), it.getDistance() - section.getDistance())); + sections.remove(it); + }); + } + + private void rearrangeSectionWithUpStation(Section section) { + sections.stream() + .filter(it -> it.isSameUpStation(section.getUpStation())) + .findFirst() + .ifPresent(it -> { + // 신규 구간의 하행역과 기존 구간의 하행역에 대한 구간을 추가한다. + sections.add(new Section(section.getLine(), section.getDownStation(), it.getDownStation(), it.getDistance() - section.getDistance())); + sections.remove(it); + }); + } + + private Station findFirstUpStation() { + List upStations = this.sections.stream() + .map(Section::getUpStation) + .collect(Collectors.toList()); + List downStations = this.sections.stream() + .map(Section::getDownStation) + .collect(Collectors.toList()); + + return upStations.stream() + .filter(it -> !downStations.contains(it)) + .findFirst() + .orElseThrow(RuntimeException::new); + } + + private void addNewSectionForDelete(Optional
upSection, Optional
downSection) { + if (upSection.isPresent() && downSection.isPresent()) { + Section newSection = new Section( + upSection.get().getLine(), + downSection.get().getUpStation(), + upSection.get().getDownStation(), + upSection.get().getDistance() + downSection.get().getDistance() + ); + + this.sections.add(newSection); + } + } + + private Optional
findSectionAsUpStation(Station finalUpStation) { + return this.sections.stream() + .filter(it -> it.isSameUpStation(finalUpStation)) + .findFirst(); + } + + private Optional
findSectionAsDownStation(Station station) { + return this.sections.stream() + .filter(it -> it.isSameDownStation(station)) + .findFirst(); + } + + public int totalDistance() { + return sections.stream().mapToInt(Section::getDistance).sum(); + } +} diff --git a/src/main/java/nextstep/subway/domain/Station.java b/src/main/java/nextstep/subway/domain/Station.java new file mode 100644 index 000000000..79e394179 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/Station.java @@ -0,0 +1,29 @@ +package nextstep.subway.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Station { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + public Station() { + } + + public Station(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/nextstep/subway/domain/StationRepository.java b/src/main/java/nextstep/subway/domain/StationRepository.java new file mode 100644 index 000000000..825227377 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/StationRepository.java @@ -0,0 +1,6 @@ +package nextstep.subway.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StationRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/nextstep/subway/domain/SubwayMap.java b/src/main/java/nextstep/subway/domain/SubwayMap.java new file mode 100644 index 000000000..8058964c7 --- /dev/null +++ b/src/main/java/nextstep/subway/domain/SubwayMap.java @@ -0,0 +1,56 @@ +package nextstep.subway.domain; + +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; + +import java.util.List; +import java.util.stream.Collectors; + +public class SubwayMap { + private List lines; + + public SubwayMap(List lines) { + this.lines = lines; + } + + public Path findPath(Station source, Station target) { + SimpleDirectedWeightedGraph graph = new SimpleDirectedWeightedGraph<>(SectionEdge.class); + + // 지하철 역(정점)을 등록 + lines.stream() + .flatMap(it -> it.getStations().stream()) + .distinct() + .collect(Collectors.toList()) + .forEach(it -> graph.addVertex(it)); + + // 지하철 역의 연결 정보(간선)을 등록 + lines.stream() + .flatMap(it -> it.getSections().stream()) + .forEach(it -> { + SectionEdge sectionEdge = SectionEdge.of(it); + graph.addEdge(it.getUpStation(), it.getDownStation(), sectionEdge); + graph.setEdgeWeight(sectionEdge, it.getDistance()); + }); + + // 지하철 역의 연결 정보(간선)을 등록 + lines.stream() + .flatMap(it -> it.getSections().stream()) + .map(it -> new Section(it.getLine(), it.getDownStation(), it.getUpStation(), it.getDistance())) + .forEach(it -> { + SectionEdge sectionEdge = SectionEdge.of(it); + graph.addEdge(it.getUpStation(), it.getDownStation(), sectionEdge); + graph.setEdgeWeight(sectionEdge, it.getDistance()); + }); + + // 다익스트라 최단 경로 찾기 + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath<>(graph); + GraphPath result = dijkstraShortestPath.getPath(source, target); + + List
sections = result.getEdgeList().stream() + .map(it -> it.getSection()) + .collect(Collectors.toList()); + + return new Path(new Sections(sections)); + } +} diff --git a/src/main/java/nextstep/subway/ui/ControllerExceptionHandler.java b/src/main/java/nextstep/subway/ui/ControllerExceptionHandler.java new file mode 100644 index 000000000..3ad5d7b65 --- /dev/null +++ b/src/main/java/nextstep/subway/ui/ControllerExceptionHandler.java @@ -0,0 +1,19 @@ +package nextstep.subway.ui; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class ControllerExceptionHandler { + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleIllegalArgsException(DataIntegrityViolationException e) { + return ResponseEntity.badRequest().build(); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgsException(IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } +} diff --git a/src/main/java/nextstep/subway/ui/LineController.java b/src/main/java/nextstep/subway/ui/LineController.java new file mode 100644 index 000000000..34066048b --- /dev/null +++ b/src/main/java/nextstep/subway/ui/LineController.java @@ -0,0 +1,63 @@ +package nextstep.subway.ui; + +import nextstep.subway.applicaion.LineService; +import nextstep.subway.applicaion.dto.LineRequest; +import nextstep.subway.applicaion.dto.LineResponse; +import nextstep.subway.applicaion.dto.SectionRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping("/lines") +public class LineController { + private LineService lineService; + + public LineController(LineService lineService) { + this.lineService = lineService; + } + + @PostMapping + public ResponseEntity createLine(@RequestBody LineRequest lineRequest) { + LineResponse line = lineService.saveLine(lineRequest); + return ResponseEntity.created(URI.create("/lines/" + line.getId())).body(line); + } + + @GetMapping + public ResponseEntity> showLines() { + List responses = lineService.findLineResponses(); + return ResponseEntity.ok().body(responses); + } + + @GetMapping("/{id}") + public ResponseEntity getLine(@PathVariable Long id) { + LineResponse lineResponse = lineService.findLineResponseById(id); + return ResponseEntity.ok().body(lineResponse); + } + + @PutMapping("/{id}") + public ResponseEntity updateLine(@PathVariable Long id, @RequestBody LineRequest lineRequest) { + lineService.updateLine(id, lineRequest); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity updateLine(@PathVariable Long id) { + lineService.deleteLine(id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{lineId}/sections") + public ResponseEntity addSection(@PathVariable Long lineId, @RequestBody SectionRequest sectionRequest) { + lineService.addSection(lineId, sectionRequest); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{lineId}/sections") + public ResponseEntity deleteSection(@PathVariable Long lineId, @RequestParam Long stationId) { + lineService.deleteSection(lineId, stationId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/nextstep/subway/ui/PathController.java b/src/main/java/nextstep/subway/ui/PathController.java new file mode 100644 index 000000000..ad76aba26 --- /dev/null +++ b/src/main/java/nextstep/subway/ui/PathController.java @@ -0,0 +1,22 @@ +package nextstep.subway.ui; + +import nextstep.subway.applicaion.PathService; +import nextstep.subway.applicaion.dto.PathResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PathController { + private PathService pathService; + + public PathController(PathService pathService) { + this.pathService = pathService; + } + + @GetMapping("/paths") + public ResponseEntity findPath(@RequestParam Long source, @RequestParam Long target) { + return ResponseEntity.ok(pathService.findPath(source, target)); + } +} diff --git a/src/main/java/nextstep/subway/ui/StationController.java b/src/main/java/nextstep/subway/ui/StationController.java new file mode 100644 index 000000000..7e56df048 --- /dev/null +++ b/src/main/java/nextstep/subway/ui/StationController.java @@ -0,0 +1,36 @@ +package nextstep.subway.ui; + +import nextstep.subway.applicaion.StationService; +import nextstep.subway.applicaion.dto.StationRequest; +import nextstep.subway.applicaion.dto.StationResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.List; + +@RestController +public class StationController { + private StationService stationService; + + public StationController(StationService stationService) { + this.stationService = stationService; + } + + @PostMapping("/stations") + public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { + StationResponse station = stationService.saveStation(stationRequest); + return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station); + } + + @GetMapping(value = "/stations") + public ResponseEntity> showStations() { + return ResponseEntity.ok().body(stationService.findAllStations()); + } + + @DeleteMapping("/stations/{id}") + public ResponseEntity deleteStation(@PathVariable Long id) { + stationService.deleteStationById(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/AcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/AcceptanceTest.java new file mode 100644 index 000000000..9077de158 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/AcceptanceTest.java @@ -0,0 +1,38 @@ +package nextstep.subway.acceptance; + +import io.restassured.RestAssured; +import nextstep.subway.utils.DataLoader; +import nextstep.subway.utils.DatabaseCleanup; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +import static nextstep.subway.acceptance.member.MemberSteps.베어러_인증_로그인_요청; + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +public class AcceptanceTest { + public static final String EMAIL = "admin@email.com"; + public static final String PASSWORD = "password"; + + @Autowired + private DatabaseCleanup databaseCleanup; + @Autowired + private DataLoader dataLoader; + + @LocalServerPort + private int port; + + public String 관리자; + + @BeforeEach + public void setUp() { + RestAssured.port = port; + databaseCleanup.execute(); + dataLoader.loadData(); + + 관리자 = 베어러_인증_로그인_요청(EMAIL, PASSWORD).jsonPath().getString("accessToken"); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/AcceptanceTestSteps.java b/src/test/java/nextstep/subway/acceptance/AcceptanceTestSteps.java new file mode 100644 index 000000000..c00b05e35 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/AcceptanceTestSteps.java @@ -0,0 +1,18 @@ +package nextstep.subway.acceptance; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +public class AcceptanceTestSteps { + + public static RequestSpecification given() { + return RestAssured + .given().log().all(); + } + + public static RequestSpecification given(String token) { + return RestAssured + .given().log().all() + .auth().oauth2(token); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/auth/AuthAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/auth/AuthAcceptanceTest.java new file mode 100644 index 000000000..e7cf237d5 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/auth/AuthAcceptanceTest.java @@ -0,0 +1,32 @@ +package nextstep.subway.acceptance.auth; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import nextstep.subway.utils.GithubResponses; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static nextstep.subway.acceptance.member.MemberSteps.깃허브_인증_로그인_요청; +import static nextstep.subway.acceptance.member.MemberSteps.베어러_인증_로그인_요청; +import static org.assertj.core.api.Assertions.assertThat; + +class AuthAcceptanceTest extends AcceptanceTest { + + + @DisplayName("Bearer Auth") + @Test + void bearerAuth() { + ExtractableResponse response = 베어러_인증_로그인_요청(EMAIL, PASSWORD); + + assertThat(response.jsonPath().getString("accessToken")).isNotBlank(); + } + + @DisplayName("Github Auth") + @Test + void githubAuth() { + ExtractableResponse response = 깃허브_인증_로그인_요청(GithubResponses.사용자1.getCode()); + + assertThat(response.jsonPath().getString("accessToken")).isNotBlank(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/acceptance/favorite/FavoriteAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/favorite/FavoriteAcceptanceTest.java new file mode 100644 index 000000000..86e8e3343 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/favorite/FavoriteAcceptanceTest.java @@ -0,0 +1,107 @@ +package nextstep.subway.acceptance.favorite; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import nextstep.subway.acceptance.line.LineSteps; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.favorite.FavoriteSteps.*; +import static nextstep.subway.acceptance.line.LineSteps.지하철_노선에_지하철_구간_생성_요청; +import static nextstep.subway.acceptance.member.MemberSteps.베어러_인증_로그인_요청; +import static nextstep.subway.acceptance.station.StationSteps.지하철역_생성_요청; + +@DisplayName("즐겨찾기 관련 기능") +public class FavoriteAcceptanceTest extends AcceptanceTest { + public static final String EMAIL = "member@email.com"; + public static final String PASSWORD = "password"; + + private Long 신분당선; + private Long 이호선; + private Long 삼호선; + private Long 강남역; + private Long 양재역; + private Long 교대역; + private Long 남부터미널역; + private Long 광교역; + private String 사용자; + + /** + * 교대역 --- *2호선* --- 강남역 + * | | + * *3호선* *신분당선* + * | | + * 남부터미널역 --- *3호선* --- 양재 + */ + @BeforeEach + public void setUp() { + super.setUp(); + + 광교역 = 지하철역_생성_요청(관리자, "광교역").jsonPath().getLong("id"); + 교대역 = 지하철역_생성_요청(관리자, "교대역").jsonPath().getLong("id"); + 강남역 = 지하철역_생성_요청(관리자, "강남역").jsonPath().getLong("id"); + 양재역 = 지하철역_생성_요청(관리자, "양재역").jsonPath().getLong("id"); + 남부터미널역 = 지하철역_생성_요청(관리자, "남부터미널역").jsonPath().getLong("id"); + + 이호선 = 지하철_노선_생성_요청("2호선", "green", 교대역, 강남역, 10); + 신분당선 = 지하철_노선_생성_요청("신분당선", "red", 강남역, 양재역, 10); + 삼호선 = 지하철_노선_생성_요청("3호선", "orange", 교대역, 남부터미널역, 2); + + 지하철_노선에_지하철_구간_생성_요청(관리자, 삼호선, createSectionCreateParams(남부터미널역, 양재역, 3)); + + 사용자 = 베어러_인증_로그인_요청(EMAIL, PASSWORD).jsonPath().getString("accessToken"); + } + + @DisplayName("즐겨찾기를 관리한다.") + @Test + void manageFavorite() { + // when + ExtractableResponse createResponse = 즐겨찾기_생성을_요청(사용자, 강남역, 남부터미널역); + // then + 즐겨찾기_생성됨(createResponse); + + // when + ExtractableResponse findResponse = 즐겨찾기_목록_조회_요청(사용자); + // then + 즐겨찾기_목록_조회됨(findResponse); + + // when + ExtractableResponse deleteResponse = 즐겨찾기_삭제_요청(사용자, createResponse); + // then + 즐겨찾기_삭제됨(deleteResponse); + } + + @DisplayName("연결되지 않은 경로로 즐겨찾기를 추가한다.") + @Test + void invalidPath() { + // when + ExtractableResponse createResponse = 즐겨찾기_생성을_요청(사용자, 강남역, 광교역); + // then + 즐겨찾기_생성_실패함(createResponse); + } + + private Long 지하철_노선_생성_요청(String name, String color, Long upStation, Long downStation, int distance) { + Map lineCreateParams; + lineCreateParams = new HashMap<>(); + lineCreateParams.put("name", name); + lineCreateParams.put("color", color); + lineCreateParams.put("upStationId", upStation + ""); + lineCreateParams.put("downStationId", downStation + ""); + lineCreateParams.put("distance", distance + ""); + + return LineSteps.지하철_노선_생성_요청(관리자, lineCreateParams).jsonPath().getLong("id"); + } + + private Map createSectionCreateParams(Long upStationId, Long downStationId, int distance) { + Map params = new HashMap<>(); + params.put("upStationId", upStationId + ""); + params.put("downStationId", downStationId + ""); + params.put("distance", distance + ""); + return params; + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/acceptance/favorite/FavoriteSteps.java b/src/test/java/nextstep/subway/acceptance/favorite/FavoriteSteps.java new file mode 100644 index 000000000..a80370c39 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/favorite/FavoriteSteps.java @@ -0,0 +1,60 @@ +package nextstep.subway.acceptance.favorite; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FavoriteSteps { + public static ExtractableResponse 즐겨찾기_생성을_요청(String accessToken, Long source, Long target) { + Map params = new HashMap<>(); + params.put("source", source + ""); + params.put("target", target + ""); + + return RestAssured.given().log().all() + .auth().oauth2(accessToken) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/favorites") + .then().log().all().extract(); + } + + public static ExtractableResponse 즐겨찾기_목록_조회_요청(String accessToken) { + return RestAssured.given().log().all() + .auth().oauth2(accessToken) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/favorites") + .then().log().all().extract(); + } + + public static ExtractableResponse 즐겨찾기_삭제_요청(String accessToken, ExtractableResponse response) { + String uri = response.header("Location"); + + return RestAssured.given().log().all() + .auth().oauth2(accessToken) + .when().delete(uri) + .then().log().all().extract(); + } + + public static void 즐겨찾기_생성됨(ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + public static void 즐겨찾기_생성_실패함(ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + public static void 즐겨찾기_목록_조회됨(ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + public static void 즐겨찾기_삭제됨(ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/line/LineAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/line/LineAcceptanceTest.java new file mode 100644 index 000000000..2f04994a5 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/line/LineAcceptanceTest.java @@ -0,0 +1,123 @@ +package nextstep.subway.acceptance.line; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.line.LineSteps.*; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지하철 노선 관리 기능") +class LineAcceptanceTest extends AcceptanceTest { + /** + * When 지하철 노선을 생성하면 + * Then 지하철 노선 목록 조회 시 생성한 노선을 찾을 수 있다 + */ + @DisplayName("지하철 노선 생성") + @Test + void createLine() { + // when + ExtractableResponse response = 지하철_노선_생성_요청("2호선", "green"); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + ExtractableResponse listResponse = 지하철_노선_목록_조회_요청(); + + assertThat(listResponse.jsonPath().getList("name")).contains("2호선"); + } + + /** + * Given 2개의 지하철 노선을 생성하고 + * When 지하철 노선 목록을 조회하면 + * Then 지하철 노선 목록 조회 시 2개의 노선을 조회할 수 있다. + */ + @DisplayName("지하철 노선 목록 조회") + @Test + void getLines() { + // given + 지하철_노선_생성_요청("2호선", "green"); + 지하철_노선_생성_요청("3호선", "orange"); + + // when + ExtractableResponse response = 지하철_노선_목록_조회_요청(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("name")).contains("2호선", "3호선"); + } + + /** + * Given 지하철 노선을 생성하고 + * When 생성한 지하철 노선을 조회하면 + * Then 생성한 지하철 노선의 정보를 응답받을 수 있다. + */ + @DisplayName("지하철 노선 조회") + @Test + void getLine() { + // given + ExtractableResponse createResponse = 지하철_노선_생성_요청("2호선", "green"); + + // when + ExtractableResponse response = 지하철_노선_조회_요청(createResponse); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getString("name")).isEqualTo("2호선"); + } + + /** + * Given 지하철 노선을 생성하고 + * When 생성한 지하철 노선을 수정하면 + * Then 해당 지하철 노선 정보는 수정된다 + */ + @DisplayName("지하철 노선 수정") + @Test + void updateLine() { + // given + ExtractableResponse createResponse = 지하철_노선_생성_요청("2호선", "green"); + + // when + Map params = new HashMap<>(); + params.put("color", "red"); + RestAssured + .given().log().all() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().put(createResponse.header("location")) + .then().log().all().extract(); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(createResponse); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getString("color")).isEqualTo("red"); + } + + /** + * Given 지하철 노선을 생성하고 + * When 생성한 지하철 노선을 삭제하면 + * Then 해당 지하철 노선 정보는 삭제된다 + */ + @DisplayName("지하철 노선 삭제") + @Test + void deleteLine() { + // given + ExtractableResponse createResponse = 지하철_노선_생성_요청("2호선", "green"); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .when().delete(createResponse.header("location")) + .then().log().all().extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/line/LineSectionAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/line/LineSectionAcceptanceTest.java new file mode 100644 index 000000000..cf5a65742 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/line/LineSectionAcceptanceTest.java @@ -0,0 +1,147 @@ +package nextstep.subway.acceptance.line; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.line.LineSteps.*; +import static nextstep.subway.acceptance.station.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지하철 구간 관리 기능") +class LineSectionAcceptanceTest extends AcceptanceTest { + private Long 신분당선; + + private Long 강남역; + private Long 양재역; + + /** + * Given 지하철역과 노선 생성을 요청 하고 + */ + @BeforeEach + public void setUp() { + super.setUp(); + + 강남역 = 지하철역_생성_요청("강남역").jsonPath().getLong("id"); + 양재역 = 지하철역_생성_요청("양재역").jsonPath().getLong("id"); + + Map lineCreateParams = createLineCreateParams(강남역, 양재역); + 신분당선 = 지하철_노선_생성_요청(lineCreateParams).jsonPath().getLong("id"); + } + + /** + * When 지하철 노선에 새로운 구간 추가를 요청 하면 + * Then 노선에 새로운 구간이 추가된다 + */ + @DisplayName("지하철 노선에 구간을 등록") + @Test + void addLineSection() { + // when + Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id"); + 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역)); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(신분당선); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 양재역, 정자역); + } + + /** + * When 지하철 노선 가운데에 새로운 구간 추가를 요청 하면 + * Then 노선에 새로운 구간이 추가된다 + */ + @DisplayName("지하철 노선 가운데에 구간을 추가") + @Test + void addLineSectionMiddle() { + // when + Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id"); + 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(강남역, 정자역)); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(신분당선); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 정자역, 양재역); + } + + /** + * When 지하철 노선에 이미 존재하는 구간 추가를 요청 하면 + * Then 노선에 새로운 구간추가를 실패한다 + */ + @DisplayName("이미 존재하는 구간을 추가") + @Test + void addSectionAlreadyIncluded() { + // when + ExtractableResponse response = 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(강남역, 양재역)); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + /** + * Given 지하철 노선에 새로운 구간 추가를 요청 하고 + * When 지하철 노선의 마지막 구간 제거를 요청 하면 + * Then 노선에 구간이 제거된다 + */ + @DisplayName("지하철 노선의 마지막 구간을 제거") + @Test + void removeLineSection() { + // given + Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id"); + 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역)); + + // when + 지하철_노선에_지하철_구간_제거_요청(신분당선, 정자역); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(신분당선); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 양재역); + } + + /** + * Given 지하철 노선에 새로운 구간 추가를 요청 하고 + * When 지하철 노선의 가운데 구간 제거를 요청 하면 + * Then 노선에 구간이 제거된다 + */ + @DisplayName("지하철 노선의 가운데 구간을 제거") + @Test + void removeLineSectionInMiddle() { + // given + Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id"); + 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역)); + + // when + 지하철_노선에_지하철_구간_제거_요청(신분당선, 양재역); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(신분당선); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 정자역); + } + + private Map createLineCreateParams(Long upStationId, Long downStationId) { + Map lineCreateParams; + lineCreateParams = new HashMap<>(); + lineCreateParams.put("name", "신분당선"); + lineCreateParams.put("color", "bg-red-600"); + lineCreateParams.put("upStationId", upStationId + ""); + lineCreateParams.put("downStationId", downStationId + ""); + lineCreateParams.put("distance", 10 + ""); + return lineCreateParams; + } + + private Map createSectionCreateParams(Long upStationId, Long downStationId) { + Map params = new HashMap<>(); + params.put("upStationId", upStationId + ""); + params.put("downStationId", downStationId + ""); + params.put("distance", 6 + ""); + return params; + } +} diff --git a/src/test/java/nextstep/subway/acceptance/line/LineSteps.java b/src/test/java/nextstep/subway/acceptance/line/LineSteps.java new file mode 100644 index 000000000..8b5118dec --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/line/LineSteps.java @@ -0,0 +1,86 @@ +package nextstep.subway.acceptance.line; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +public class LineSteps { + public static ExtractableResponse 지하철_노선_생성_요청(String name, String color) { + Map params = new HashMap<>(); + params.put("name", name); + params.put("color", color); + return RestAssured + .given().log().all() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/lines") + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선_생성_요청(String accessToken, Map params) { + return RestAssured + .given().log().all() + .auth().oauth2(accessToken) + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/lines") + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선_목록_조회_요청() { + return RestAssured + .given().log().all() + .when().get("/lines") + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선_조회_요청(ExtractableResponse createResponse) { + return RestAssured + .given().log().all() + .when().get(createResponse.header("location")) + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선_조회_요청(Long id) { + return RestAssured + .given().log().all() + .when().get("/lines/{id}", id) + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선_생성_요청(Map params) { + return RestAssured + .given().log().all() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/lines") + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선에_지하철_구간_생성_요청(Long lineId, Map params) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/lines/{lineId}/sections", lineId) + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선에_지하철_구간_생성_요청(String accessToken, Long lineId, Map params) { + return RestAssured.given().log().all() + .auth().oauth2(accessToken) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/lines/{lineId}/sections", lineId) + .then().log().all().extract(); + } + + public static ExtractableResponse 지하철_노선에_지하철_구간_제거_요청(Long lineId, Long stationId) { + return RestAssured.given().log().all() + .when().delete("/lines/{lineId}/sections?stationId={stationId}", lineId, stationId) + .then().log().all().extract(); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/line/SectionAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/line/SectionAcceptanceTest.java new file mode 100644 index 000000000..7e297f3df --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/line/SectionAcceptanceTest.java @@ -0,0 +1,95 @@ +package nextstep.subway.acceptance.line; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.line.LineSteps.*; +import static nextstep.subway.acceptance.station.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지하철 구간 관리 기능") +class SectionAcceptanceTest extends AcceptanceTest { + private Long 신분당선; + + private Long 강남역; + private Long 양재역; + + /** + * Given 지하철역과 노선 생성을 요청 하고 + */ + @BeforeEach + public void setUp() { + super.setUp(); + + 강남역 = 지하철역_생성_요청("강남역").jsonPath().getLong("id"); + 양재역 = 지하철역_생성_요청("양재역").jsonPath().getLong("id"); + + Map lineCreateParams = createLineCreateParams(강남역, 양재역); + 신분당선 = 지하철_노선_생성_요청(lineCreateParams).jsonPath().getLong("id"); + } + + /** + * When 지하철 노선에 새로운 구간 추가를 요청 하면 + * Then 노선에 새로운 구간이 추가된다 + */ + @DisplayName("지하철 노선에 구간을 등록") + @Test + void addLineSection() { + // when + Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id"); + 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역)); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(신분당선); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 양재역, 정자역); + } + + /** + * Given 지하철 노선에 새로운 구간 추가를 요청 하고 + * When 지하철 노선의 마지막 구간 제거를 요청 하면 + * Then 노선에 구간이 제거된다 + */ + @DisplayName("지하철 노선에 구간을 제거") + @Test + void removeLineSection() { + // given + Long 정자역 = 지하철역_생성_요청("정자역").jsonPath().getLong("id"); + 지하철_노선에_지하철_구간_생성_요청(신분당선, createSectionCreateParams(양재역, 정자역)); + + // when + 지하철_노선에_지하철_구간_제거_요청(신분당선, 정자역); + + // then + ExtractableResponse response = 지하철_노선_조회_요청(신분당선); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(강남역, 양재역); + } + + private Map createLineCreateParams(Long upStationId, Long downStationId) { + Map lineCreateParams; + lineCreateParams = new HashMap<>(); + lineCreateParams.put("name", "신분당선"); + lineCreateParams.put("color", "bg-red-600"); + lineCreateParams.put("upStationId", upStationId + ""); + lineCreateParams.put("downStationId", downStationId + ""); + lineCreateParams.put("distance", 10 + ""); + return lineCreateParams; + } + + private Map createSectionCreateParams(Long upStationId, Long downStationId) { + Map params = new HashMap<>(); + params.put("upStationId", upStationId + ""); + params.put("downStationId", downStationId + ""); + params.put("distance", 6 + ""); + return params; + } +} diff --git a/src/test/java/nextstep/subway/acceptance/member/MemberAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/member/MemberAcceptanceTest.java new file mode 100644 index 000000000..b630d7b26 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/member/MemberAcceptanceTest.java @@ -0,0 +1,79 @@ +package nextstep.subway.acceptance.member; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static nextstep.subway.acceptance.member.MemberSteps.*; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("회원 관리 기능") +class MemberAcceptanceTest extends AcceptanceTest { + public static final String EMAIL = "email@email.com"; + public static final String ADMIN = "admin@email.com"; + public static final String PASSWORD = "password"; + public static final int AGE = 20; + + @DisplayName("회원가입을 한다.") + @Test + void createMember() { + // when + ExtractableResponse response = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @DisplayName("회원 정보를 조회한다.") + @Test + void getMember() { + // given + ExtractableResponse createResponse = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // when + ExtractableResponse response = 회원_정보_조회_요청(createResponse); + + // then + 회원_정보_조회됨(response, EMAIL, AGE); + + } + + @DisplayName("회원 정보를 수정한다.") + @Test + void updateMember() { + // given + ExtractableResponse createResponse = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // when + ExtractableResponse response = 회원_정보_수정_요청(createResponse, "new" + EMAIL, "new" + PASSWORD, AGE); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @DisplayName("회원 정보를 삭제한다.") + @Test + void deleteMember() { + // given + ExtractableResponse createResponse = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // when + ExtractableResponse response = 회원_삭제_요청(createResponse); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @DisplayName("내 정보를 조회한다.") + @Test + void getMyInfo() { + String accessToken = 베어러_인증_로그인_요청(ADMIN, PASSWORD).jsonPath().getString("accessToken"); + + ExtractableResponse response = 회원_정보_조회_요청(accessToken); + + 회원_정보_조회됨(response, ADMIN, AGE); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/acceptance/member/MemberSteps.java b/src/test/java/nextstep/subway/acceptance/member/MemberSteps.java new file mode 100644 index 000000000..a8f59693d --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/member/MemberSteps.java @@ -0,0 +1,114 @@ +package nextstep.subway.acceptance.member; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MemberSteps { + + public static ExtractableResponse 베어러_인증_로그인_요청(String email, String password) { + Map params = new HashMap<>(); + params.put("email", email); + params.put("password", password); + + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/login/token") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + } + + public static ExtractableResponse 깃허브_인증_로그인_요청(String code) { + Map params = new HashMap<>(); + params.put("code", code); + + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/login/github") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + } + + public static ExtractableResponse 회원_생성_요청(String email, String password, Integer age) { + Map params = new HashMap<>(); + params.put("email", email); + params.put("password", password); + params.put("age", age + ""); + + return RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/members") + .then().log().all().extract(); + } + + public static ExtractableResponse 회원_정보_조회_요청(ExtractableResponse response) { + String uri = response.header("Location"); + + return RestAssured.given().log().all() + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get(uri) + .then().log().all() + .extract(); + } + + public static ExtractableResponse 회원_정보_조회_요청(String accessToken) { + + return RestAssured.given().log().all() + .auth().oauth2(accessToken) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/members/me") + .then().log().all() + .extract(); + } + + public static ExtractableResponse 회원_정보_수정_요청(ExtractableResponse response, String email, String password, Integer age) { + String uri = response.header("Location"); + + Map params = new HashMap<>(); + params.put("email", email); + params.put("password", password); + params.put("age", age + ""); + + return RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().put(uri) + .then().log().all().extract(); + } + + public static ExtractableResponse 회원_삭제_요청(ExtractableResponse response) { + String uri = response.header("Location"); + return RestAssured + .given().log().all() + .when().delete(uri) + .then().log().all().extract(); + } + + public static ExtractableResponse 베이직_인증으로_내_회원_정보_조회_요청(String username, String password) { + return RestAssured.given().log().all() + .auth().preemptive().basic(username, password) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/members/me") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + } + + public static void 회원_정보_조회됨(ExtractableResponse response, String email, int age) { + assertThat(response.jsonPath().getString("id")).isNotNull(); + assertThat(response.jsonPath().getString("email")).isEqualTo(email); + assertThat(response.jsonPath().getInt("age")).isEqualTo(age); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/acceptance/path/PathAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/path/PathAcceptanceTest.java new file mode 100644 index 000000000..473b103ce --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/path/PathAcceptanceTest.java @@ -0,0 +1,81 @@ +package nextstep.subway.acceptance.path; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import nextstep.subway.acceptance.line.LineSteps; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.line.LineSteps.지하철_노선에_지하철_구간_생성_요청; +import static nextstep.subway.acceptance.path.PathSteps.두_역의_최단_거리_경로_조회를_요청; +import static nextstep.subway.acceptance.station.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지하철 경로 검색") +class PathAcceptanceTest extends AcceptanceTest { + private Long 교대역; + private Long 강남역; + private Long 양재역; + private Long 남부터미널역; + private Long 이호선; + private Long 신분당선; + private Long 삼호선; + + /** + * 교대역 --- *2호선* --- 강남역 + * | | + * *3호선* *신분당선* + * | | + * 남부터미널역 --- *3호선* --- 양재 + */ + @BeforeEach + public void setUp() { + super.setUp(); + + 교대역 = 지하철역_생성_요청("교대역").jsonPath().getLong("id"); + 강남역 = 지하철역_생성_요청("강남역").jsonPath().getLong("id"); + 양재역 = 지하철역_생성_요청("양재역").jsonPath().getLong("id"); + 남부터미널역 = 지하철역_생성_요청("남부터미널역").jsonPath().getLong("id"); + + 이호선 = 지하철_노선_생성_요청("2호선", "green", 교대역, 강남역, 10); + 신분당선 = 지하철_노선_생성_요청("신분당선", "red", 강남역, 양재역, 10); + 삼호선 = 지하철_노선_생성_요청("3호선", "orange", 교대역, 남부터미널역, 2); + + 지하철_노선에_지하철_구간_생성_요청(삼호선, createSectionCreateParams(남부터미널역, 양재역, 3)); + } + + @DisplayName("두 역의 최단 거리 경로를 조회한다.") + @Test + void findPathByDistance() { + // when + ExtractableResponse response = 두_역의_최단_거리_경로_조회를_요청(교대역, 양재역); + + // then + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactly(교대역, 남부터미널역, 양재역); + } + + private Long 지하철_노선_생성_요청(String name, String color, Long upStation, Long downStation, int distance) { + Map lineCreateParams; + lineCreateParams = new HashMap<>(); + lineCreateParams.put("name", name); + lineCreateParams.put("color", color); + lineCreateParams.put("upStationId", upStation + ""); + lineCreateParams.put("downStationId", downStation + ""); + lineCreateParams.put("distance", distance + ""); + + return LineSteps.지하철_노선_생성_요청(lineCreateParams).jsonPath().getLong("id"); + } + + private Map createSectionCreateParams(Long upStationId, Long downStationId, int distance) { + Map params = new HashMap<>(); + params.put("upStationId", upStationId + ""); + params.put("downStationId", downStationId + ""); + params.put("distance", distance + ""); + return params; + } +} diff --git a/src/test/java/nextstep/subway/acceptance/path/PathSteps.java b/src/test/java/nextstep/subway/acceptance/path/PathSteps.java new file mode 100644 index 000000000..cbbbd0ef9 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/path/PathSteps.java @@ -0,0 +1,25 @@ +package nextstep.subway.acceptance.path; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import nextstep.subway.acceptance.AcceptanceTestSteps; +import org.springframework.http.MediaType; + +import static nextstep.subway.acceptance.AcceptanceTestSteps.given; + +public class PathSteps { + public static ExtractableResponse 두_역의_최단_거리_경로_조회를_요청(RequestSpecification requestSpecification, Long source, Long target) { + return requestSpecification + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/paths?source={sourceId}&target={targetId}", source, target) + .then().log().all().extract(); + } + + public static ExtractableResponse 두_역의_최단_거리_경로_조회를_요청(Long source, Long target) { + return AcceptanceTestSteps.given() + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/paths?source={sourceId}&target={targetId}", source, target) + .then().log().all().extract(); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/station/StationAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/station/StationAcceptanceTest.java new file mode 100644 index 000000000..d89f48617 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/station/StationAcceptanceTest.java @@ -0,0 +1,93 @@ +package nextstep.subway.acceptance.station; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceTest; +import nextstep.subway.applicaion.dto.StationResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import java.util.List; + +import static nextstep.subway.acceptance.station.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지하철역 관련 기능") +public class StationAcceptanceTest extends AcceptanceTest { + + /** + * When 지하철역을 생성하면 + * Then 지하철역이 생성된다 + * Then 지하철역 목록 조회 시 생성한 역을 찾을 수 있다 + */ + @DisplayName("지하철역을 생성한다.") + @Test + void createStation() { + // when + ExtractableResponse response = 지하철역_생성_요청("강남역"); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + + // then + List stationNames = + RestAssured.given().log().all() + .when().get("/stations") + .then().log().all() + .extract().jsonPath().getList("name", String.class); + assertThat(stationNames).containsAnyOf("강남역"); + } + + /** + * Given 2개의 지하철역을 생성하고 + * When 지하철역 목록을 조회하면 + * Then 2개의 지하철역을 응답 받는다 + */ + @DisplayName("지하철역을 조회한다.") + @Test + void getStations() { + // given + 지하철역_생성_요청("강남역"); + 지하철역_생성_요청("역삼역"); + + // when + ExtractableResponse stationResponse = RestAssured.given().log().all() + .when().get("/stations") + .then().log().all() + .extract(); + + // then + List stations = stationResponse.jsonPath().getList(".", StationResponse.class); + assertThat(stations).hasSize(2); + } + + /** + * Given 지하철역을 생성하고 + * When 그 지하철역을 삭제하면 + * Then 그 지하철역 목록 조회 시 생성한 역을 찾을 수 없다 + */ + @DisplayName("지하철역을 제거한다.") + @Test + void deleteStation() { + // given + ExtractableResponse createResponse = 지하철역_생성_요청("강남역"); + + // when + String location = createResponse.header("location"); + RestAssured.given().log().all() + .when() + .delete(location) + .then().log().all() + .extract(); + + // then + List stationNames = + RestAssured.given().log().all() + .when().get("/stations") + .then().log().all() + .extract().jsonPath().getList("name", String.class); + assertThat(stationNames).doesNotContain("강남역"); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/acceptance/station/StationSteps.java b/src/test/java/nextstep/subway/acceptance/station/StationSteps.java new file mode 100644 index 000000000..acf3bcba5 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/station/StationSteps.java @@ -0,0 +1,36 @@ +package nextstep.subway.acceptance.station; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +public class StationSteps { + public static ExtractableResponse 지하철역_생성_요청(String name) { + Map params = new HashMap<>(); + params.put("name", name); + return RestAssured.given().log().all() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/stations") + .then().log().all() + .extract(); + } + + public static ExtractableResponse 지하철역_생성_요청(String accessToken, String name) { + Map params = new HashMap<>(); + params.put("name", name); + return RestAssured.given().log().all() + .auth().oauth2(accessToken) + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/stations") + .then().log().all() + .extract(); + } +} diff --git a/src/test/java/nextstep/subway/documentation/Documentation.java b/src/test/java/nextstep/subway/documentation/Documentation.java index 844d7be5e..34c9823af 100644 --- a/src/test/java/nextstep/subway/documentation/Documentation.java +++ b/src/test/java/nextstep/subway/documentation/Documentation.java @@ -1,26 +1,40 @@ package nextstep.subway.documentation; +import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.restassured3.RestDocumentationFilter; import org.springframework.test.context.ActiveProfiles; import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; @ActiveProfiles("test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ExtendWith(RestDocumentationExtension.class) public class Documentation { protected RequestSpecification spec; + @LocalServerPort + int port; + @BeforeEach public void setUp(RestDocumentationContextProvider restDocumentation) { + RestAssured.port = port; + this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(restDocumentation)) .build(); } + + public RequestSpecification given(RestDocumentationFilter document) { + return RestAssured + .given(spec).log().all() + .filter(document); + } } diff --git a/src/test/java/nextstep/subway/documentation/PathDocumentation.java b/src/test/java/nextstep/subway/documentation/PathDocumentation.java index 7ccd95711..154763e46 100644 --- a/src/test/java/nextstep/subway/documentation/PathDocumentation.java +++ b/src/test/java/nextstep/subway/documentation/PathDocumentation.java @@ -1,19 +1,54 @@ package nextstep.subway.documentation; -import io.restassured.RestAssured; +import nextstep.subway.applicaion.PathService; +import nextstep.subway.applicaion.dto.PathResponse; +import nextstep.subway.applicaion.dto.StationResponse; +import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.RequestParametersSnippet; +import org.springframework.restdocs.restassured3.RestDocumentationFilter; + +import static nextstep.subway.acceptance.path.PathSteps.두_역의_최단_거리_경로_조회를_요청; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + public class PathDocumentation extends Documentation { + @MockBean + private PathService pathService; @Test void path() { - RestAssured - .given().log().all() - .accept(MediaType.APPLICATION_JSON_VALUE) - .queryParam("source", 1L) - .queryParam("target", 2L) - .when().get("/paths") - .then().log().all().extract(); + PathResponse pathResponse = new PathResponse( + Lists.newArrayList( + new StationResponse(1L, "강남역"), + new StationResponse(2L, "역삼역") + ), 10 + ); + + + when(pathService.findPath(anyLong(), anyLong())).thenReturn(pathResponse); + + RequestParametersSnippet requestParametersSnippet = requestParameters(parameterWithName("source").description("출발역"), parameterWithName("target").description("도착역")); + ResponseFieldsSnippet responseFieldsSnippet = responseFields(fieldWithPath("stations[].id").description("역 ID"), + fieldWithPath("stations[].name").description("역 이름"), + fieldWithPath("distance").description("총 거리")); + + RestDocumentationFilter document = document("path", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestParametersSnippet, + responseFieldsSnippet); + 두_역의_최단_거리_경로_조회를_요청(given(document), 1L, 2L); } + + } diff --git a/src/test/java/nextstep/subway/utils/DataLoader.java b/src/test/java/nextstep/subway/utils/DataLoader.java new file mode 100644 index 000000000..fc9926271 --- /dev/null +++ b/src/test/java/nextstep/subway/utils/DataLoader.java @@ -0,0 +1,26 @@ +package nextstep.subway.utils; + +import nextstep.member.domain.Member; +import nextstep.member.domain.MemberRepository; +import nextstep.member.domain.RoleType; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile("test") +@Component +public class DataLoader { + private MemberRepository memberRepository; + + public DataLoader(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public void loadData() { + memberRepository.save(new Member("admin@email.com", "password", 20, RoleType.ROLE_ADMIN.name())); + memberRepository.save(new Member("member@email.com", "password", 20, RoleType.ROLE_MEMBER.name())); + memberRepository.save(new Member(GithubResponses.사용자1.getEmail(), "password", 20, RoleType.ROLE_MEMBER.name())); + memberRepository.save(new Member(GithubResponses.사용자2.getEmail(), "password", 20, RoleType.ROLE_MEMBER.name())); + memberRepository.save(new Member(GithubResponses.사용자3.getEmail(), "password", 20, RoleType.ROLE_MEMBER.name())); + memberRepository.save(new Member(GithubResponses.사용자4.getEmail(), "password", 20, RoleType.ROLE_MEMBER.name())); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/utils/DataLoaderBootstrap.java b/src/test/java/nextstep/subway/utils/DataLoaderBootstrap.java new file mode 100644 index 000000000..be991ce6d --- /dev/null +++ b/src/test/java/nextstep/subway/utils/DataLoaderBootstrap.java @@ -0,0 +1,19 @@ +package nextstep.subway.utils; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +@Component +public class DataLoaderBootstrap implements ApplicationListener { + private DataLoader dataLoader; + + public DataLoaderBootstrap(DataLoader dataLoader) { + this.dataLoader = dataLoader; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + dataLoader.loadData(); + } +} diff --git a/src/test/java/nextstep/subway/utils/DatabaseCleanup.java b/src/test/java/nextstep/subway/utils/DatabaseCleanup.java new file mode 100644 index 000000000..5baf7b8fa --- /dev/null +++ b/src/test/java/nextstep/subway/utils/DatabaseCleanup.java @@ -0,0 +1,40 @@ +package nextstep.subway.utils; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; +import java.util.stream.Collectors; + +@Profile("test") +@Service +public class DatabaseCleanup implements InitializingBean { + @PersistenceContext + private EntityManager entityManager; + + private List tableNames; + + @Override + public void afterPropertiesSet() { + tableNames = entityManager.getMetamodel().getEntities().stream() + .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null) + .map(entity -> entity.getName()) + .collect(Collectors.toList()); + } + + @Transactional + public void execute() { + entityManager.flush(); + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String tableName : tableNames) { + entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate(); + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/subway/utils/GithubResponses.java b/src/test/java/nextstep/subway/utils/GithubResponses.java new file mode 100644 index 000000000..fe3abca5c --- /dev/null +++ b/src/test/java/nextstep/subway/utils/GithubResponses.java @@ -0,0 +1,54 @@ +package nextstep.subway.utils; + +import java.util.Arrays; +import java.util.Objects; + +public enum GithubResponses { + 사용자1("1", "access_token_1", "email1@email.com", 20), + 사용자2("2", "access_token_2", "email2@email.com", 20), + 사용자3("3", "access_token_3", "email3@email.com", 20), + 사용자4("4", "access_token_4", "email4@email.com", 20); + + private String code; + private String accessToken; + private String email; + private int age; + + GithubResponses(String code, String accessToken, String email, int age) { + this.code = code; + this.accessToken = accessToken; + this.email = email; + this.age = age; + } + + public static GithubResponses findByCode(String code) { + return Arrays.stream(values()) + .filter(it -> Objects.equals(it.code, code)) + .findFirst() + .orElseThrow(RuntimeException::new); + } + + public static GithubResponses findByToken(String accessToken) { + return Arrays.stream(values()) + .filter(it -> Objects.equals(it.accessToken, accessToken)) + .findFirst() + .orElseThrow(RuntimeException::new); + } + + public String getCode() { + return code; + } + + public String getAccessToken() { + return accessToken; + } + + public String getEmail() { + return email; + } + + public int getAge() { + return age; + } +} + diff --git a/src/test/java/nextstep/subway/utils/GithubTestController.java b/src/test/java/nextstep/subway/utils/GithubTestController.java new file mode 100644 index 000000000..be311796f --- /dev/null +++ b/src/test/java/nextstep/subway/utils/GithubTestController.java @@ -0,0 +1,29 @@ +package nextstep.subway.utils; + +import nextstep.auth.token.oauth2.github.GithubAccessTokenRequest; +import nextstep.auth.token.oauth2.github.GithubAccessTokenResponse; +import nextstep.auth.token.oauth2.github.GithubProfileResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +public class GithubTestController { + + @PostMapping("/github/login/oauth/access_token") + public ResponseEntity accessToken( + @RequestBody GithubAccessTokenRequest tokenRequest) { + String accessToken = GithubResponses.findByCode(tokenRequest.getCode()).getAccessToken(); + GithubAccessTokenResponse response = new GithubAccessTokenResponse(accessToken, "", "", ""); + return ResponseEntity.ok(response); + } + + @GetMapping("/github/user") + public ResponseEntity user( + @RequestHeader("Authorization") String authorization) { + String accessToken = authorization.split(" ")[1]; + GithubResponses githubResponse = GithubResponses.findByToken(accessToken); + GithubProfileResponse response = new GithubProfileResponse(githubResponse.getEmail(), githubResponse.getAge()); + return ResponseEntity.ok(response); + } +} +