From 5f497697f7a357af4ce11285c032ddd54f5866ef Mon Sep 17 00:00:00 2001 From: boorownie Date: Thu, 4 Jan 2024 23:47:59 +0900 Subject: [PATCH 1/4] 8th init - default sample --- src/main/java/nextstep/auth/AuthConfig.java | 22 +++ .../auth/AuthenticationException.java | 8 + .../principal/AuthenticationPrincipal.java | 11 ++ ...thenticationPrincipalArgumentResolver.java | 35 ++++ .../auth/principal/UserPrincipal.java | 13 ++ .../nextstep/auth/token/JwtTokenProvider.java | 43 +++++ .../nextstep/auth/token/TokenController.java | 30 ++++ .../nextstep/auth/token/TokenRequest.java | 22 +++ .../nextstep/auth/token/TokenResponse.java | 16 ++ .../nextstep/auth/token/TokenService.java | 52 ++++++ .../auth/token/oauth2/OAuth2User.java | 5 + .../auth/token/oauth2/OAuth2UserRequest.java | 7 + .../auth/token/oauth2/OAuth2UserService.java | 5 + .../github/GithubAccessTokenRequest.java | 29 ++++ .../github/GithubAccessTokenResponse.java | 43 +++++ .../token/oauth2/github/GithubClient.java | 64 ++++++++ .../oauth2/github/GithubProfileResponse.java | 31 ++++ .../oauth2/github/GithubTokenRequest.java | 16 ++ .../auth/userdetails/UserDetails.java | 7 + .../auth/userdetails/UserDetailsService.java | 5 + .../favorite/application/FavoriteService.java | 54 +++++++ .../application/dto/FavoriteRequest.java | 22 +++ .../application/dto/FavoriteResponse.java | 35 ++++ .../nextstep/favorite/domain/Favorite.java | 54 +++++++ .../favorite/domain/FavoriteRepository.java | 10 ++ .../favorite/ui/FavoriteController.java | 41 +++++ .../application/CustomOAuth2UserService.java | 26 +++ .../application/CustomUserDetailsService.java | 24 +++ .../member/application/MemberService.java | 49 ++++++ .../member/application/dto/MemberRequest.java | 40 +++++ .../application/dto/MemberResponse.java | 34 ++++ .../member/domain/CustomOAuth2User.java | 16 ++ .../member/domain/CustomUserDetails.java | 23 +++ .../java/nextstep/member/domain/Member.java | 50 ++++++ .../member/domain/MemberRepository.java | 11 ++ .../nextstep/member/ui/MemberController.java | 51 ++++++ .../subway/applicaion/LineService.java | 89 ++++++++++ .../subway/applicaion/PathService.java | 31 ++++ .../subway/applicaion/StationService.java | 49 ++++++ .../subway/applicaion/dto/LineRequest.java | 29 ++++ .../subway/applicaion/dto/LineResponse.java | 47 ++++++ .../subway/applicaion/dto/PathResponse.java | 33 ++++ .../subway/applicaion/dto/SectionRequest.java | 28 ++++ .../subway/applicaion/dto/StationRequest.java | 9 ++ .../applicaion/dto/StationResponse.java | 37 +++++ .../java/nextstep/subway/domain/Line.java | 61 +++++++ .../subway/domain/LineRepository.java | 10 ++ .../java/nextstep/subway/domain/Path.java | 23 +++ .../java/nextstep/subway/domain/Section.java | 70 ++++++++ .../nextstep/subway/domain/SectionEdge.java | 19 +++ .../java/nextstep/subway/domain/Sections.java | 153 ++++++++++++++++++ .../java/nextstep/subway/domain/Station.java | 29 ++++ .../subway/domain/StationRepository.java | 6 + .../nextstep/subway/domain/SubwayMap.java | 56 +++++++ .../subway/ui/ControllerExceptionHandler.java | 25 +++ .../nextstep/subway/ui/LineController.java | 63 ++++++++ .../nextstep/subway/ui/PathController.java | 22 +++ .../nextstep/subway/ui/StationController.java | 36 +++++ .../resources/application-test.properties | 10 ++ src/main/resources/application.properties | 10 ++ .../acceptance/FavoriteAcceptanceTest.java | 110 +++++++++++++ .../domain/FavoriteRepositoryTest.java | 62 +++++++ .../member/acceptance/AuthAcceptanceTest.java | 73 +++++++++ .../acceptance/MemberAcceptanceTest.java | 77 +++++++++ .../member/acceptance/MemberSteps.java | 67 ++++++++ .../subway/acceptance/FavoriteSteps.java | 24 +++ .../subway/acceptance/LineAcceptanceTest.java | 123 ++++++++++++++ .../nextstep/subway/acceptance/LineSteps.java | 87 ++++++++++ .../subway/acceptance/PathAcceptanceTest.java | 66 ++++++++ .../acceptance/SectionAcceptanceTest.java | 95 +++++++++++ .../acceptance/StationAcceptanceTest.java | 93 +++++++++++ .../subway/acceptance/StationSteps.java | 23 +++ .../java/nextstep/utils/AcceptanceTest.java | 48 ++++++ src/test/java/nextstep/utils/DataLoader.java | 25 +++ .../nextstep/utils/DataLoaderBootstrap.java | 19 +++ .../java/nextstep/utils/DatabaseCleanup.java | 40 +++++ .../java/nextstep/utils/GithubResponses.java | 54 +++++++ .../nextstep/utils/GithubTestController.java | 29 ++++ 78 files changed, 3064 insertions(+) create mode 100644 src/main/java/nextstep/auth/AuthConfig.java create mode 100644 src/main/java/nextstep/auth/AuthenticationException.java create mode 100644 src/main/java/nextstep/auth/principal/AuthenticationPrincipal.java create mode 100644 src/main/java/nextstep/auth/principal/AuthenticationPrincipalArgumentResolver.java create mode 100644 src/main/java/nextstep/auth/principal/UserPrincipal.java create mode 100644 src/main/java/nextstep/auth/token/JwtTokenProvider.java create mode 100644 src/main/java/nextstep/auth/token/TokenController.java create mode 100644 src/main/java/nextstep/auth/token/TokenRequest.java create mode 100644 src/main/java/nextstep/auth/token/TokenResponse.java create mode 100644 src/main/java/nextstep/auth/token/TokenService.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/OAuth2User.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/OAuth2UserRequest.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/OAuth2UserService.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenRequest.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/github/GithubAccessTokenResponse.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/github/GithubClient.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/github/GithubProfileResponse.java create mode 100644 src/main/java/nextstep/auth/token/oauth2/github/GithubTokenRequest.java create mode 100644 src/main/java/nextstep/auth/userdetails/UserDetails.java create mode 100644 src/main/java/nextstep/auth/userdetails/UserDetailsService.java create mode 100644 src/main/java/nextstep/favorite/application/FavoriteService.java create mode 100644 src/main/java/nextstep/favorite/application/dto/FavoriteRequest.java create mode 100644 src/main/java/nextstep/favorite/application/dto/FavoriteResponse.java create mode 100644 src/main/java/nextstep/favorite/domain/Favorite.java create mode 100644 src/main/java/nextstep/favorite/domain/FavoriteRepository.java create mode 100644 src/main/java/nextstep/favorite/ui/FavoriteController.java create mode 100644 src/main/java/nextstep/member/application/CustomOAuth2UserService.java create mode 100644 src/main/java/nextstep/member/application/CustomUserDetailsService.java create mode 100644 src/main/java/nextstep/member/application/MemberService.java create mode 100644 src/main/java/nextstep/member/application/dto/MemberRequest.java create mode 100644 src/main/java/nextstep/member/application/dto/MemberResponse.java create mode 100644 src/main/java/nextstep/member/domain/CustomOAuth2User.java create mode 100644 src/main/java/nextstep/member/domain/CustomUserDetails.java create mode 100644 src/main/java/nextstep/member/domain/Member.java create mode 100644 src/main/java/nextstep/member/domain/MemberRepository.java create mode 100644 src/main/java/nextstep/member/ui/MemberController.java create mode 100644 src/main/java/nextstep/subway/applicaion/LineService.java create mode 100644 src/main/java/nextstep/subway/applicaion/PathService.java create mode 100644 src/main/java/nextstep/subway/applicaion/StationService.java create mode 100644 src/main/java/nextstep/subway/applicaion/dto/LineRequest.java create mode 100644 src/main/java/nextstep/subway/applicaion/dto/LineResponse.java create mode 100644 src/main/java/nextstep/subway/applicaion/dto/PathResponse.java create mode 100644 src/main/java/nextstep/subway/applicaion/dto/SectionRequest.java create mode 100644 src/main/java/nextstep/subway/applicaion/dto/StationRequest.java create mode 100644 src/main/java/nextstep/subway/applicaion/dto/StationResponse.java create mode 100644 src/main/java/nextstep/subway/domain/Line.java create mode 100644 src/main/java/nextstep/subway/domain/LineRepository.java create mode 100644 src/main/java/nextstep/subway/domain/Path.java create mode 100644 src/main/java/nextstep/subway/domain/Section.java create mode 100644 src/main/java/nextstep/subway/domain/SectionEdge.java create mode 100644 src/main/java/nextstep/subway/domain/Sections.java create mode 100644 src/main/java/nextstep/subway/domain/Station.java create mode 100644 src/main/java/nextstep/subway/domain/StationRepository.java create mode 100644 src/main/java/nextstep/subway/domain/SubwayMap.java create mode 100644 src/main/java/nextstep/subway/ui/ControllerExceptionHandler.java create mode 100644 src/main/java/nextstep/subway/ui/LineController.java create mode 100644 src/main/java/nextstep/subway/ui/PathController.java create mode 100644 src/main/java/nextstep/subway/ui/StationController.java create mode 100644 src/main/resources/application-test.properties create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/nextstep/favorite/acceptance/FavoriteAcceptanceTest.java create mode 100644 src/test/java/nextstep/favorite/domain/FavoriteRepositoryTest.java create mode 100644 src/test/java/nextstep/member/acceptance/AuthAcceptanceTest.java create mode 100644 src/test/java/nextstep/member/acceptance/MemberAcceptanceTest.java create mode 100644 src/test/java/nextstep/member/acceptance/MemberSteps.java create mode 100644 src/test/java/nextstep/subway/acceptance/FavoriteSteps.java create mode 100644 src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java create mode 100644 src/test/java/nextstep/subway/acceptance/LineSteps.java create mode 100644 src/test/java/nextstep/subway/acceptance/PathAcceptanceTest.java create mode 100644 src/test/java/nextstep/subway/acceptance/SectionAcceptanceTest.java create mode 100644 src/test/java/nextstep/subway/acceptance/StationAcceptanceTest.java create mode 100644 src/test/java/nextstep/subway/acceptance/StationSteps.java create mode 100644 src/test/java/nextstep/utils/AcceptanceTest.java create mode 100644 src/test/java/nextstep/utils/DataLoader.java create mode 100644 src/test/java/nextstep/utils/DataLoaderBootstrap.java create mode 100644 src/test/java/nextstep/utils/DatabaseCleanup.java create mode 100644 src/test/java/nextstep/utils/GithubResponses.java create mode 100644 src/test/java/nextstep/utils/GithubTestController.java 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..f67f5451c --- /dev/null +++ b/src/main/java/nextstep/auth/principal/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,35 @@ +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); + + return new UserPrincipal(username); + } +} 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..fcc53abf4 --- /dev/null +++ b/src/main/java/nextstep/auth/principal/UserPrincipal.java @@ -0,0 +1,13 @@ +package nextstep.auth.principal; + +public class UserPrincipal { + private String username; + + public UserPrincipal(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } +} 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..5c4a5d46d --- /dev/null +++ b/src/main/java/nextstep/auth/token/JwtTokenProvider.java @@ -0,0 +1,43 @@ +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) { + 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) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + public String getPrincipal(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + 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..cc2f5cbf0 --- /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()); + + 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()); + + 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..fd3bd7b34 --- /dev/null +++ b/src/main/java/nextstep/auth/token/oauth2/OAuth2User.java @@ -0,0 +1,5 @@ +package nextstep.auth.token.oauth2; + +public interface OAuth2User { + String getUsername(); +} 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..dafed3fc8 --- /dev/null +++ b/src/main/java/nextstep/auth/userdetails/UserDetails.java @@ -0,0 +1,7 @@ +package nextstep.auth.userdetails; + +public interface UserDetails { + String getUsername(); + + String getPassword(); +} 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..bbc250587 --- /dev/null +++ b/src/main/java/nextstep/favorite/application/FavoriteService.java @@ -0,0 +1,54 @@ +package nextstep.favorite.application; + +import nextstep.auth.principal.UserPrincipal; +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.domain.Member; +import nextstep.subway.applicaion.StationService; +import nextstep.subway.domain.Station; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FavoriteService { + private FavoriteRepository favoriteRepository; + private MemberService memberService; + private StationService stationService; + + public FavoriteService(FavoriteRepository favoriteRepository, MemberService memberService, StationService stationService) { + this.favoriteRepository = favoriteRepository; + this.memberService = memberService; + this.stationService = stationService; + } + + public FavoriteResponse createFavorite(UserPrincipal userPrincipal, FavoriteRequest request) { + Member member = memberService.findMemberByEmail(userPrincipal.getUsername()); + Station sourceStation = stationService.findById(request.getSource()); + Station targetStation = stationService.findById(request.getTarget()); + + Favorite favorite = favoriteRepository.save(new Favorite(sourceStation, targetStation, member)); + return FavoriteResponse.of(favorite); + } + + public List findFavorites(UserPrincipal userPrincipal) { + Member member = memberService.findMemberByEmail(userPrincipal.getUsername()); + List favorites = favoriteRepository.findByMember(member); + return favorites.stream() + .map(FavoriteResponse::of) + .collect(Collectors.toList()); + } + + public void deleteFavorite(UserPrincipal userPrincipal, Long id) { + Member member = memberService.findMemberByEmail(userPrincipal.getUsername()); + Favorite favorite = favoriteRepository.findById(id).orElseThrow(RuntimeException::new); + if (!favorite.isCreatedBy(member)) { + throw new RuntimeException(); + } + favoriteRepository.deleteById(id); + } +} 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..42155547d --- /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) { + return new FavoriteResponse(favorite.getId(), StationResponse.of(favorite.getSourceStation()), StationResponse.of(favorite.getTargetStation())); + } + + 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..30de92551 --- /dev/null +++ b/src/main/java/nextstep/favorite/domain/Favorite.java @@ -0,0 +1,54 @@ +package nextstep.favorite.domain; + +import nextstep.member.domain.Member; +import nextstep.subway.domain.Station; + +import javax.persistence.*; + +@Entity +public class Favorite { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "sourceStationId") + private Station sourceStation; + + @ManyToOne + @JoinColumn(name = "targetStationId") + private Station targetStation; + + @ManyToOne + @JoinColumn(name = "memberId") + private Member member; + + public Favorite() { + } + + public Favorite(Station sourceStation, Station targetStation, Member member) { + this.sourceStation = sourceStation; + this.targetStation = targetStation; + this.member = member; + } + + public Long getId() { + return id; + } + + public Station getSourceStation() { + return sourceStation; + } + + public Station getTargetStation() { + return targetStation; + } + + public Member getMember() { + return member; + } + + public boolean isCreatedBy(Member member) { + return this.member.equals(member); + } +} 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..d54383f34 --- /dev/null +++ b/src/main/java/nextstep/favorite/domain/FavoriteRepository.java @@ -0,0 +1,10 @@ +package nextstep.favorite.domain; + +import nextstep.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FavoriteRepository extends JpaRepository { + List findByMember(Member member); +} 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..008d39cdb --- /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, request); + return ResponseEntity + .created(URI.create("/favorites/" + 1L)) + .build(); + } + + @GetMapping("/favorites") + public ResponseEntity> getFavorites(@AuthenticationPrincipal UserPrincipal userPrincipal) { + List favorites = favoriteService.findFavorites(userPrincipal); + return ResponseEntity.ok().body(favorites); + } + + @DeleteMapping("/favorites/{id}") + public ResponseEntity deleteFavorite(@AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable Long id) { + favoriteService.deleteFavorite(userPrincipal, 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..2a5cc65f4 --- /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()); + } +} \ 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..7eead770b --- /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()); + } +} 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..5717c71d3 --- /dev/null +++ b/src/main/java/nextstep/member/application/MemberService.java @@ -0,0 +1,49 @@ +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 Member findMemberByEmail(String email) { + return memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); + } + + public Member save(MemberRequest request) { + return memberRepository.save(request.toMember()); + } + + public MemberResponse findByMe(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..34c0e492b --- /dev/null +++ b/src/main/java/nextstep/member/application/dto/MemberRequest.java @@ -0,0 +1,40 @@ +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 MemberRequest(String email, Integer age) { + this.email = email; + this.age = age; + this.password = "default-password"; + } + + 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..97ae08a67 --- /dev/null +++ b/src/main/java/nextstep/member/domain/CustomOAuth2User.java @@ -0,0 +1,16 @@ +package nextstep.member.domain; + +import nextstep.auth.token.oauth2.OAuth2User; + +public class CustomOAuth2User implements OAuth2User { + private String email; + + public CustomOAuth2User(String email) { + this.email = email; + } + + @Override + public String getUsername() { + return email; + } +} 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..620eb64a6 --- /dev/null +++ b/src/main/java/nextstep/member/domain/CustomUserDetails.java @@ -0,0 +1,23 @@ +package nextstep.member.domain; + +import nextstep.auth.userdetails.UserDetails; + +public class CustomUserDetails implements UserDetails { + private String email; + private String password; + + public CustomUserDetails(String email, String password) { + this.email = email; + this.password = password; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } +} 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..886385fd6 --- /dev/null +++ b/src/main/java/nextstep/member/domain/Member.java @@ -0,0 +1,50 @@ +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; + + public Member() { + } + + public Member(String email, String password, Integer age) { + this.email = email; + this.password = password; + this.age = age; + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public Integer getAge() { + return age; + } + + 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/ui/MemberController.java b/src/main/java/nextstep/member/ui/MemberController.java new file mode 100644 index 000000000..0ff1826cd --- /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.findByMe(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..c875db19a --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/StationService.java @@ -0,0 +1,49 @@ +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.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); + } +} 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..153c35378 --- /dev/null +++ b/src/main/java/nextstep/subway/applicaion/dto/LineResponse.java @@ -0,0 +1,47 @@ +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() { + } + + 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..100dca5a7 --- /dev/null +++ b/src/main/java/nextstep/subway/ui/ControllerExceptionHandler.java @@ -0,0 +1,25 @@ +package nextstep.subway.ui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 { + private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class); + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleIllegalArgsException(DataIntegrityViolationException e) { + logger.error("DataIntegrityViolationException occurred: ", e); + return ResponseEntity.badRequest().build(); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgsException(IllegalArgumentException e) { + logger.error("IllegalArgumentException occurred: ", 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/main/resources/application-test.properties b/src/main/resources/application-test.properties new file mode 100644 index 000000000..c20b3bf91 --- /dev/null +++ b/src/main/resources/application-test.properties @@ -0,0 +1,10 @@ +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.properties.hibernate.format_sql=true + +security.jwt.token.secret-key= atdd-secret-key +security.jwt.token.expire-length= 3600000 + +github.client.id= test_id +github.client.secret= test_secret +github.url.access-token= http://localhost:8080/github/login/oauth/access_token +github.url.profile= http://localhost:8080/github/user \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..e300659a6 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,10 @@ +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.properties.hibernate.format_sql=true + +security.jwt.token.secret-key= atdd-secret-key +security.jwt.token.expire-length= 3600000 + +github.client.id= client_id +github.client.secret= client_secret +github.url.access-token= https://github.com/login/oauth/access_token +github.url.profile= https://api.github.com/user \ No newline at end of file diff --git a/src/test/java/nextstep/favorite/acceptance/FavoriteAcceptanceTest.java b/src/test/java/nextstep/favorite/acceptance/FavoriteAcceptanceTest.java new file mode 100644 index 000000000..7b2e99d52 --- /dev/null +++ b/src/test/java/nextstep/favorite/acceptance/FavoriteAcceptanceTest.java @@ -0,0 +1,110 @@ +package nextstep.favorite.acceptance; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.favorite.application.dto.FavoriteResponse; +import nextstep.utils.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 org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.Map; + +import static nextstep.subway.acceptance.FavoriteSteps.즐겨찾기_생성을_요청; +import static nextstep.subway.acceptance.LineSteps.*; +import static nextstep.subway.acceptance.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("즐겨찾기 관련 기능") +public class FavoriteAcceptanceTest 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)); + } + + @Test + @DisplayName("즐겨찾기를 추가한다.") + void addFavorite() { + // when + Map params = new HashMap<>(); + params.put("source", 교대역 + ""); + params.put("target", 양재역 + ""); + + ExtractableResponse response = RestAssured + .given().log().all() + .auth().oauth2(token) + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/favorites") + .then().log().all().extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + assertThat(response.header("Location")).isNotBlank(); + } + + @Test + @DisplayName("즐겨찾기를 조회한다.") + void getFavorites() { + // given + 즐겨찾기_생성을_요청(token, 교대역, 양재역); + + // when + ExtractableResponse response = RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/favorites") + .then().log().all().extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(response.jsonPath().getList(".", FavoriteResponse.class)).hasSize(1); + } + + @Test + @DisplayName("즐겨찾기를 삭제한다.") + void deleteFavorite() { + // given + ExtractableResponse response = 즐겨찾기_생성을_요청(token, 교대역, 양재역); + + // when + ExtractableResponse deleteResponse = RestAssured + .given().log().all() + .auth().oauth2(token) + .when().delete(response.header("Location")) + .then().log().all().extract(); + + // then + assertThat(deleteResponse.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/favorite/domain/FavoriteRepositoryTest.java b/src/test/java/nextstep/favorite/domain/FavoriteRepositoryTest.java new file mode 100644 index 000000000..6cc84ca2a --- /dev/null +++ b/src/test/java/nextstep/favorite/domain/FavoriteRepositoryTest.java @@ -0,0 +1,62 @@ +package nextstep.favorite.domain; + +import nextstep.member.domain.Member; +import nextstep.member.domain.MemberRepository; +import nextstep.subway.domain.Station; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DataJpaTest +class FavoriteRepositoryTest { + @Autowired + private TestEntityManager entityManager; + + @Autowired + private FavoriteRepository favoriteRepository; + @Autowired + private MemberRepository memberRepository; + + @Test + void save() { + // given + Station 강남역 = new Station("강남역"); + entityManager.persist(강남역); + Station 양재역 = new Station("양재역"); + entityManager.persist(양재역); + Member member = new Member("email@email.com", "password", 12); + Favorite favorite = new Favorite(강남역, 양재역, member); + + // when + Favorite savedFavorite = favoriteRepository.save(favorite); + + // then + assertNotNull(savedFavorite.getId()); + } + + @Test + void isCreatedBy() { + // given + Station 강남역 = new Station("강남역"); + entityManager.persist(강남역); + Station 양재역 = new Station("양재역"); + entityManager.persist(양재역); + Member 회원 = new Member("email", "password", 12); + entityManager.persist(회원); + Favorite savedFavorite = favoriteRepository.save(new Favorite(강남역, 양재역, 회원)); + + entityManager.clear(); + entityManager.flush(); + + // when + Favorite favorite = favoriteRepository.findById(savedFavorite.getId()).orElseThrow(RuntimeException::new); + Member member = memberRepository.findById(savedFavorite.getMember().getId()).orElseThrow(RuntimeException::new); + + // then + assertThat(favorite.isCreatedBy(member)).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/member/acceptance/AuthAcceptanceTest.java b/src/test/java/nextstep/member/acceptance/AuthAcceptanceTest.java new file mode 100644 index 000000000..5a660521d --- /dev/null +++ b/src/test/java/nextstep/member/acceptance/AuthAcceptanceTest.java @@ -0,0 +1,73 @@ +package nextstep.member.acceptance; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.utils.AcceptanceTest; +import nextstep.utils.GithubResponses; +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 org.assertj.core.api.Assertions.assertThat; + +class AuthAcceptanceTest extends AcceptanceTest { + public static final String EMAIL = "admin@email.com"; + public static final String PASSWORD = "password"; + public static final Integer AGE = 20; + + @DisplayName("Bearer Auth") + @Test + void bearerAuth() { + Map params = new HashMap<>(); + params.put("email", EMAIL); + params.put("password", PASSWORD); + + ExtractableResponse response = RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/login/token") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + + String accessToken = response.jsonPath().getString("accessToken"); + assertThat(accessToken).isNotBlank(); + + ExtractableResponse response2 = RestAssured.given().log().all() + .auth().oauth2(accessToken) + .when().get("/members/me") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + + assertThat(response2.jsonPath().getString("email")).isEqualTo(EMAIL); + } + + @DisplayName("Github Auth") + @Test + void githubAuth() { + Map params = new HashMap<>(); + params.put("code", GithubResponses.사용자1.getCode()); + + ExtractableResponse response = RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/login/github") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + + String accessToken = response.jsonPath().getString("accessToken"); + assertThat(accessToken).isNotBlank(); + + ExtractableResponse response2 = RestAssured.given().log().all() + .auth().oauth2(accessToken) + .when().get("/members/me") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + + assertThat(response2.jsonPath().getString("email")).isEqualTo(GithubResponses.사용자1.getEmail()); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/member/acceptance/MemberAcceptanceTest.java b/src/test/java/nextstep/member/acceptance/MemberAcceptanceTest.java new file mode 100644 index 000000000..3b3a6ad88 --- /dev/null +++ b/src/test/java/nextstep/member/acceptance/MemberAcceptanceTest.java @@ -0,0 +1,77 @@ +package nextstep.member.acceptance; + +import nextstep.utils.AcceptanceTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import static nextstep.member.acceptance.MemberSteps.*; +import static org.assertj.core.api.Assertions.assertThat; + +class MemberAcceptanceTest extends AcceptanceTest { + public static final String EMAIL = "email@email.com"; + public static final String PASSWORD = "password"; + public static final int AGE = 20; + + @DisplayName("회원가입을 한다.") + @Test + void createMember() { + // when + var response = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @DisplayName("회원 정보를 조회한다.") + @Test + void getMember() { + // given + var createResponse = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // when + var response = 회원_정보_조회_요청(createResponse); + + // then + 회원_정보_조회됨(response, EMAIL, AGE); + + } + + @DisplayName("회원 정보를 수정한다.") + @Test + void updateMember() { + // given + var createResponse = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // when + var response = 회원_정보_수정_요청(createResponse, "new" + EMAIL, "new" + PASSWORD, AGE); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @DisplayName("회원 정보를 삭제한다.") + @Test + void deleteMember() { + // given + var createResponse = 회원_생성_요청(EMAIL, PASSWORD, AGE); + + // when + var response = 회원_삭제_요청(createResponse); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + /** + * Given 회원 가입을 생성하고 + * And 로그인을 하고 + * When 토큰을 통해 내 정보를 조회하면 + * Then 내 정보를 조회할 수 있다 + */ + @DisplayName("내 정보를 조회한다.") + @Test + void getMyInfo() { + + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/member/acceptance/MemberSteps.java b/src/test/java/nextstep/member/acceptance/MemberSteps.java new file mode 100644 index 000000000..a210a13a6 --- /dev/null +++ b/src/test/java/nextstep/member/acceptance/MemberSteps.java @@ -0,0 +1,67 @@ +package nextstep.member.acceptance; + +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; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MemberSteps { + 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 회원_정보_수정_요청(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 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/FavoriteSteps.java b/src/test/java/nextstep/subway/acceptance/FavoriteSteps.java new file mode 100644 index 000000000..f50aa3e9d --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/FavoriteSteps.java @@ -0,0 +1,24 @@ +package nextstep.subway.acceptance; + +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 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(); + } +} diff --git a/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java new file mode 100644 index 000000000..57ee75c0f --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/LineAcceptanceTest.java @@ -0,0 +1,123 @@ +package nextstep.subway.acceptance; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.utils.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.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/LineSteps.java b/src/test/java/nextstep/subway/acceptance/LineSteps.java new file mode 100644 index 000000000..1b13e77a7 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/LineSteps.java @@ -0,0 +1,87 @@ +package nextstep.subway.acceptance; + +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 지하철_노선_목록_조회_요청() { + 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 지하철_노선에_지하철_구간_제거_요청(Long lineId, Long stationId) { + return RestAssured.given().log().all() + .when().delete("/lines/{lineId}/sections?stationId={stationId}", lineId, stationId) + .then().log().all().extract(); + } + + public static 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"); + } + + public static 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/PathAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/PathAcceptanceTest.java new file mode 100644 index 000000000..2bec9e6a9 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/PathAcceptanceTest.java @@ -0,0 +1,66 @@ +package nextstep.subway.acceptance; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.utils.AcceptanceTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import static nextstep.subway.acceptance.LineSteps.*; +import static nextstep.subway.acceptance.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 ExtractableResponse 두_역의_최단_거리_경로_조회를_요청(Long source, Long target) { + return RestAssured + .given().log().all() + .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/SectionAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/SectionAcceptanceTest.java new file mode 100644 index 000000000..61eb70d97 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/SectionAcceptanceTest.java @@ -0,0 +1,95 @@ +package nextstep.subway.acceptance; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.utils.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.LineSteps.*; +import static nextstep.subway.acceptance.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/StationAcceptanceTest.java b/src/test/java/nextstep/subway/acceptance/StationAcceptanceTest.java new file mode 100644 index 000000000..9d210d4cf --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/StationAcceptanceTest.java @@ -0,0 +1,93 @@ +package nextstep.subway.acceptance; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.applicaion.dto.StationResponse; +import nextstep.utils.AcceptanceTest; +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.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/StationSteps.java b/src/test/java/nextstep/subway/acceptance/StationSteps.java new file mode 100644 index 000000000..92078c1ac --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/StationSteps.java @@ -0,0 +1,23 @@ +package nextstep.subway.acceptance; + +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(); + } +} diff --git a/src/test/java/nextstep/utils/AcceptanceTest.java b/src/test/java/nextstep/utils/AcceptanceTest.java new file mode 100644 index 000000000..daee8fae2 --- /dev/null +++ b/src/test/java/nextstep/utils/AcceptanceTest.java @@ -0,0 +1,48 @@ +package nextstep.utils; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import java.util.HashMap; +import java.util.Map; + +@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"; + public static final Integer AGE = 20; + + @Autowired + private DatabaseCleanup databaseCleanup; + @Autowired + private DataLoader dataLoader; + + public String token; + + @BeforeEach + public void setUp() { + databaseCleanup.execute(); + dataLoader.loadData(); + + Map params = new HashMap<>(); + params.put("email", EMAIL); + params.put("password", PASSWORD); + + ExtractableResponse response = RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/login/token") + .then().log().all() + .statusCode(HttpStatus.OK.value()).extract(); + + token = response.jsonPath().getString("accessToken"); + } +} diff --git a/src/test/java/nextstep/utils/DataLoader.java b/src/test/java/nextstep/utils/DataLoader.java new file mode 100644 index 000000000..6a8cd751f --- /dev/null +++ b/src/test/java/nextstep/utils/DataLoader.java @@ -0,0 +1,25 @@ +package nextstep.utils; + +import nextstep.member.domain.Member; +import nextstep.member.domain.MemberRepository; +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)); + memberRepository.save(new Member("member@email.com", "password", 20)); + memberRepository.save(new Member(GithubResponses.사용자1.getEmail(), "password", 20)); + memberRepository.save(new Member(GithubResponses.사용자2.getEmail(), "password", 20)); + memberRepository.save(new Member(GithubResponses.사용자3.getEmail(), "password", 20)); + memberRepository.save(new Member(GithubResponses.사용자4.getEmail(), "password", 20)); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/utils/DataLoaderBootstrap.java b/src/test/java/nextstep/utils/DataLoaderBootstrap.java new file mode 100644 index 000000000..1585fee4f --- /dev/null +++ b/src/test/java/nextstep/utils/DataLoaderBootstrap.java @@ -0,0 +1,19 @@ +package nextstep.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/utils/DatabaseCleanup.java b/src/test/java/nextstep/utils/DatabaseCleanup.java new file mode 100644 index 000000000..8b0331049 --- /dev/null +++ b/src/test/java/nextstep/utils/DatabaseCleanup.java @@ -0,0 +1,40 @@ +package nextstep.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/utils/GithubResponses.java b/src/test/java/nextstep/utils/GithubResponses.java new file mode 100644 index 000000000..a4fade00c --- /dev/null +++ b/src/test/java/nextstep/utils/GithubResponses.java @@ -0,0 +1,54 @@ +package nextstep.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/utils/GithubTestController.java b/src/test/java/nextstep/utils/GithubTestController.java new file mode 100644 index 000000000..0d507da6a --- /dev/null +++ b/src/test/java/nextstep/utils/GithubTestController.java @@ -0,0 +1,29 @@ +package nextstep.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); + } +} + From 58b72104ebed678ccbb3e6cc13b8fbc2695d1c0a Mon Sep 17 00:00:00 2001 From: junwoochoi Date: Tue, 23 Jan 2024 20:08:59 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20path.feature=20=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=88=98=EC=A1=B0=EA=B1=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/features/path.feature | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/test/resources/features/path.feature diff --git a/src/test/resources/features/path.feature b/src/test/resources/features/path.feature new file mode 100644 index 000000000..972689cd2 --- /dev/null +++ b/src/test/resources/features/path.feature @@ -0,0 +1,10 @@ +Feature: 경로 조회 관련 기능 + Scenario: 두 역의 최단 거리 경로를 조회한다. + Given 지하철역들을 생성 요청하고 + | name | + | 교대역 | + | 강남역 | + | 양재역 | + | 남부터미널역 | + When "교대역"과 "양재역"의 경로를 조회하면 + Then 두 역의 최단 경로를 찾을 수 있다 \ No newline at end of file From 88a9e764143c35938596fb8f1ac0f9517cce9f9d Mon Sep 17 00:00:00 2001 From: junwoochoi Date: Tue, 23 Jan 2024 20:47:38 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20path=20steps=20def=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/cucumber/steps/PathStepDef.java | 69 +++++++++++++++++++ .../subway/acceptance/AcceptanceContext.java | 27 ++++++++ src/test/resources/features/path.feature | 20 ++++-- 3 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/test/java/nextstep/cucumber/steps/PathStepDef.java create mode 100644 src/test/java/nextstep/subway/acceptance/AcceptanceContext.java diff --git a/src/test/java/nextstep/cucumber/steps/PathStepDef.java b/src/test/java/nextstep/cucumber/steps/PathStepDef.java new file mode 100644 index 000000000..f617ed1d4 --- /dev/null +++ b/src/test/java/nextstep/cucumber/steps/PathStepDef.java @@ -0,0 +1,69 @@ +package nextstep.cucumber.steps; + +import io.cucumber.datatable.DataTable; +import io.cucumber.java8.En; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import nextstep.subway.acceptance.AcceptanceContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static nextstep.subway.acceptance.LineSteps.*; +import static nextstep.subway.acceptance.StationSteps.지하철역_생성_요청; +import static org.assertj.core.api.Assertions.assertThat; + +public class PathStepDef implements En { + @Autowired + private AcceptanceContext acceptanceContext; + private ExtractableResponse response; + public PathStepDef() { + Given("지하철역들을 생성 요청하고", (DataTable table) -> { + List> maps = table.asMaps(); + for (String name : maps.stream().map(it -> it.get("name")).collect(Collectors.toList())) { + Long id = 지하철역_생성_요청(name).jsonPath().getLong("id"); + acceptanceContext.put(name, id); + } + }); + Given("지하철 노선들을 생성 요청하고", (DataTable table) -> { + List> maps = table.asMaps(); + for (Map map : maps) { + String name = map.get("name"); + String color = map.get("color"); + String upStationName = map.get("upStationName"); + String downStationName = map.get("downStationName"); + String distance = map.get("distance"); + Long id = 지하철_노선_생성_요청(name, color, (Long) acceptanceContext.get(upStationName), (Long) acceptanceContext.get(downStationName), Integer.parseInt(distance)); + + acceptanceContext.put(name, id); + } + }); + Given("{string} 노선에 지하철 {string} {string} 구간을 거리 {int}으로 생성 요청하고", (String lineName, String upStationName, String downStationName, Integer distance) -> { + Long lineId = (Long) acceptanceContext.get(lineName); + Long upStationId = (Long) acceptanceContext.get(upStationName); + Long downStationId = (Long) acceptanceContext.get(downStationName); + 지하철_노선에_지하철_구간_생성_요청(lineId, createSectionCreateParams(upStationId, downStationId, distance)); + }); + When("{string}과 {string}의 경로를 조회하면", (String sourceName, String targetName) -> { + Long sourceId = (Long) acceptanceContext.get(sourceName); + Long targetId = (Long) acceptanceContext.get(targetName); + response = RestAssured + .given().log().all() + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/paths?source={sourceId}&target={targetId}", sourceId, targetId) + .then().log().all().extract(); + }); + + Then("두 역의 최단 경로를 찾을 수 있다", (DataTable table) -> { + List stationIds = table.asList().stream().map(name -> (Long) acceptanceContext.get(name)).collect(Collectors.toList()); + assertThat(response.jsonPath().getList("stations.id", Long.class)).containsExactlyElementsOf(stationIds); + }); + + } +} diff --git a/src/test/java/nextstep/subway/acceptance/AcceptanceContext.java b/src/test/java/nextstep/subway/acceptance/AcceptanceContext.java new file mode 100644 index 000000000..3fc9f73f3 --- /dev/null +++ b/src/test/java/nextstep/subway/acceptance/AcceptanceContext.java @@ -0,0 +1,27 @@ +package nextstep.subway.acceptance; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Profile("test") +@Component +public class AcceptanceContext { + private final Map store = new HashMap<>(); + + public void put(String key, Object value) { + this.store.put(key, value); + } + + public void clear(){ + this.store.clear(); + } + + public Object get(String key) { + return this.store.get(key); + } +} diff --git a/src/test/resources/features/path.feature b/src/test/resources/features/path.feature index 972689cd2..f3a978dd3 100644 --- a/src/test/resources/features/path.feature +++ b/src/test/resources/features/path.feature @@ -1,10 +1,20 @@ Feature: 경로 조회 관련 기능 + Scenario: 두 역의 최단 거리 경로를 조회한다. Given 지하철역들을 생성 요청하고 - | name | - | 교대역 | - | 강남역 | - | 양재역 | + | name | + | 교대역 | + | 강남역 | + | 양재역 | | 남부터미널역 | + Given 지하철 노선들을 생성 요청하고 + | name | color | upStationName | downStationName | distance | + | 이호선 | green | 교대역 | 강남역 | 10 | + | 신분당선 | red | 강남역 | 양재역 | 10 | + | 삼호선 | orange | 교대역 | 남부터미널역 | 2 | + Given "삼호선" 노선에 지하철 "남부터미널역" "양재역" 구간을 거리 3으로 생성 요청하고 When "교대역"과 "양재역"의 경로를 조회하면 - Then 두 역의 최단 경로를 찾을 수 있다 \ No newline at end of file + Then 두 역의 최단 경로를 찾을 수 있다 + | 교대역 | + | 남부터미널역 | + | 양재역 | \ No newline at end of file From 88cca8e6f6dfa75f294602d261c42c1f3f0dad45 Mon Sep 17 00:00:00 2001 From: junwoochoi Date: Tue, 23 Jan 2024 20:52:12 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20Before=20=ED=9B=85=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cucumber/steps/BeforeStepDef.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/test/java/nextstep/cucumber/steps/BeforeStepDef.java diff --git a/src/test/java/nextstep/cucumber/steps/BeforeStepDef.java b/src/test/java/nextstep/cucumber/steps/BeforeStepDef.java new file mode 100644 index 000000000..288bf84d0 --- /dev/null +++ b/src/test/java/nextstep/cucumber/steps/BeforeStepDef.java @@ -0,0 +1,21 @@ +package nextstep.cucumber.steps; + +import io.cucumber.java.BeforeStep; +import io.cucumber.java8.En; +import nextstep.utils.DataLoader; +import nextstep.utils.DatabaseCleanup; +import org.springframework.beans.factory.annotation.Autowired; + +public class BeforeStepDef implements En { + @Autowired + private DatabaseCleanup databaseCleanup; + @Autowired + private DataLoader dataLoader; + + public BeforeStepDef() { + Before(() -> { + databaseCleanup.execute(); + dataLoader.loadData(); + }); + } +} \ No newline at end of file