From 2c6c9d294cc51564de8128d71b2060b280199f14 Mon Sep 17 00:00:00 2001 From: sws6641 Date: Sun, 24 Sep 2023 21:46:45 +0900 Subject: [PATCH 1/2] =?UTF-8?q?10=EC=A3=BC=EC=9E=90=20=EA=B3=BC=EC=A0=9C?= =?UTF-8?q?=20=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/auth/LoginService.java | 47 +++++++ .../application/auth/LogoutService.java | 20 +++ .../application/auth/SignupService.java | 41 ++++++ .../cart/AddProductToCartService.java | 7 +- .../cart/ChangeCartItemQuantityService.java | 5 +- .../application/cart/GetCartService.java | 7 +- .../product/GetProductListService.java | 2 +- .../controllers/LineItemController.java | 30 +++-- .../controllers/ProductController.java | 6 +- .../controllers/SessionController.java | 54 ++++++++ .../controllers/UserController.java | 39 ++++++ .../controllers/WelcomeController.java | 25 ++++ .../dtos/auth/LoginRequestDto.java | 7 + .../dtos/auth/LoginResultDto.java | 9 ++ .../dtos/auth/SignupRequestDto.java | 10 ++ .../dtos/auth/SignupResultDto.java | 6 + .../dtos/{ => cart}/AddCartLineItemDto.java | 2 +- .../dtos/{ => cart}/CartDto.java | 2 +- .../{ => cart}/ChangeCartLineItemDto.java | 2 +- .../dtos/{ => product}/CreateProductDto.java | 2 +- .../dtos/{ => product}/ProductListDto.java | 2 +- .../UserAlreadyExistsException.java | 4 + .../infrastructure/CartDtoFetcher.java | 24 ++-- .../infrastructure/ProductDtoFetcher.java | 2 +- .../infrastructure/UserDetailsDao.java | 94 ++++++++++++++ .../backendsurvivalweek10/models/Cart.java | 10 ++ .../backendsurvivalweek10/models/CartId.java | 2 +- .../backendsurvivalweek10/models/UserId.java | 19 +++ .../repositories/CartRepository.java | 4 + .../AccessTokenAuthenticationFilter.java | 46 +++++++ .../security/AccessTokenService.java | 33 +++++ .../security/WebSecurityConfig.java | 40 +++++- .../util/AccessTokenGenerator.java | 40 ++++++ src/main/resources/application.yml | 2 +- .../cart/AddProductToCartServiceTest.java | 13 +- .../ChangeCartItemQuantityServiceTest.java | 10 +- .../application/cart/GetCartServiceTest.java | 4 +- .../controllers/ControllerTest.java | 64 ++++++++++ .../controllers/LineItemControllerTest.java | 55 ++++---- .../controllers/ProductControllerTest.java | 2 +- .../controllers/SessionControllerTest.java | 120 ++++++++++++++++++ .../controllers/UserControllerTest.java | 83 ++++++++++++ .../controllers/WelcomeControllerTest.java | 70 ++++++++++ web/package.json | 2 +- 44 files changed, 991 insertions(+), 77 deletions(-) create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LoginService.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LogoutService.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/SignupService.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/controllers/SessionController.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/controllers/UserController.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeController.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginRequestDto.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginResultDto.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupRequestDto.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupResultDto.java rename src/main/java/kr/megaptera/backendsurvivalweek10/dtos/{ => cart}/AddCartLineItemDto.java (59%) rename src/main/java/kr/megaptera/backendsurvivalweek10/dtos/{ => cart}/CartDto.java (88%) rename src/main/java/kr/megaptera/backendsurvivalweek10/dtos/{ => cart}/ChangeCartLineItemDto.java (53%) rename src/main/java/kr/megaptera/backendsurvivalweek10/dtos/{ => product}/CreateProductDto.java (55%) rename src/main/java/kr/megaptera/backendsurvivalweek10/dtos/{ => product}/ProductListDto.java (86%) create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/exceptions/UserAlreadyExistsException.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/UserDetailsDao.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/models/UserId.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenAuthenticationFilter.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenService.java create mode 100644 src/main/java/kr/megaptera/backendsurvivalweek10/util/AccessTokenGenerator.java create mode 100644 src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ControllerTest.java create mode 100644 src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java create mode 100644 src/test/java/kr/megaptera/backendsurvivalweek10/controllers/UserControllerTest.java create mode 100644 src/test/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeControllerTest.java diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LoginService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LoginService.java new file mode 100644 index 0000000..882312a --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LoginService.java @@ -0,0 +1,47 @@ +package kr.megaptera.backendsurvivalweek10.application.auth; + +import jakarta.transaction.Transactional; +import kr.megaptera.backendsurvivalweek10.dtos.auth.LoginResultDto; +import kr.megaptera.backendsurvivalweek10.infrastructure.UserDetailsDao; +import kr.megaptera.backendsurvivalweek10.util.AccessTokenGenerator; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.stream.Collectors; + +@Service +@Transactional +public class LoginService { + private final AccessTokenGenerator accessTokenGenerator; + + private final PasswordEncoder passwordEncoder; + + private final UserDetailsDao userDetailsDao; + + public LoginService(AccessTokenGenerator accessTokenGenerator, + PasswordEncoder passwordEncoder, + UserDetailsDao userDetailsDao) { + this.accessTokenGenerator = accessTokenGenerator; + this.passwordEncoder = passwordEncoder; + this.userDetailsDao = userDetailsDao; + } + + public LoginResultDto login(String username, String password) { + return userDetailsDao.findByUsername(username) + .filter(userDetails -> passwordEncoder.matches( + password, userDetails.getPassword())) + .map(userDetails -> { + String id = userDetails.getUsername(); + String accessToken = accessTokenGenerator.generate(id); + userDetailsDao.addAccessToken(id, accessToken); + return new LoginResultDto( + accessToken, + userDetails + .getAuthorities() + .stream().map(Object::toString) + .collect(Collectors.toList())); + }) + .orElseThrow(() -> new BadCredentialsException("Login failed")); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LogoutService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LogoutService.java new file mode 100644 index 0000000..0384bd6 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/LogoutService.java @@ -0,0 +1,20 @@ +package kr.megaptera.backendsurvivalweek10.application.auth; + +import jakarta.transaction.Transactional; +import kr.megaptera.backendsurvivalweek10.infrastructure.UserDetailsDao; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class LogoutService { + private final UserDetailsDao userDetailsDao; + + + public LogoutService(UserDetailsDao userDetailsDao) { + this.userDetailsDao = userDetailsDao; + } + + public void logout(String accessToken) { + userDetailsDao.removeAccessToken(accessToken); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/SignupService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/SignupService.java new file mode 100644 index 0000000..acb742c --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/auth/SignupService.java @@ -0,0 +1,41 @@ +package kr.megaptera.backendsurvivalweek10.application.auth; + +import io.hypersistence.tsid.TSID; +import jakarta.transaction.Transactional; +import kr.megaptera.backendsurvivalweek10.dtos.auth.SignupResultDto; +import kr.megaptera.backendsurvivalweek10.exceptions.UserAlreadyExistsException; +import kr.megaptera.backendsurvivalweek10.infrastructure.UserDetailsDao; +import kr.megaptera.backendsurvivalweek10.util.AccessTokenGenerator; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class SignupService { + private final AccessTokenGenerator accessTokenGenerator; + private final PasswordEncoder passwordEncoder; + private final UserDetailsDao userDetailsDao; + + public SignupService(AccessTokenGenerator accessTokenGenerator, + PasswordEncoder passwordEncoder, + UserDetailsDao userDetailsDao) { + this.accessTokenGenerator = accessTokenGenerator; + this.passwordEncoder = passwordEncoder; + this.userDetailsDao = userDetailsDao; + } + + public SignupResultDto signup(String username, String password) { + if (userDetailsDao.existsByUsername(username)) { + throw new UserAlreadyExistsException(); + } + + String id = TSID.Factory.getTsid().toString(); + String encodedPassword = passwordEncoder.encode(password); + String accessToken = accessTokenGenerator.generate(id); + + userDetailsDao.addUser(id, username, encodedPassword); + userDetailsDao.addAccessToken(id, accessToken); + + return new SignupResultDto(accessToken); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartService.java index d65824a..4b2c44f 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartService.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartService.java @@ -4,6 +4,7 @@ import kr.megaptera.backendsurvivalweek10.models.CartId; import kr.megaptera.backendsurvivalweek10.models.Product; import kr.megaptera.backendsurvivalweek10.models.ProductId; +import kr.megaptera.backendsurvivalweek10.models.UserId; import kr.megaptera.backendsurvivalweek10.repositories.CartRepository; import kr.megaptera.backendsurvivalweek10.repositories.ProductRepository; import org.springframework.stereotype.Service; @@ -21,12 +22,12 @@ public AddProductToCartService(CartRepository cartRepository, this.productRepository = productRepository; } - public Cart addProduct(ProductId productId, int quantity) { + public Cart addProduct(String userId, ProductId productId, int quantity) { Product product = productRepository.findById(productId) .orElseThrow(); - Cart cart = cartRepository.findById(CartId.DEFAULT) - .orElse(new Cart(CartId.DEFAULT)); + Cart cart = cartRepository.findByUserId(UserId.of(userId)) + .orElse(new Cart(CartId.generate(), UserId.of(userId))); cart.addProduct(product, quantity); diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityService.java index 6f0fc6d..27f2022 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityService.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityService.java @@ -3,6 +3,7 @@ import kr.megaptera.backendsurvivalweek10.models.Cart; import kr.megaptera.backendsurvivalweek10.models.CartId; import kr.megaptera.backendsurvivalweek10.models.LineItemId; +import kr.megaptera.backendsurvivalweek10.models.UserId; import kr.megaptera.backendsurvivalweek10.repositories.CartRepository; import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; @@ -16,8 +17,8 @@ public ChangeCartItemQuantityService(CartRepository cartRepository) { this.cartRepository = cartRepository; } - public void changeQuantity(LineItemId lineItemId, int quantity) { - Cart cart = cartRepository.findById(CartId.DEFAULT).get(); + public void changeQuantity(String userId, LineItemId lineItemId, int quantity) { + Cart cart = cartRepository.findByUserId(UserId.of(userId)).get(); cart.changeLineItemQuantity(lineItemId, quantity); } diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartService.java index c9093f8..879115c 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartService.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartService.java @@ -1,8 +1,9 @@ package kr.megaptera.backendsurvivalweek10.application.cart; -import kr.megaptera.backendsurvivalweek10.dtos.CartDto; +import kr.megaptera.backendsurvivalweek10.dtos.cart.CartDto; import kr.megaptera.backendsurvivalweek10.infrastructure.CartDtoFetcher; import kr.megaptera.backendsurvivalweek10.models.CartId; +import kr.megaptera.backendsurvivalweek10.models.UserId; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,7 +16,7 @@ public GetCartService(CartDtoFetcher cartDtoFetcher) { this.cartDtoFetcher = cartDtoFetcher; } - public CartDto getCartDto() { - return cartDtoFetcher.fetchCartDto(CartId.DEFAULT); + public CartDto getCartDto(String userId) { + return cartDtoFetcher.fetchCartDto(UserId.of(userId)); } } diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/application/product/GetProductListService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/application/product/GetProductListService.java index e2dba96..c29db14 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/application/product/GetProductListService.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/application/product/GetProductListService.java @@ -1,6 +1,6 @@ package kr.megaptera.backendsurvivalweek10.application.product; -import kr.megaptera.backendsurvivalweek10.dtos.ProductListDto; +import kr.megaptera.backendsurvivalweek10.dtos.product.ProductListDto; import kr.megaptera.backendsurvivalweek10.infrastructure.ProductDtoFetcher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemController.java b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemController.java index 2c15435..232e16b 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemController.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemController.java @@ -3,14 +3,16 @@ import kr.megaptera.backendsurvivalweek10.application.cart.AddProductToCartService; import kr.megaptera.backendsurvivalweek10.application.cart.ChangeCartItemQuantityService; import kr.megaptera.backendsurvivalweek10.application.cart.GetCartService; -import kr.megaptera.backendsurvivalweek10.dtos.AddCartLineItemDto; -import kr.megaptera.backendsurvivalweek10.dtos.CartDto; -import kr.megaptera.backendsurvivalweek10.dtos.ChangeCartLineItemDto; +import kr.megaptera.backendsurvivalweek10.dtos.cart.AddCartLineItemDto; +import kr.megaptera.backendsurvivalweek10.dtos.cart.CartDto; +import kr.megaptera.backendsurvivalweek10.dtos.cart.ChangeCartLineItemDto; import kr.megaptera.backendsurvivalweek10.models.LineItemId; import kr.megaptera.backendsurvivalweek10.models.ProductId; import org.springframework.http.HttpStatus; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.NoSuchElementException; @RestController @@ -30,28 +32,40 @@ public LineItemController( } @GetMapping - public CartDto list() { - return getCartService.getCartDto(); + @Secured({"ROLE_USER", "ROLE_ADMIN"}) + public CartDto list(Principal principal) { + String userId = principal.getName(); + + return getCartService.getCartDto(userId); } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public void create(@RequestBody AddCartLineItemDto dto) { + @Secured({"ROLE_USER", "ROLE_ADMIN"}) + public void create( + Principal principal, + @RequestBody AddCartLineItemDto dto) { + String userId = principal.getName(); + ProductId productId = new ProductId(dto.productId()); int quantity = dto.quantity(); - addProductToCartService.addProduct(productId, quantity); + addProductToCartService.addProduct(userId, productId, quantity); } @PatchMapping("{id}") @ResponseStatus(HttpStatus.NO_CONTENT) + @Secured({"ROLE_USER", "ROLE_ADMIN"}) public void update( + Principal principal, @PathVariable("id") String id, @RequestBody ChangeCartLineItemDto dto) { + String userId = principal.getName(); + LineItemId lineItemId = new LineItemId(id); int quantity = dto.quantity(); - changeCartItemQuantityService.changeQuantity(lineItemId, quantity); + changeCartItemQuantityService.changeQuantity(userId, lineItemId, quantity); } @ExceptionHandler(NoSuchElementException.class) diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/ProductController.java b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/ProductController.java index 63c1e20..3228df6 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/ProductController.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/ProductController.java @@ -2,10 +2,11 @@ import kr.megaptera.backendsurvivalweek10.application.product.CreateProductService; import kr.megaptera.backendsurvivalweek10.application.product.GetProductListService; -import kr.megaptera.backendsurvivalweek10.dtos.CreateProductDto; -import kr.megaptera.backendsurvivalweek10.dtos.ProductListDto; +import kr.megaptera.backendsurvivalweek10.dtos.product.CreateProductDto; +import kr.megaptera.backendsurvivalweek10.dtos.product.ProductListDto; import kr.megaptera.backendsurvivalweek10.models.Money; import org.springframework.http.HttpStatus; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; @RestController @@ -27,6 +28,7 @@ public ProductListDto list() { @PostMapping @ResponseStatus(HttpStatus.CREATED) + @Secured("ROLE_ADMIN") public void create(@RequestBody CreateProductDto dto) { String name = dto.name().strip(); Money price = new Money(dto.price()); diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/SessionController.java b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/SessionController.java new file mode 100644 index 0000000..fd80eb0 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/SessionController.java @@ -0,0 +1,54 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import jakarta.validation.Valid; +import kr.megaptera.backendsurvivalweek10.application.auth.LoginService; +import kr.megaptera.backendsurvivalweek10.application.auth.LogoutService; +import kr.megaptera.backendsurvivalweek10.dtos.auth.LoginRequestDto; +import kr.megaptera.backendsurvivalweek10.dtos.auth.LoginResultDto; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/session") +public class SessionController { + + private final LoginService loginService; + private final LogoutService logoutService; + + public SessionController(LoginService loginService, LogoutService logoutService) { + this.loginService = loginService; + this.logoutService = logoutService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public LoginResultDto login( + @Valid @RequestBody LoginRequestDto dto) { + return loginService.login(dto.username(), dto.password()); + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public String logout(Authentication authentication) { + String accessToken = authentication.getCredentials().toString(); + + logoutService.logout(accessToken); + + return "Logout"; + } + + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String loginFailed() { + return "Bad Request"; + } + +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/UserController.java b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/UserController.java new file mode 100644 index 0000000..9cb6b31 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/UserController.java @@ -0,0 +1,39 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import jakarta.validation.Valid; +import kr.megaptera.backendsurvivalweek10.application.auth.SignupService; +import kr.megaptera.backendsurvivalweek10.dtos.auth.SignupRequestDto; +import kr.megaptera.backendsurvivalweek10.dtos.auth.SignupResultDto; +import kr.megaptera.backendsurvivalweek10.exceptions.UserAlreadyExistsException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +public class UserController { + private final SignupService signupService; + + public UserController(SignupService signupService) { + this.signupService = signupService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public SignupResultDto signup( + @Valid @RequestBody SignupRequestDto signupRequestDto) { + return signupService.signup( + signupRequestDto.username().trim(), + signupRequestDto.password().trim()); + } + + @ExceptionHandler(UserAlreadyExistsException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public String alreadyExists() { + return "User already exists."; + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeController.java b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeController.java new file mode 100644 index 0000000..f9f3df9 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeController.java @@ -0,0 +1,25 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WelcomeController { + @GetMapping("/") + public String home() { + return "Hello, world!"; + } + + @GetMapping("/admin") + @Secured("ROLE_ADMIN") + public String admin() { + return "Hello, world!"; + } + + @GetMapping("/user") + @Secured({"ROLE_USER", "ROLE_ADMIN"}) + public String user() { + return "Hello, world!"; + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginRequestDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginRequestDto.java new file mode 100644 index 0000000..4287eaf --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginRequestDto.java @@ -0,0 +1,7 @@ +package kr.megaptera.backendsurvivalweek10.dtos.auth; + +public record LoginRequestDto( + String username, + String password +) { +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginResultDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginResultDto.java new file mode 100644 index 0000000..ea890a9 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/LoginResultDto.java @@ -0,0 +1,9 @@ +package kr.megaptera.backendsurvivalweek10.dtos.auth; + +import java.util.List; + +public record LoginResultDto( + String accessToken, + List roles +) { +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupRequestDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupRequestDto.java new file mode 100644 index 0000000..f97cc5b --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupRequestDto.java @@ -0,0 +1,10 @@ +package kr.megaptera.backendsurvivalweek10.dtos.auth; + +import jakarta.validation.constraints.NotBlank; + +public record SignupRequestDto( + @NotBlank + String username, + String password +) { +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupResultDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupResultDto.java new file mode 100644 index 0000000..3ae4d91 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/auth/SignupResultDto.java @@ -0,0 +1,6 @@ +package kr.megaptera.backendsurvivalweek10.dtos.auth; + +public record SignupResultDto( + String accessToken +) { +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/AddCartLineItemDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/AddCartLineItemDto.java similarity index 59% rename from src/main/java/kr/megaptera/backendsurvivalweek10/dtos/AddCartLineItemDto.java rename to src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/AddCartLineItemDto.java index ca192c1..2994fc2 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/AddCartLineItemDto.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/AddCartLineItemDto.java @@ -1,4 +1,4 @@ -package kr.megaptera.backendsurvivalweek10.dtos; +package kr.megaptera.backendsurvivalweek10.dtos.cart; public record AddCartLineItemDto( String productId, diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/CartDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/CartDto.java similarity index 88% rename from src/main/java/kr/megaptera/backendsurvivalweek10/dtos/CartDto.java rename to src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/CartDto.java index cae94db..9dd5a27 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/CartDto.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/CartDto.java @@ -1,4 +1,4 @@ -package kr.megaptera.backendsurvivalweek10.dtos; +package kr.megaptera.backendsurvivalweek10.dtos.cart; import java.util.List; diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/ChangeCartLineItemDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/ChangeCartLineItemDto.java similarity index 53% rename from src/main/java/kr/megaptera/backendsurvivalweek10/dtos/ChangeCartLineItemDto.java rename to src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/ChangeCartLineItemDto.java index c8f2898..200c2c2 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/ChangeCartLineItemDto.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/cart/ChangeCartLineItemDto.java @@ -1,4 +1,4 @@ -package kr.megaptera.backendsurvivalweek10.dtos; +package kr.megaptera.backendsurvivalweek10.dtos.cart; public record ChangeCartLineItemDto( int quantity diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/CreateProductDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/product/CreateProductDto.java similarity index 55% rename from src/main/java/kr/megaptera/backendsurvivalweek10/dtos/CreateProductDto.java rename to src/main/java/kr/megaptera/backendsurvivalweek10/dtos/product/CreateProductDto.java index d936ef2..73c9b2d 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/CreateProductDto.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/product/CreateProductDto.java @@ -1,4 +1,4 @@ -package kr.megaptera.backendsurvivalweek10.dtos; +package kr.megaptera.backendsurvivalweek10.dtos.product; public record CreateProductDto( String name, diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/ProductListDto.java b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/product/ProductListDto.java similarity index 86% rename from src/main/java/kr/megaptera/backendsurvivalweek10/dtos/ProductListDto.java rename to src/main/java/kr/megaptera/backendsurvivalweek10/dtos/product/ProductListDto.java index 7af7c4a..e6b319f 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/ProductListDto.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/dtos/product/ProductListDto.java @@ -1,4 +1,4 @@ -package kr.megaptera.backendsurvivalweek10.dtos; +package kr.megaptera.backendsurvivalweek10.dtos.product; import java.util.List; diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/exceptions/UserAlreadyExistsException.java b/src/main/java/kr/megaptera/backendsurvivalweek10/exceptions/UserAlreadyExistsException.java new file mode 100644 index 0000000..589ee9a --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/exceptions/UserAlreadyExistsException.java @@ -0,0 +1,4 @@ +package kr.megaptera.backendsurvivalweek10.exceptions; + +public class UserAlreadyExistsException extends RuntimeException{ +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/CartDtoFetcher.java b/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/CartDtoFetcher.java index 5c861cc..dec53ee 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/CartDtoFetcher.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/CartDtoFetcher.java @@ -1,7 +1,8 @@ package kr.megaptera.backendsurvivalweek10.infrastructure; -import kr.megaptera.backendsurvivalweek10.dtos.CartDto; +import kr.megaptera.backendsurvivalweek10.dtos.cart.CartDto; import kr.megaptera.backendsurvivalweek10.models.CartId; +import kr.megaptera.backendsurvivalweek10.models.UserId; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @@ -16,16 +17,17 @@ public CartDtoFetcher(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public CartDto fetchCartDto(CartId cartId) { + public CartDto fetchCartDto(UserId userId) { String sql = """ - SELECT - *, - products.name AS product_name - FROM line_items - JOIN products ON line_items.product_id = products.id - WHERE line_items.cart_id = ? - ORDER BY line_items.id - """; + SELECT + *, + products.name AS product_name + FROM line_items + JOIN products ON line_items.product_id = products.id + JOIN carts ON line_items.cart_id = carts.id + WHERE carts.user_id = ? + ORDER BY line_items.id + """; List lineItemDtos = jdbcTemplate.query( sql, @@ -36,7 +38,7 @@ public CartDto fetchCartDto(CartId cartId) { resultSet.getInt("quantity"), resultSet.getLong("total_price") ), - cartId.toString() + userId.toString() ); return new CartDto(lineItemDtos); diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/ProductDtoFetcher.java b/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/ProductDtoFetcher.java index b9a15ba..f53c61d 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/ProductDtoFetcher.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/ProductDtoFetcher.java @@ -1,6 +1,6 @@ package kr.megaptera.backendsurvivalweek10.infrastructure; -import kr.megaptera.backendsurvivalweek10.dtos.ProductListDto; +import kr.megaptera.backendsurvivalweek10.dtos.product.ProductListDto; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/UserDetailsDao.java b/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/UserDetailsDao.java new file mode 100644 index 0000000..4610946 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/infrastructure/UserDetailsDao.java @@ -0,0 +1,94 @@ +package kr.megaptera.backendsurvivalweek10.infrastructure; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.sql.ResultSet; +import java.util.Optional; +import java.util.stream.DoubleStream; + +@Component +public class UserDetailsDao { + private final JdbcTemplate jdbcTemplate; + + public UserDetailsDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + + public Optional findByUsername(String username) { + String query = "SELECT id, password, role FROM users WHERE username=?"; + + return jdbcTemplate.query(query, resultSet -> { + if(!resultSet.next()){ + return Optional.empty(); + } + + String id = resultSet.getString("id"); + String password = resultSet.getString("password"); + String role = resultSet.getString("role"); + + UserDetails userDetails = User.withUsername(id) + .password(password) + .authorities(role) + .build(); + + return Optional.of(userDetails); + }, username); + } + + public void addAccessToken(String userId, String accessToken) { + jdbcTemplate.update(""" + INSERT INTO access_tokens (token, user_id) + VALUES (?, ?) + """, + accessToken, userId + ); + } + + public Optional findByAccessToken(String accessToken) { + String query = """ + SELECT users.id, users.role + FROM users + JOIN access_tokens ON access_tokens.user_id=users.id + WHERE access_tokens.token=? + """; + + return jdbcTemplate.query(query, resultSet -> { + if (!resultSet.next()) { + return Optional.empty(); + } + + String id = resultSet.getString("id"); + String role = resultSet.getString("role"); + + UserDetails userDetails = User.withUsername(id) + .password(accessToken) // ← Access Token을 Controller까지 전달하는 용도. + .authorities(role) + .build(); + + return Optional.of(userDetails); + }, accessToken); + } + + public boolean existsByUsername(String username) { + String query = "SELECT id FROM users WHERE username=?"; + return Boolean.TRUE.equals(jdbcTemplate.query(query, ResultSet::next, username)); + } + + public void addUser(String id, String username, String encodedPassword) { + jdbcTemplate.update(""" + INSERT INTO users (id, username, password, role) + VALUES (?, ?, ?, ?) + """, id, username, encodedPassword, "ROLE_USER"); + + } + + public void removeAccessToken(String accessToken) { + jdbcTemplate.update(""" + DELETE FROM access_tokens WHERE token=? + """, accessToken); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/models/Cart.java b/src/main/java/kr/megaptera/backendsurvivalweek10/models/Cart.java index 0926b77..eeccd3b 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/models/Cart.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/models/Cart.java @@ -15,6 +15,10 @@ public class Cart { @EmbeddedId private CartId cartId; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "user_id")) + private UserId userId; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "cart_id") @OrderBy("id") @@ -33,6 +37,12 @@ public Cart(CartId cartId) { this.cartId = cartId; } + public Cart(CartId cartId, UserId userId) { + this.cartId = cartId; + this.userId = userId; + } + + public Cart(CartId cartId, List lineItems) { this.cartId = cartId; this.lineItems = lineItems; diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/models/CartId.java b/src/main/java/kr/megaptera/backendsurvivalweek10/models/CartId.java index 87c4ea5..15d9794 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/models/CartId.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/models/CartId.java @@ -5,7 +5,7 @@ @Embeddable public class CartId extends EntityId { // TODO: 계정 별로 카트를 생성할 수 있도록 수정해 주세요. - public static final CartId DEFAULT = new CartId("0BV000000CART"); +// public static final CartId DEFAULT = new CartId("0BV000000CART"); private CartId() { super(); diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/models/UserId.java b/src/main/java/kr/megaptera/backendsurvivalweek10/models/UserId.java new file mode 100644 index 0000000..e975172 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/models/UserId.java @@ -0,0 +1,19 @@ +package kr.megaptera.backendsurvivalweek10.models; + +public class UserId extends EntityId { + private UserId() { + super(); + } + + public static UserId of(String value) { + return new UserId(value); + } + + public UserId(String value) { + super(value); + } + + public static UserId generate() { + return new UserId(newTsid()); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/repositories/CartRepository.java b/src/main/java/kr/megaptera/backendsurvivalweek10/repositories/CartRepository.java index 2d60f7f..d167cb0 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/repositories/CartRepository.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/repositories/CartRepository.java @@ -2,7 +2,11 @@ import kr.megaptera.backendsurvivalweek10.models.Cart; import kr.megaptera.backendsurvivalweek10.models.CartId; +import kr.megaptera.backendsurvivalweek10.models.UserId; import org.springframework.data.repository.CrudRepository; +import java.util.Optional; + public interface CartRepository extends CrudRepository { + Optional findByUserId(UserId userId); } diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenAuthenticationFilter.java b/src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenAuthenticationFilter.java new file mode 100644 index 0000000..fb57447 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenAuthenticationFilter.java @@ -0,0 +1,46 @@ +package kr.megaptera.backendsurvivalweek10.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Optional; + +@Component +public class AccessTokenAuthenticationFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION_PREFIX = "Bearer "; + private final AccessTokenService accessTokenService; + + public AccessTokenAuthenticationFilter(AccessTokenService accessTokenService) { + this.accessTokenService = accessTokenService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + String accessToken = parseAccessToken(request); + + Authentication authentication = accessTokenService.authenticate(accessToken); + + SecurityContextHolder.getContext() + .setAuthentication(authentication); + + + filterChain.doFilter(request, response); + } + + private String parseAccessToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader("Authorization")) + .filter(i -> i.startsWith(AUTHORIZATION_PREFIX)) + .map(i -> i.substring(AUTHORIZATION_PREFIX.length())) + .orElse(""); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenService.java b/src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenService.java new file mode 100644 index 0000000..049fc14 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/security/AccessTokenService.java @@ -0,0 +1,33 @@ +package kr.megaptera.backendsurvivalweek10.security; + +import kr.megaptera.backendsurvivalweek10.infrastructure.UserDetailsDao; +import kr.megaptera.backendsurvivalweek10.util.AccessTokenGenerator; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@Service +public class AccessTokenService { + private final AccessTokenGenerator accessTokenGenerator; + private final UserDetailsDao userDetailsDao; + + public AccessTokenService(AccessTokenGenerator accessTokenGenerator, + UserDetailsDao userDetailsDao) { + this.accessTokenGenerator = accessTokenGenerator; + this.userDetailsDao = userDetailsDao; + } + + public Authentication authenticate(String accessToken) { + if (!accessTokenGenerator.verify(accessToken)) { + return null; + } + + return userDetailsDao.findByAccessToken(accessToken) + .map(userDetails -> + UsernamePasswordAuthenticationToken.authenticated( + userDetails.getUsername(), + userDetails.getPassword(), + userDetails.getAuthorities())) + .orElse(null); + } +} diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/security/WebSecurityConfig.java b/src/main/java/kr/megaptera/backendsurvivalweek10/security/WebSecurityConfig.java index d91b885..9e98218 100644 --- a/src/main/java/kr/megaptera/backendsurvivalweek10/security/WebSecurityConfig.java +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/security/WebSecurityConfig.java @@ -2,13 +2,51 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) public class WebSecurityConfig { - // CORS 문제가 발생하기 때문에 아래와 같이 세팅을 해줘야 합니다. + private final AccessTokenAuthenticationFilter authenticationFilter; + + public WebSecurityConfig(AccessTokenAuthenticationFilter authenticationFilter) { + this.authenticationFilter = authenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) + throws Exception { + http.csrf().disable(); + + http.addFilterBefore( + authenticationFilter, BasicAuthenticationFilter.class); + + http.authorizeHttpRequests() + .requestMatchers(HttpMethod.POST, "/session").permitAll() + .requestMatchers(HttpMethod.POST, "/users").permitAll() + .requestMatchers(HttpMethod.GET, "/products").permitAll() + .anyRequest().authenticated() + .and().cors(); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + } + @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); diff --git a/src/main/java/kr/megaptera/backendsurvivalweek10/util/AccessTokenGenerator.java b/src/main/java/kr/megaptera/backendsurvivalweek10/util/AccessTokenGenerator.java new file mode 100644 index 0000000..3677275 --- /dev/null +++ b/src/main/java/kr/megaptera/backendsurvivalweek10/util/AccessTokenGenerator.java @@ -0,0 +1,40 @@ +package kr.megaptera.backendsurvivalweek10.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +@Component +public class AccessTokenGenerator { + private final Algorithm algorithm; + + public AccessTokenGenerator( + @Value("${jwt.secret}") + String secret) { + this.algorithm = Algorithm.HMAC256(secret); + } + + public String generate(String userId){ + return JWT.create() + .withClaim("userId", userId) + .withExpiresAt(Instant.now().plus(24, ChronoUnit.HOURS)) + .sign(algorithm); + } + + public boolean verify(String accessToken) { + try { + JWTVerifier verifier = JWT.require(algorithm).build(); + verifier.verify(accessToken); + + return true; + } catch (JWTVerificationException e) { + return false; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b285df8..70c54f8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:postgresql://localhost:5432/postgres + url: jdbc:postgresql://localhost:5432/assignment username: postgres password: password jpa: diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartServiceTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartServiceTest.java index 29bc494..d5c0ef5 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartServiceTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/AddProductToCartServiceTest.java @@ -37,14 +37,15 @@ void setUp() { @Test @DisplayName("addProduct - when cart doesn't exist") void cartNotExists() { + String userId = "USER_ID"; Product product = Fixtures.product(); ProductId productId = product.id(); - given(cartRepository.findById(any())).willReturn(Optional.empty()); + given(cartRepository.findByUserId(any())).willReturn(Optional.empty()); given(productRepository.findById(productId)) .willReturn(Optional.of(product)); - Cart cart = addProductToCartService.addProduct(productId, 1); + Cart cart = addProductToCartService.addProduct(userId, productId, 1); assertThat(cart.lineItemsSize()).isEqualTo(1); @@ -54,16 +55,17 @@ void cartNotExists() { @Test @DisplayName("addProduct - when cart exists") void cartExists() { + String userId = "USER_ID"; Cart cart = Fixtures.cart(); Product product = Fixtures.product(); ProductId productId = product.id(); - given(cartRepository.findById(any())).willReturn(Optional.of(cart)); + given(cartRepository.findByUserId(any())).willReturn(Optional.of(cart)); given(productRepository.findById(productId)) .willReturn(Optional.of(product)); - addProductToCartService.addProduct(productId, 1); + addProductToCartService.addProduct(userId, productId, 1); assertThat(cart.lineItemsSize()).isEqualTo(1); } @@ -71,13 +73,14 @@ void cartExists() { @Test @DisplayName("addProduct - when product doesn't exist") void productNotExists() { + String userId = "USER_ID"; ProductId productId = new ProductId("test-product-id"); given(productRepository.findById(productId)) .willReturn(Optional.empty()); assertThatThrownBy(() -> { - addProductToCartService.addProduct(productId, 1); + addProductToCartService.addProduct(userId, productId, 1); }); } } diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityServiceTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityServiceTest.java index 7975e71..6fdb02e 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityServiceTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/ChangeCartItemQuantityServiceTest.java @@ -34,14 +34,15 @@ void setUp() { @Test @DisplayName("changeQuantity - when line item exists") void changeQuantity() { + String userId = "USER_ID"; Cart cart = Fixtures.cart(List.of(Fixtures.product())); LineItem lineItem = cart.lineItem(0); LineItemId lineItemId = lineItem.id(); - given(cartRepository.findById(any())).willReturn(Optional.of(cart)); + given(cartRepository.findByUserId(any())).willReturn(Optional.of(cart)); - changeCartItemQuantityService.changeQuantity(lineItemId, 10); + changeCartItemQuantityService.changeQuantity(userId, lineItemId, 10); assertThat(lineItem.quantity()).isEqualTo(10); } @@ -49,14 +50,15 @@ void changeQuantity() { @Test @DisplayName("changeQuantity - with incorrect item ID") void changeQuantityWithIncorrectID() { + String userId = "USER_ID"; Cart cart = Fixtures.cart(List.of(Fixtures.product())); LineItemId lineItemId = new LineItemId("test-id"); - given(cartRepository.findById(any())).willReturn(Optional.of(cart)); + given(cartRepository.findByUserId(any())).willReturn(Optional.of(cart)); assertThatThrownBy(() -> { - changeCartItemQuantityService.changeQuantity(lineItemId, 10); + changeCartItemQuantityService.changeQuantity(userId, lineItemId, 10); }); } } diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartServiceTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartServiceTest.java index 77a628d..47c1ef8 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartServiceTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/application/cart/GetCartServiceTest.java @@ -22,7 +22,9 @@ void setUp() { @Test void getCartDto() { - getCartService.getCartDto(); + String userId = "USER_ID"; + + getCartService.getCartDto(userId); verify(cartDtoFetcher).fetchCartDto(any()); } diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ControllerTest.java new file mode 100644 index 0000000..7e695f5 --- /dev/null +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ControllerTest.java @@ -0,0 +1,64 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import kr.megaptera.backendsurvivalweek10.BackendSurvivalWeek10Application; +import kr.megaptera.backendsurvivalweek10.infrastructure.UserDetailsDao; +import kr.megaptera.backendsurvivalweek10.security.AccessTokenService; +import kr.megaptera.backendsurvivalweek10.security.WebSecurityConfig; +import kr.megaptera.backendsurvivalweek10.util.AccessTokenGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import java.util.Optional; + +import static org.mockito.BDDMockito.given; + + +@ContextConfiguration(classes = { + BackendSurvivalWeek10Application.class, + WebSecurityConfig.class, +}) +@ActiveProfiles("test") +public abstract class ControllerTest { + protected static final String USER_ID = "UserId"; + protected static final String ADMIN_ID = "AdminId"; + + @SpyBean + private AccessTokenService accessTokenService; + + @SpyBean + protected AccessTokenGenerator accessTokenGenerator; + + @MockBean + protected UserDetailsDao userDetailsDao; + + protected String userAccessToken; + + protected String adminAccessToken; + + @BeforeEach + void setUpAccessTokenAndUserDetailsDaoForAuthentication() { + userAccessToken = accessTokenGenerator.generate(USER_ID); + adminAccessToken = accessTokenGenerator.generate(ADMIN_ID); + + UserDetails user = User.withUsername(USER_ID) + .password(userAccessToken) + .authorities("ROLE_USER") + .build(); + + given(userDetailsDao.findByAccessToken(userAccessToken)) + .willReturn(Optional.of(user)); + + UserDetails admin = User.withUsername(ADMIN_ID) + .password(adminAccessToken) + .authorities("ROLE_ADMIN") + .build(); + + given(userDetailsDao.findByAccessToken(adminAccessToken)) + .willReturn(Optional.of(admin)); + } +} diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemControllerTest.java index 33e1af4..d7f3375 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemControllerTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/LineItemControllerTest.java @@ -3,7 +3,7 @@ import kr.megaptera.backendsurvivalweek10.application.cart.AddProductToCartService; import kr.megaptera.backendsurvivalweek10.application.cart.ChangeCartItemQuantityService; import kr.megaptera.backendsurvivalweek10.application.cart.GetCartService; -import kr.megaptera.backendsurvivalweek10.dtos.CartDto; +import kr.megaptera.backendsurvivalweek10.dtos.cart.CartDto; import kr.megaptera.backendsurvivalweek10.models.LineItemId; import kr.megaptera.backendsurvivalweek10.models.ProductId; import org.junit.jupiter.api.DisplayName; @@ -25,8 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(LineItemController.class) -@ActiveProfiles("test") -class LineItemControllerTest { +class LineItemControllerTest extends ControllerTest { @Autowired private MockMvc mockMvc; @@ -42,10 +41,11 @@ class LineItemControllerTest { @Test @DisplayName("GET /cart-line-items") void list() throws Exception { - given(getCartService.getCartDto()).willReturn(new CartDto(List.of())); + given(getCartService.getCartDto(USER_ID)).willReturn(new CartDto(List.of())); - mockMvc.perform(get("/cart-line-items")) - .andExpect(status().isOk()); + mockMvc.perform(get("/cart-line-items") + .header("Authorization", "Bearer " + userAccessToken)) + .andExpect(status().isOk()); } @Test @@ -54,21 +54,22 @@ void create() throws Exception { ProductId productId = new ProductId("test-id"); String json = String.format( - """ - { - "productId": "%s", - "quantity": 3 - } - """, - productId + """ + { + "productId": "%s", + "quantity": 3 + } + """, + productId ); mockMvc.perform(post("/cart-line-items") - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isCreated()); + .header("Authorization", "Bearer " + userAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()); - verify(addProductToCartService).addProduct(productId, 3); + verify(addProductToCartService).addProduct(USER_ID, productId, 3); } @Test @@ -79,11 +80,12 @@ void update() throws Exception { String json = "{\"quantity\": 3}"; mockMvc.perform(patch("/cart-line-items/" + lineItemId) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isNoContent()); + .header("Authorization", "Bearer " + userAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNoContent()); - verify(changeCartItemQuantityService).changeQuantity(lineItemId, 3); + verify(changeCartItemQuantityService).changeQuantity(USER_ID, lineItemId, 3); } @Test @@ -94,12 +96,13 @@ void updateWithIncorrectID() throws Exception { String json = "{\"quantity\": 3}"; doThrow(new NoSuchElementException()) - .when(changeCartItemQuantityService) - .changeQuantity(lineItemId, 3); + .when(changeCartItemQuantityService) + .changeQuantity(USER_ID, lineItemId, 3); mockMvc.perform(patch("/cart-line-items/" + lineItemId) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isNotFound()); + .header("Authorization", "Bearer " + userAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()); } } diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java index 79e0c61..a13c408 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java @@ -2,7 +2,7 @@ import kr.megaptera.backendsurvivalweek10.application.product.CreateProductService; import kr.megaptera.backendsurvivalweek10.application.product.GetProductListService; -import kr.megaptera.backendsurvivalweek10.dtos.ProductListDto; +import kr.megaptera.backendsurvivalweek10.dtos.product.ProductListDto; import kr.megaptera.backendsurvivalweek10.models.Money; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java new file mode 100644 index 0000000..12c9606 --- /dev/null +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java @@ -0,0 +1,120 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import kr.megaptera.backendsurvivalweek10.application.auth.LoginService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(SessionController.class) +class SessionControllerTest extends ControllerTest { + @Autowired + private MockMvc mockMvc; + + @SpyBean + private LoginService loginService; + +// @SpyBean +// private LogoutService logoutService; + + @SpyBean + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setUp() { + UserDetails userDetails = User.withUsername("UserID") + .password(passwordEncoder.encode("password")) + .authorities("ROLE_USER") + .build(); + + given(userDetailsDao.findByUsername("tester")) + .willReturn(Optional.of(userDetails)); + } + + @Test + @DisplayName("POST /session - with correct username and password") + void loginSuccess() throws Exception { + String json = """ + { + "username": "tester", + "password": "password" + } + """; + + mockMvc.perform(post("/session") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("POST /session - with incorrect username") + void loginWithIncorrectUsername() throws Exception { + String json = """ + { + "username": "xxx", + "password": "password" + } + """; + + mockMvc.perform(post("/session") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /session - with incorrect password") + void loginWithIncorrectPassword() throws Exception { + String json = """ + { + "username": "tester", + "password": "xxx" + } + """; + + mockMvc.perform(post("/session") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + +// @Test +// @DisplayName("DELETE /session - with correct access token") +// void logoutWithCorrectAccessToken() throws Exception { +// mockMvc.perform(delete("/session") +// .header("Authorization", "Bearer " + userAccessToken)) +// .andExpect(status().isOk()); +// } +// +// @Test +// @DisplayName("DELETE /session - with incorrect access token") +// void logoutWithIncorrectAccessToken() throws Exception { +// mockMvc.perform(delete("/session") +// .header("Authorization", "Bearer XXX")) +// .andExpect(status().isForbidden()); +// } +// +// @Test +// @DisplayName("DELETE /session - without access token") +// void logoutWithoutAccessToken() throws Exception { +// mockMvc.perform(delete("/session")) +// .andExpect(status().isForbidden()); +// } + +} + diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/UserControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/UserControllerTest.java new file mode 100644 index 0000000..eb8e86c --- /dev/null +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/UserControllerTest.java @@ -0,0 +1,83 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import kr.megaptera.backendsurvivalweek10.application.auth.SignupService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +class UserControllerTest extends ControllerTest { + @Autowired + private MockMvc mockMvc; + + @SpyBean + private SignupService signupService; + + @SpyBean + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setUp() { + given(userDetailsDao.existsByUsername("tester")).willReturn(true); + } + + @Test + @DisplayName("POST /users - with valid attributes") + void signupSuccess() throws Exception { + String json = """ + { + "username": "newbie", + "password": "password" + } + """; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("POST /users - with invalid attributes") + void signupWithInvalidAttributes() throws Exception { + String json = """ + { + "username": "", + "password": "" + } + """; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /users - when username already exists") + void signupDuplicated() throws Exception { + String json = """ + { + "username": "tester", + "password": "password" + } + """; + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } +} diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeControllerTest.java new file mode 100644 index 0000000..750ce5d --- /dev/null +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/WelcomeControllerTest.java @@ -0,0 +1,70 @@ +package kr.megaptera.backendsurvivalweek10.controllers; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(WelcomeController.class) +class WelcomeControllerTest extends ControllerTest { + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("GET / - with correct access token") + void homeWithAccessToken() throws Exception { + mockMvc.perform(get("/").header("Authorization", "Bearer " + userAccessToken)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("GET / - with incorrect access token") + void homeWithIncorrectAccessToken() throws Exception { + mockMvc.perform(get("/").header("Authorization", "Bearer XXX")) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("GET / - without access token") + void homeWithoutAccessToken() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("GET /admin - when the current user is ROLE_ADMIN") + void adminWithRoleAdmin() throws Exception { + mockMvc.perform(get("/admin") + .header("Authorization", "Bearer " + adminAccessToken)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("GET /admin - when the current user is ROLE_USER") + void adminWithRoleUser() throws Exception { + mockMvc.perform(get("/admin") + .header("Authorization", "Bearer " + userAccessToken)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("GET /user - when the current user is ROLE_USER") + void userWithRoleUser() throws Exception { + mockMvc.perform(get("/user") + .header("Authorization", "Bearer " + userAccessToken)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("GET /user - when the current user is ROLE_ADMIN") + void userWithRoleAdmin() throws Exception { + mockMvc.perform(get("/user") + .header("Authorization", "Bearer " + adminAccessToken)) + .andExpect(status().isOk()); + } +} diff --git a/web/package.json b/web/package.json index ef35c94..d1b3ed0 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ "serve": "servor dist index.html 8000 --reload", "codeceptjs:headless": "HEADLESS=true codeceptjs run --steps", "codeceptjs": "codeceptjs run --steps", - "test": "concurrently -s first -k 'npm run serve' 'npm run codeceptjs'", + "test": "concurrently -s first -k \"npm run serve\" \"npm run codeceptjs\"", "ci": "concurrently -s first -k 'npm run server' 'npm run serve' 'sleep 30 && npm run codeceptjs:headless'", "watch:test": "jest --watchAll" }, From 9e47851f611c210417e3a8efea890a8b415aec85 Mon Sep 17 00:00:00 2001 From: sws6641 Date: Sun, 24 Sep 2023 21:59:30 +0900 Subject: [PATCH 2/2] =?UTF-8?q?10=EC=A3=BC=EC=9E=90=20=EA=B3=BC=EC=A0=9C?= =?UTF-8?q?=20=EC=A0=9C=EC=B6=9C2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ProductControllerTest.java | 61 +++++++++++++------ .../controllers/SessionControllerTest.java | 49 +++++++-------- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java index a13c408..dd0c535 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/ProductControllerTest.java @@ -23,8 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(ProductController.class) -@ActiveProfiles("test") -class ProductControllerTest { +class ProductControllerTest extends ControllerTest { @Autowired private MockMvc mockMvc; @@ -38,35 +37,57 @@ class ProductControllerTest { @DisplayName("GET /products") void list() throws Exception { ProductListDto.ProductDto productDto = - new ProductListDto.ProductDto("test-id", "제품", 100_000L); + new ProductListDto.ProductDto("test-id", "제품", 100_000L); given(getProductListService.getProductListDto()).willReturn( - new ProductListDto(List.of(productDto))); + new ProductListDto(List.of(productDto))); - mockMvc.perform(get("/products")) - .andExpect(status().isOk()) - .andExpect(contentContains("제품")); + mockMvc.perform(get("/products") + .header("Authorization", "Bearer " + userAccessToken)) + .andExpect(status().isOk()) + .andExpect(contentContains("제품")); } @Test - @DisplayName("POST /products") - void create() throws Exception { + @DisplayName("POST /products - when the current user is ROLE_ADMIN") + void createWithRoleAdmin() throws Exception { String json = String.format( - """ - { - "name": "멋진 제품", - "price": %d - } - """, - 100_000L + """ + { + "name": "멋진 제품", + "price": %d + } + """, + 100_000L ); mockMvc.perform(post("/products") - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isCreated()); + .header("Authorization", "Bearer " + adminAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()); verify(createProductService) - .createProduct("멋진 제품", new Money(100_000L)); + .createProduct("멋진 제품", new Money(100_000L)); + } + + @Test + @DisplayName("POST /products - when the current user is ROLE_USER") + void createWithRoleUSER() throws Exception { + String json = String.format( + """ + { + "name": "멋진 제품", + "price": %d + } + """, + 100_000L + ); + + mockMvc.perform(post("/products") + .header("Authorization", "Bearer " + userAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isForbidden()); } } diff --git a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java index 12c9606..08f3e8e 100644 --- a/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java +++ b/src/test/java/kr/megaptera/backendsurvivalweek10/controllers/SessionControllerTest.java @@ -1,6 +1,7 @@ package kr.megaptera.backendsurvivalweek10.controllers; import kr.megaptera.backendsurvivalweek10.application.auth.LoginService; +import kr.megaptera.backendsurvivalweek10.application.auth.LogoutService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,8 +29,8 @@ class SessionControllerTest extends ControllerTest { @SpyBean private LoginService loginService; -// @SpyBean -// private LogoutService logoutService; + @SpyBean + private LogoutService logoutService; @SpyBean private PasswordEncoder passwordEncoder; @@ -93,28 +94,28 @@ void loginWithIncorrectPassword() throws Exception { .andExpect(status().isBadRequest()); } -// @Test -// @DisplayName("DELETE /session - with correct access token") -// void logoutWithCorrectAccessToken() throws Exception { -// mockMvc.perform(delete("/session") -// .header("Authorization", "Bearer " + userAccessToken)) -// .andExpect(status().isOk()); -// } -// -// @Test -// @DisplayName("DELETE /session - with incorrect access token") -// void logoutWithIncorrectAccessToken() throws Exception { -// mockMvc.perform(delete("/session") -// .header("Authorization", "Bearer XXX")) -// .andExpect(status().isForbidden()); -// } -// -// @Test -// @DisplayName("DELETE /session - without access token") -// void logoutWithoutAccessToken() throws Exception { -// mockMvc.perform(delete("/session")) -// .andExpect(status().isForbidden()); -// } + @Test + @DisplayName("DELETE /session - with correct access token") + void logoutWithCorrectAccessToken() throws Exception { + mockMvc.perform(delete("/session") + .header("Authorization", "Bearer " + userAccessToken)) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("DELETE /session - with incorrect access token") + void logoutWithIncorrectAccessToken() throws Exception { + mockMvc.perform(delete("/session") + .header("Authorization", "Bearer XXX")) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("DELETE /session - without access token") + void logoutWithoutAccessToken() throws Exception { + mockMvc.perform(delete("/session")) + .andExpect(status().isForbidden()); + } }