diff --git a/build.gradle b/build.gradle index c0e8e778..413e4804 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,10 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + + testImplementation 'io.rest-assured:rest-assured:5.4.0' + testImplementation 'io.rest-assured:json-path:5.4.0' + testImplementation 'io.rest-assured:json-schema-validator:5.4.0' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java b/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java index 5bdd0679..957a0502 100644 --- a/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java +++ b/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java @@ -10,13 +10,9 @@ @Component public class JwtResolver { - private static final String ISSUER = "RunUSAuthService"; + private static final String ISSUER = "RUNIMO_SERVICE"; @Value("${jwt.secret}") private String jwtSecret; - @Value("${jwt.expiration}") - private long jwtExpiration; - @Value("${jwt.refresh.expiration}") - private long jwtRefreshExpiration; public DecodedJWT verifyAccessToken(String token) throws JWTVerificationException { return JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(ISSUER).build().verify(token); diff --git a/src/main/java/org/runimo/runimo/auth/jwt/JwtTokenFactory.java b/src/main/java/org/runimo/runimo/auth/jwt/JwtTokenFactory.java index d2851c53..911bf008 100644 --- a/src/main/java/org/runimo/runimo/auth/jwt/JwtTokenFactory.java +++ b/src/main/java/org/runimo/runimo/auth/jwt/JwtTokenFactory.java @@ -23,24 +23,24 @@ public class JwtTokenFactory { @Value("${jwt.refresh.expiration}") private long jwtRefreshExpiration; - public String generateAccessToken(User user) { + public String generateAccessToken(String userPublicId) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpiration); return JWT.create() - .withSubject(user.getPublicId()) + .withSubject(userPublicId) .withIssuedAt(now) .withExpiresAt(expiryDate) .withIssuer(ISSUER) .sign(Algorithm.HMAC256(jwtSecret)); } - public String generateRefreshToken(User user) { + public String generateRefreshToken(String userPublicId) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtRefreshExpiration); return JWT.create() - .withSubject(user.getPublicId()) + .withSubject(userPublicId) .withIssuedAt(now) .withExpiresAt(expiryDate) .withIssuer(ISSUER) @@ -49,8 +49,8 @@ public String generateRefreshToken(User user) { } public TokenPair generateTokenPair(User user) { - String accessToken = generateAccessToken(user); - String refreshToken = generateRefreshToken(user); + String accessToken = generateAccessToken(user.getPublicId()); + String refreshToken = generateRefreshToken(user.getPublicId()); return new TokenPair(accessToken, refreshToken); } } diff --git a/src/main/java/org/runimo/runimo/common/BaseEntity.java b/src/main/java/org/runimo/runimo/common/BaseEntity.java index 84fc971e..23de1604 100644 --- a/src/main/java/org/runimo/runimo/common/BaseEntity.java +++ b/src/main/java/org/runimo/runimo/common/BaseEntity.java @@ -1,9 +1,6 @@ package org.runimo.runimo.common; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.CreationTimestamp; diff --git a/src/main/java/org/runimo/runimo/common/GlobalConsts.java b/src/main/java/org/runimo/runimo/common/GlobalConsts.java index fe72b427..9ba476a4 100644 --- a/src/main/java/org/runimo/runimo/common/GlobalConsts.java +++ b/src/main/java/org/runimo/runimo/common/GlobalConsts.java @@ -3,19 +3,15 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; -import java.time.ZoneId; import java.util.Set; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class GlobalConsts { public static final String TIME_ZONE_ID = "Asia/Seoul"; - public static final ZoneId ZONE_ID = ZoneId.of(TIME_ZONE_ID); - public static final String DEFAULT_IMG_URL = "default_img_url"; - public static final String SESSION_ATTRIBUTE_USER = "user-info"; public static final Set WHITE_LIST_ENDPOINTS = Set.of( - "/test/auth", - "/auth", + "/api/v1/test/auth", + "/api/v1/users/auth", "/swagger-ui", "/v3/api-docs" ); diff --git a/src/main/java/org/runimo/runimo/config/SecurityConfig.java b/src/main/java/org/runimo/runimo/config/SecurityConfig.java index de740b9c..3ba6ca54 100644 --- a/src/main/java/org/runimo/runimo/config/SecurityConfig.java +++ b/src/main/java/org/runimo/runimo/config/SecurityConfig.java @@ -28,7 +28,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/auth/**").permitAll() + .requestMatchers("/api/v1/users/auth/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -45,7 +45,7 @@ public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exce .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/auth/**").permitAll() + .requestMatchers("/api/v1/users/auth/**").permitAll() .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ); diff --git a/src/main/java/org/runimo/runimo/config/WebMvcConfig.java b/src/main/java/org/runimo/runimo/config/WebMvcConfig.java new file mode 100644 index 00000000..fb70ab99 --- /dev/null +++ b/src/main/java/org/runimo/runimo/config/WebMvcConfig.java @@ -0,0 +1,21 @@ +package org.runimo.runimo.config; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.controller.UserIdResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final UserIdResolver userIdResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdResolver); + } +} diff --git a/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java b/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java index 9fff3689..9094913c 100644 --- a/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java +++ b/src/main/java/org/runimo/runimo/item/domain/ItemActivity.java @@ -1,8 +1,6 @@ package org.runimo.runimo.item.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -17,11 +15,12 @@ public class ItemActivity extends BaseEntity { @Column(name = "activity_user_id", nullable = false) private Long userId; - @Column(name = "activity_event_id", nullable = false) + @Column(name = "activity_item_id", nullable = false) private Long itemId; - + @Column(name = "quantity", nullable = false) private Long quantity; @Column(name = "activity_event_type", nullable = false) + @Enumerated(EnumType.STRING) private ActivityType type; @Builder diff --git a/src/main/java/org/runimo/runimo/user/controller/UserController.java b/src/main/java/org/runimo/runimo/user/controller/UserController.java index a3bb2850..47b205fc 100644 --- a/src/main/java/org/runimo/runimo/user/controller/UserController.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserController.java @@ -41,7 +41,7 @@ public class UserController { @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), @ApiResponse(responseCode = "401", description = "인증 실패") }) - @PostMapping("/login") + @PostMapping("/auth/login") public ResponseEntity> login( @Valid @RequestBody AuthLoginRequest request ) { @@ -63,7 +63,7 @@ public ResponseEntity> login( @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), @ApiResponse(responseCode = "409", description = "이미 존재하는 사용자") }) - @PostMapping("/signup") + @PostMapping("/auth/signup") public ResponseEntity> signupAndLogin( @Valid @RequestBody AuthSignupRequest request) { SignupUserInfo authResult = userOAuthUsecase.validateAndSignup( diff --git a/src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java b/src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java index 580955a8..9a559230 100644 --- a/src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java +++ b/src/main/java/org/runimo/runimo/user/controller/UserIdResolver.java @@ -31,7 +31,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m if (authentication == null || !authentication.isAuthenticated()) { throw new SecurityException("No authentication found"); } - User user = userFinder.findUserById(Long.valueOf(authentication.getName())) + User user = userFinder.findUserByPublicId(authentication.getName()) .orElseThrow(NoPermissionException::new); return user.getId(); } diff --git a/src/main/java/org/runimo/runimo/user/controller/UserItemController.java b/src/main/java/org/runimo/runimo/user/controller/UserItemController.java new file mode 100644 index 00000000..c62658b8 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/UserItemController.java @@ -0,0 +1,27 @@ +package org.runimo.runimo.user.controller; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.service.dtos.ItemQueryResponse; +import org.runimo.runimo.user.service.usecases.MyItemQueryUsecase; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users/me/items") +@RequiredArgsConstructor +public class UserItemController { + + private final MyItemQueryUsecase myItemQueryUsecase; + + @GetMapping + public ResponseEntity> queryItems( + @UserId Long userId + ) { + ItemQueryResponse response = myItemQueryUsecase.execute(userId); + return ResponseEntity.ok(SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response)); + } +} diff --git a/src/main/java/org/runimo/runimo/user/repository/MyItemRepository.java b/src/main/java/org/runimo/runimo/user/repository/MyItemRepository.java new file mode 100644 index 00000000..3092317c --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/repository/MyItemRepository.java @@ -0,0 +1,19 @@ +package org.runimo.runimo.user.repository; + +import org.runimo.runimo.user.domain.UserItem; +import org.runimo.runimo.user.service.dtos.InventoryItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MyItemRepository extends JpaRepository { + + @Query("SELECT new org.runimo.runimo.user.service.dtos.InventoryItem(i.itemId, it.name, i.quantity, it.imgUrl) " + + "FROM UserItem i" + + " join Item it on i.itemId = it.id" + + " WHERE i.userId = :userId") + List findInventoryItemsByUserId(Long userId); +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/InventoryItem.java b/src/main/java/org/runimo/runimo/user/service/dtos/InventoryItem.java new file mode 100644 index 00000000..a4db6d1f --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/InventoryItem.java @@ -0,0 +1,9 @@ +package org.runimo.runimo.user.service.dtos; + +public record InventoryItem( + Long itemId, + String name, + Long amount, + String imgUrl +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/dtos/ItemQueryResponse.java b/src/main/java/org/runimo/runimo/user/service/dtos/ItemQueryResponse.java new file mode 100644 index 00000000..7302548d --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dtos/ItemQueryResponse.java @@ -0,0 +1,8 @@ +package org.runimo.runimo.user.service.dtos; + +import java.util.List; + +public record ItemQueryResponse( + List items +) { +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecase.java new file mode 100644 index 00000000..114b6644 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecase.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.service.usecases; + +import org.runimo.runimo.user.service.dtos.ItemQueryResponse; + +public interface MyItemQueryUsecase { + ItemQueryResponse execute(Long userId); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecaseImpl.java new file mode 100644 index 00000000..0f04f400 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/MyItemQueryUsecaseImpl.java @@ -0,0 +1,23 @@ +package org.runimo.runimo.user.service.usecases; + + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.repository.MyItemRepository; +import org.runimo.runimo.user.service.dtos.InventoryItem; +import org.runimo.runimo.user.service.dtos.ItemQueryResponse; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MyItemQueryUsecaseImpl implements MyItemQueryUsecase { + + private final MyItemRepository myItemRepository; + + @Override + public ItemQueryResponse execute(Long userId) { + List myItems = myItemRepository.findInventoryItemsByUserId(userId); + return new ItemQueryResponse(myItems); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java index 6b2b150c..abee58ae 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UserOAuthUsecaseImpl.java @@ -47,7 +47,7 @@ public SignupUserInfo validateAndSignup(final UserSignupCommand command, final S .ifPresent(oAuthInfo -> { throw new IllegalArgumentException(); }); - User savedUser = userRegisterService.register(command, provider, pid); + User savedUser = userRegisterService.register(command, pid); return new SignupUserInfo(savedUser.getId(), jwtfactory.generateTokenPair(savedUser)); } } diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java b/src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java index 20c146dc..d2138060 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UserRegisterService.java @@ -2,7 +2,6 @@ import lombok.RequiredArgsConstructor; import org.runimo.runimo.rewards.service.eggs.EggGrantService; -import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; import org.runimo.runimo.user.service.UserCreator; import org.runimo.runimo.user.service.UserItemCreator; @@ -19,9 +18,9 @@ public class UserRegisterService { private final EggGrantService eggGrantService; @Transactional - public User register(UserSignupCommand command, SocialProvider provider, String providerId) { + public User register(UserSignupCommand command, String providerId) { User savedUser = userCreator.createUser(command); - userCreator.createUserOAuthInfo(savedUser, provider, providerId); + userCreator.createUserOAuthInfo(savedUser, command.provider(), providerId); userItemCreator.createAll(savedUser.getId()); eggGrantService.grantGreetingEggToUser(savedUser); return savedUser; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f2cfdabe..5513c22e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,7 +18,7 @@ spring: password: ${DB_PASSWORD} jpa: hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} + ddl-auto: ${JPA_DDL_AUTO:none} properties: hibernate: dialect: ${JPA_DIALECT} diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql index d13df5d6..1fa8b843 100644 --- a/src/main/resources/sql/schema.sql +++ b/src/main/resources/sql/schema.sql @@ -13,115 +13,99 @@ DROP TABLE IF EXISTS users; SET FOREIGN_KEY_CHECKS = 1; -CREATE TABLE `users` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `public_id` VARCHAR(255), - `nickname` VARCHAR(255), - `img_url` VARCHAR(255), - `total_distance_in_meters` BIGINT NOT NULL DEFAULT 0, - `total_time_in_seconds` BIGINT NOT NULL DEFAULT 0, - `updated_at` TIMESTAMP, - `created_at` TIMESTAMP, - `deleted_at` TIMESTAMP +CREATE TABLE `users` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `public_id` VARCHAR(255), + `nickname` VARCHAR(255), + `img_url` VARCHAR(255), + `total_distance_in_meters` BIGINT NOT NULL DEFAULT 0, + `total_time_in_seconds` BIGINT NOT NULL DEFAULT 0, + `updated_at` TIMESTAMP, + `created_at` TIMESTAMP, + `deleted_at` TIMESTAMP ); -CREATE TABLE `user_token` ( - `user_id` BIGINT NOT NULL, - `device_token` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE -); +CREATE TABLE `user_token` +( + `user_id` BIGINT NOT NULL, + `device_token` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP, + `updated_at` TIMESTAMP, + `deleted_at` TIMESTAMP, -CREATE TABLE `oauth_accounts` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `user_id` BIGINT NOT NULL, - `provider` VARCHAR(255), - `provider_id` VARCHAR(255), - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ); -CREATE TABLE `running_records` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `user_id` BIGINT NOT NULL, - `record_public_id` VARCHAR(255) NOT NULL, - `start_at` TIMESTAMP, - `end_at` TIMESTAMP, - `total_distance` BIGINT, - `pace_in_milli_seconds` BIGINT, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP +CREATE TABLE `oauth_accounts` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `provider` VARCHAR(255), + `provider_id` VARCHAR(255), + `created_at` TIMESTAMP, + `updated_at` TIMESTAMP, + `deleted_at` TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ); -CREATE TABLE `items` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `name` VARCHAR(255) NOT NULL, - `item_code` VARCHAR(255) NOT NULL, - `description` VARCHAR(255), - `item_type` VARCHAR(255) NOT NULL, - `img_url` VARCHAR(255), - `dtype` VARCHAR(255), - `egg_type` VARCHAR(255), - `hatch_require_amount` BIGINT, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP -); -CREATE TABLE `item_activity` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `activity_user_id` BIGINT NOT NULL, - `activity_item_id` BIGINT NOT NULL, - `activity_event_type` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP +CREATE TABLE `running_records` +( + `id` integer PRIMARY KEY AUTO_INCREMENT, + `user_id` integer NOT NULL, + `record_public_id` VARCHAR(255) NOT NULL, + `title` varchar(255), + `started_at` timestamp, + `end_at` timestamp, + `total_distance` integer, + `pace_in_milli_seconds` integer, + `created_at` timestamp, + `updated_at` timestamp, + `deleted_at` TIMESTAMP ); -CREATE TABLE `user_item` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `user_id` BIGINT NOT NULL, - `item_id` BIGINT NOT NULL, - `quantity` BIGINT NOT NULL, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP +CREATE TABLE `items` +( + `id` integer PRIMARY KEY AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `item_code` varchar(255) NOT NULL, + `description` varchar(255), + `item_type` varchar(255) NOT NULL, + `img_url` varchar(255), + `dtype` varchar(255), + `egg_type` varchar(255), + `hatch_require_amount` long, + `created_at` timestamp, + `updated_at` timestamp, + `deleted_at` TIMESTAMP ); -CREATE TABLE `incubator` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `user_id` BIGINT NOT NULL, - `slot` BIGINT NOT NULL, - `egg_id` BIGINT NOT NULL, - `progress` BIGINT DEFAULT 0, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP +CREATE TABLE `item_activity` +( + `id` integer PRIMARY KEY AUTO_INCREMENT, + `activity_user_id` integer NOT NULL, + `activity_item_id` integer NOT NULL, + `activity_event_type` varchar(255) NOT NULL, + `quantity` integer, + `created_at` timestamp, + `updated_at` timestamp, + `deleted_at` TIMESTAMP ); -CREATE TABLE `runimo` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `name` VARCHAR(255), - `description` VARCHAR(255), - `type` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP +CREATE TABLE `user_item` +( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` integer NOT NULL, + `item_id` integer NOT NULL, + `quantity` integer NOT NULL, + `created_at` timestamp, + `updated_at` timestamp, + `deleted_at` TIMESTAMP ); -CREATE TABLE `user_runimo` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `user_id` BIGINT NOT NULL, - `runimo_id` BIGINT NOT NULL, - `created_at` TIMESTAMP, - `updated_at` TIMESTAMP, - `deleted_at` TIMESTAMP -); +ALTER TABLE `user_token` + ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`); -ALTER TABLE `user_token` ADD FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE; -ALTER TABLE `oauth_accounts` ADD FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE; \ No newline at end of file +ALTER TABLE `oauth_accounts` + ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`); diff --git a/src/test/java/org/runimo/runimo/rewards/RewardTest.java b/src/test/java/org/runimo/runimo/rewards/RewardTest.java index 15a8b662..06a98286 100644 --- a/src/test/java/org/runimo/runimo/rewards/RewardTest.java +++ b/src/test/java/org/runimo/runimo/rewards/RewardTest.java @@ -49,7 +49,7 @@ class RewardTest { void setUp() { //given UserSignupCommand command = new UserSignupCommand("test", SocialProvider.KAKAO, "1234"); - savedUser = userRegisterService.register(command, SocialProvider.KAKAO, "1234"); + savedUser = userRegisterService.register(command, "1234"); } @AfterEach diff --git a/src/test/java/org/runimo/runimo/user/UserFixtures.java b/src/test/java/org/runimo/runimo/user/UserFixtures.java new file mode 100644 index 00000000..4800024a --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/UserFixtures.java @@ -0,0 +1,15 @@ +package org.runimo.runimo.user; + +import org.runimo.runimo.user.domain.User; + +public final class UserFixtures { + + public static User getDefaultUser() { + return User.builder() + .nickname("test") + .imgUrl("test") + .totalDistanceInMeters(0L) + .totalTimeInSeconds(0L) + .build(); + } +} diff --git a/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java b/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java new file mode 100644 index 00000000..e3fe2835 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/api/QueryItemControllerTest.java @@ -0,0 +1,52 @@ +package org.runimo.runimo.user.api; + +import org.junit.jupiter.api.Test; +import org.runimo.runimo.auth.jwt.JwtTokenFactory; +import org.runimo.runimo.user.UserFixtures; +import org.runimo.runimo.user.service.UserFinder; +import org.runimo.runimo.user.service.dtos.ItemQueryResponse; +import org.runimo.runimo.user.service.usecases.MyItemQueryUsecase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class QueryItemControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private JwtTokenFactory jwtTokenFactory; + @MockitoBean + private UserFinder userFinder; + @MockitoBean + private MyItemQueryUsecase myItemQueryUsecase; + + @Test + void 보유한_아이템_조회_성공() throws Exception { + String accessToken = jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + given(myItemQueryUsecase.execute(any())) + .willReturn(new ItemQueryResponse(new ArrayList<>())); + given(userFinder.findUserByPublicId(any())) + .willReturn(Optional.of(UserFixtures.getDefaultUser())); + mockMvc.perform(get("/api/v1/users/me/items") + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java new file mode 100644 index 00000000..49908a77 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java @@ -0,0 +1,159 @@ +package org.runimo.runimo.user.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.response.ValidatableResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.runimo.runimo.CleanUpUtil; +import org.runimo.runimo.auth.jwt.JwtTokenFactory; +import org.runimo.runimo.auth.service.OidcService; +import org.runimo.runimo.user.controller.request.AuthSignupRequest; +import org.runimo.runimo.user.controller.request.UseItemRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.jdbc.Sql; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + +@ActiveProfiles("test") +class UserItemAcceptanceTest { + + @LocalServerPort + private int port; + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private JwtTokenFactory jwtTokenFactory; + + @MockitoBean + private OidcService oidcService; + + @Autowired + private CleanUpUtil cleanUpUtil; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @AfterEach() + void tearDown() { + cleanUpUtil.cleanUpUserInfos(); + } + + @Test + @Sql(scripts = "/sql/user_item_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 아이템_조회_성공() { + // given + String jwt = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + + // when + then + given() + .header("Authorization", jwt) + .when() + .get("/api/v1/users/me/items") + .then() + .log().all() + .statusCode(200) + .contentType(ContentType.JSON) + .body("success", equalTo(true)) + .body("payload", notNullValue()) + .body("payload.size()", greaterThan(0)) + .body("payload.items[0].item_id", not(emptyOrNullString())) + .body("payload.items[0].amount", greaterThanOrEqualTo(0)); + } + + @Test + @Sql(scripts = "/sql/user_item_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 아이템_사용시_보유량감소() throws JsonProcessingException { + String jwt = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + + UseItemRequest request = new UseItemRequest(1L, 2L); + + given() + .header("Authorization", jwt) + .body(objectMapper.writeValueAsString(request)) + .contentType(ContentType.JSON) + .when() + .post("/api/v1/users/me/items/use") + .then() + .log().ifError() + .statusCode(HttpStatus.OK.value()); + + given() + .header("Authorization", jwt) + .when() + .get("/api/v1/users/me/items") + .then() + .statusCode(HttpStatus.OK.value()) + .body("payload", notNullValue()) + .body("payload.size()", greaterThan(0)) + .body("payload.items[0].item_id", not(emptyOrNullString())) + .body("payload.items[0].amount", equalTo(0)); + } + + @Test + @Sql(scripts = "/sql/user_item_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 보유한_수량보다_더_많은_요청_시_에러() throws JsonProcessingException { + String jwt = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + UseItemRequest request = new UseItemRequest(1L, 10L); + + given() + .header("Authorization", jwt) + .body(objectMapper.writeValueAsString(request)) + .contentType(ContentType.JSON) + .when() + .post("/api/v1/users/me/items/use") + .then() + .log().ifError() + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void 회원가입후_알_지급_성공() throws JsonProcessingException { + String token = jwtTokenFactory.generateAccessToken("test-user-uuid-1"); + when(oidcService.validateOidcTokenAndGetProviderId(any(), any())) + .thenReturn("123"); + + AuthSignupRequest request = new AuthSignupRequest(token, "KAKAO", "1234", "https://example.com/image.jpg"); + + ValidatableResponse res = given() + .body(objectMapper.writeValueAsString(request)) + .contentType(ContentType.JSON) + .when() + .post("/api/v1/users/auth/signup") + .then() + .log().ifError() + .statusCode(HttpStatus.CREATED.value()); + + String accessToken = res.extract().body().jsonPath().getString("payload.access_token"); + + given() + .header("Authorization", "Bearer " + accessToken) + .when() + .get("/api/v1/users/me/items") + .then() + .log().all() + .statusCode(200) + .contentType(ContentType.JSON) + .body("success", equalTo(true)) + .body("payload", notNullValue()) + .body("payload.size()", greaterThan(0)) + .body("payload.items[0].item_id", not(emptyOrNullString())) + .body("payload.items[0].amount", greaterThanOrEqualTo(0)); + } +} diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java new file mode 100644 index 00000000..fa7f1021 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/service/usecases/QueryUserItemUsecaseTest.java @@ -0,0 +1,46 @@ +package org.runimo.runimo.user.service.usecases; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.runimo.runimo.user.repository.MyItemRepository; +import org.runimo.runimo.user.service.dtos.InventoryItem; +import org.runimo.runimo.user.service.dtos.ItemQueryResponse; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.MockitoAnnotations.openMocks; + +/* + * 1. 유저의 아이템을 조회한다. + * */ +class QueryUserItemUsecaseTest { + + private MyItemQueryUsecase myItemQueryUsecase; + @Mock + private MyItemRepository myItemRepository; + + @BeforeEach + void setUp() { + openMocks(this); + myItemQueryUsecase = new MyItemQueryUsecaseImpl(myItemRepository); + } + + @Test + void 사용자의_보유_아이템_전체_조회() { + // given + Long userId = 1L; + given(myItemRepository.findInventoryItemsByUserId(userId)).willReturn(List.of( + new InventoryItem(1L, "마당알", 1L, "imgUrl"), + new InventoryItem(2L, "숲알", 30L, "imgUr2l")) + ); + // when + ItemQueryResponse res = myItemQueryUsecase.execute(userId); + // then + assertNotNull(res); + assertEquals(2, res.items().size()); + } +} diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java index 953476de..a23b80f1 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UserRegisterServiceTest.java @@ -1,6 +1,8 @@ package org.runimo.runimo.user.service.usecases; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.runimo.runimo.CleanUpUtil; import org.runimo.runimo.item.repository.ItemRepository; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; @@ -25,12 +27,19 @@ class UserRegisterServiceTest { private UserItemRepository userItemRepository; @Autowired private ItemRepository itemRepository; + @Autowired + private CleanUpUtil cleanUpUtil; + + @AfterEach + void tearDown() { + cleanUpUtil.cleanUpUserInfos(); + } @Test void 회원가입_알_지급_테스트() { // given UserSignupCommand command = new UserSignupCommand("test", SocialProvider.KAKAO, "1234"); - User createdUser = userRegisterService.register(command, SocialProvider.KAKAO, "1234"); + User createdUser = userRegisterService.register(command, "1234"); UserItem ui = userItemRepository.findByUserIdAndItemId(createdUser.getId(), 1L).get(); assertNotNull(ui); assertEquals(1L, ui.getQuantity()); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application.yml similarity index 97% rename from src/test/resources/application-test.yml rename to src/test/resources/application.yml index 4f302129..f1985a73 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application.yml @@ -17,7 +17,7 @@ spring: password: ${DB_PASSWORD} jpa: hibernate: - ddl-auto: ${JPA_DDL_AUTO:update} + ddl-auto: none properties: hibernate: dialect: org.hibernate.dialect.H2Dialect diff --git a/src/test/resources/sql/schema.sql b/src/test/resources/sql/schema.sql index fa4e7a14..1fa8b843 100644 --- a/src/test/resources/sql/schema.sql +++ b/src/test/resources/sql/schema.sql @@ -87,6 +87,7 @@ CREATE TABLE `item_activity` `activity_user_id` integer NOT NULL, `activity_item_id` integer NOT NULL, `activity_event_type` varchar(255) NOT NULL, + `quantity` integer, `created_at` timestamp, `updated_at` timestamp, `deleted_at` TIMESTAMP diff --git a/src/test/resources/sql/user_item_test_data.sql b/src/test/resources/sql/user_item_test_data.sql new file mode 100644 index 00000000..78aea917 --- /dev/null +++ b/src/test/resources/sql/user_item_test_data.sql @@ -0,0 +1,25 @@ +-- 사용자 +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE users; +INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, total_time_in_seconds, created_at, + updated_at) +VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, NOW(), NOW()), + (2, 'test-user-uuid-2', 'Moon', 'https://example.com/images/user2.png', 5000, 1800, NOW(), NOW()); +SET FOREIGN_KEY_CHECKS = 1; + + +TRUNCATE TABLE items; +INSERT INTO items (name, item_code, description, item_type, img_url, dtype, egg_type, hatch_require_amount, created_at, + updated_at) +VALUES ('마당알', 'A100', '마당알: 기본 알', 'USABLE', 'example.url', 'Egg', 'MADANG', 10, NOW(), NOW()); + +INSERT INTO items (name, item_code, description, item_type, img_url, dtype, egg_type, hatch_require_amount, created_at, + updated_at) +VALUES ('숲알', 'A101', '숲알: 기본 알', 'USABLE', 'example1.url', 'Egg', 'FOREST', 20, NOW(), NOW()); + + +-- 보유 아이템 +TRUNCATE TABLE user_item; +INSERT INTO user_item (id, user_id, item_id, quantity, created_at, updated_at) +VALUES (1001, 1, 1, 2, NOW(), NOW()), + (1002, 1, 2, 1, NOW(), NOW())