From 92989728a0faeae4bfba78fd7b53e6a4eb4fda20 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 00:05:01 +0900 Subject: [PATCH 01/10] :sparkles: feat : add `device_token`, `device_platform` to UserSignupDtos --- .../controller/request/AuthSignupRequest.java | 53 ++++++++++++++++--- .../auth/service/dto/UserSignupCommand.java | 5 +- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/controller/request/AuthSignupRequest.java b/src/main/java/org/runimo/runimo/auth/controller/request/AuthSignupRequest.java index 50f3dc04..85b9b0e2 100644 --- a/src/main/java/org/runimo/runimo/auth/controller/request/AuthSignupRequest.java +++ b/src/main/java/org/runimo/runimo/auth/controller/request/AuthSignupRequest.java @@ -2,23 +2,62 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.runimo.runimo.auth.service.dto.UserSignupCommand; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; import org.springframework.web.multipart.MultipartFile; @Schema(description = "사용자 회원가입 요청 DTO") -public record AuthSignupRequest( +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AuthSignupRequest { + @Schema(description = "회원가입용 임시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI...") - @NotBlank String registerToken, + @NotBlank(message = "회원가입 토큰은 필수입니다") + private String registerToken; @Schema(description = "사용자 닉네임", example = "RunimoUser") - @NotBlank String nickname, + @NotBlank + String nickname; @Schema(description = "성별", example = "FEMALE") - Gender gender -) { + private Gender gender; + + @Schema(description = "디바이스 토큰", example = "string") + private String deviceToken; + + @Schema(description = "디바이스 플랫폼", example = "FCM / APNS") + private String devicePlatform; + + public AuthSignupRequest(String registerToken, String nickname, Gender gender) { + this.registerToken = registerToken; + this.nickname = nickname; + this.gender = gender; + } public UserSignupCommand toUserSignupCommand(MultipartFile file) { - return new UserSignupCommand(registerToken, nickname, file, gender); + if (hasDeviceToken() && (devicePlatform == null || devicePlatform.trim().isEmpty())) { + throw new IllegalArgumentException("디바이스 토큰이 있으면 플랫폼도 필수입니다."); + } + return new UserSignupCommand( + registerToken, + nickname, + file, + gender, + deviceToken, + devicePlatform != null ? DevicePlatform.fromString(devicePlatform) : null + ); + } + + private boolean hasDeviceToken() { + return deviceToken != null && !deviceToken.trim().isEmpty(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/runimo/runimo/auth/service/dto/UserSignupCommand.java b/src/main/java/org/runimo/runimo/auth/service/dto/UserSignupCommand.java index 9eefe623..4c1b9a6f 100644 --- a/src/main/java/org/runimo/runimo/auth/service/dto/UserSignupCommand.java +++ b/src/main/java/org/runimo/runimo/auth/service/dto/UserSignupCommand.java @@ -1,6 +1,7 @@ package org.runimo.runimo.auth.service.dto; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; import org.springframework.web.multipart.MultipartFile; @@ -8,7 +9,9 @@ public record UserSignupCommand( String registerToken, String nickname, MultipartFile profileImage, - Gender gender + Gender gender, + String deviceToken, + DevicePlatform devicePlatform ) { } From 2acf56cde09c06586b43f6af6a54abd63c9f61d1 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 00:05:14 +0900 Subject: [PATCH 02/10] :sparkles: feat : add UserDeviceToken entity --- .../runimo/user/domain/DevicePlatform.java | 11 ++++ .../runimo/user/domain/UserDeviceToken.java | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java create mode 100644 src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java diff --git a/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java b/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java new file mode 100644 index 00000000..a382ec89 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java @@ -0,0 +1,11 @@ +package org.runimo.runimo.user.domain; + +public enum DevicePlatform { + FCM, + APNS; + + + public static DevicePlatform fromString(String value) { + return DevicePlatform.valueOf(value.toUpperCase()); + } +} diff --git a/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java b/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java new file mode 100644 index 00000000..fbdc19ba --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java @@ -0,0 +1,60 @@ +package org.runimo.runimo.user.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.runimo.runimo.common.CreateUpdateAuditEntity; + +@Table(name = "user_token") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserDeviceToken extends CreateUpdateAuditEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "user_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @Column(name = "device_token", nullable = false) + private String deviceToken; + + @Column(name = "platform", nullable = false) + private DevicePlatform platform; + + @Column(name = "notification_allowed", nullable = false) + private Boolean notificationAllowed; + + @Column(name = "last_used_at") + private LocalDateTime lastUsedAt; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + public static UserDeviceToken from(String deviceToken, DevicePlatform platform, + Boolean notificationAllowed) { + return UserDeviceToken.builder() + .deviceToken(deviceToken) + .notificationAllowed(notificationAllowed) + .platform(platform) + .build(); + } + +} From 120c1457bd746f39f99edefbfbf7f0e5b3430172 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 00:07:36 +0900 Subject: [PATCH 03/10] :sparkles: feat : implement saving user-device-token when register --- .../repository/UserDeviceTokenRepository.java | 10 ++++++++++ .../runimo/runimo/user/service/UserCreator.java | 16 ++++++++++++++++ .../runimo/user/service/UserRegisterService.java | 2 ++ .../service/dto/command/UserRegisterCommand.java | 5 ++++- 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java diff --git a/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java b/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java new file mode 100644 index 00000000..e10ff445 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java @@ -0,0 +1,10 @@ +package org.runimo.runimo.user.repository; + +import org.runimo.runimo.user.domain.UserDeviceToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserDeviceTokenRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/org/runimo/runimo/user/service/UserCreator.java b/src/main/java/org/runimo/runimo/user/service/UserCreator.java index 9ae53c79..dd0f1959 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserCreator.java +++ b/src/main/java/org/runimo/runimo/user/service/UserCreator.java @@ -1,12 +1,15 @@ package org.runimo.runimo.user.service; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.LovePoint; import org.runimo.runimo.user.domain.OAuthInfo; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; +import org.runimo.runimo.user.domain.UserDeviceToken; import org.runimo.runimo.user.repository.LovePointRepository; import org.runimo.runimo.user.repository.OAuthInfoRepository; +import org.runimo.runimo.user.repository.UserDeviceTokenRepository; import org.runimo.runimo.user.repository.UserRepository; import org.runimo.runimo.user.service.dto.command.UserCreateCommand; import org.springframework.stereotype.Component; @@ -19,6 +22,7 @@ public class UserCreator { private final UserRepository userRepository; private final OAuthInfoRepository oAuthInfoRepository; private final LovePointRepository lovePointRepository; + private final UserDeviceTokenRepository userDeviceTokenRepository; @Transactional public User createUser(UserCreateCommand command) { @@ -48,4 +52,16 @@ public LovePoint createLovePoint(Long userId) { .build(); return lovePointRepository.save(lovePoint); } + + @Transactional + public UserDeviceToken createUserDeviceToken(User user, String deviceToken, + DevicePlatform platform) { + UserDeviceToken userDeviceToken = UserDeviceToken.builder() + .user(user) + .deviceToken(deviceToken) + .platform(platform) + .notificationAllowed(true) + .build(); + return userDeviceTokenRepository.save(userDeviceToken); + } } diff --git a/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java b/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java index 2e2e08ef..da9d865f 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java +++ b/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java @@ -26,6 +26,8 @@ public User registerUser(UserRegisterCommand command) { userCreator.createUserOAuthInfo(savedUser, command.socialProvider(), command.providerId()); userCreator.createLovePoint(savedUser.getId()); userItemCreator.createAll(savedUser.getId()); + userCreator.createUserDeviceToken(savedUser, command.deviceToken(), + command.devicePlatform()); return savedUser; } diff --git a/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java b/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java index 02037fe0..32404faa 100644 --- a/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java +++ b/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java @@ -1,6 +1,7 @@ package org.runimo.runimo.user.service.dto.command; import jakarta.validation.constraints.NotNull; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; import org.runimo.runimo.user.domain.SocialProvider; @@ -9,7 +10,9 @@ public record UserRegisterCommand( String imgUrl, Gender gender, @NotNull String providerId, - @NotNull SocialProvider socialProvider + @NotNull SocialProvider socialProvider, + String deviceToken, + DevicePlatform devicePlatform ) { } From dd6c0070918082c31b6af0aaf09e43d7aeb8ee42 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 01:55:19 +0900 Subject: [PATCH 04/10] :hammer: ddl : update `user_token` table; enhance columns --- src/main/resources/sql/schema.sql | 14 +++++++++----- src/test/resources/sql/schema.sql | 15 ++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql index eeb23855..f696949a 100644 --- a/src/main/resources/sql/schema.sql +++ b/src/main/resources/sql/schema.sql @@ -37,11 +37,15 @@ CREATE TABLE `users` CREATE TABLE `user_token` ( - `user_id` BIGINT NOT NULL, - `device_token` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `deleted_at` TIMESTAMP NULL, + `id` BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL, + `user_id` BIGINT NOT NULL, + `device_token` VARCHAR(255) NOT NULL, + `platform` ENUM ('FCM', 'APNS') NOT NULL DEFAULT 'APNS', + `notification_allowed` BOOLEAN NOT NULL DEFAULT TRUE, + `last_used_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL, FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ); diff --git a/src/test/resources/sql/schema.sql b/src/test/resources/sql/schema.sql index 77cf95e1..8353769d 100644 --- a/src/test/resources/sql/schema.sql +++ b/src/test/resources/sql/schema.sql @@ -35,13 +35,18 @@ CREATE TABLE `users` `deleted_at` TIMESTAMP NULL ); + CREATE TABLE `user_token` ( - `user_id` BIGINT NOT NULL, - `device_token` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `deleted_at` TIMESTAMP NULL, + `id` BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL, + `user_id` BIGINT NOT NULL, + `device_token` VARCHAR(255) NOT NULL, + `platform` ENUM ('FCM', 'APNS') NOT NULL DEFAULT 'APNS', + `notification_allowed` BOOLEAN NOT NULL DEFAULT TRUE, + `last_used_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP NULL, FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ); From 653ab2d812909b41e4826cef34a688cbf71f6b36 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 02:38:54 +0900 Subject: [PATCH 05/10] :sparkles: feat : add DeviceTokenDto --- .../service/dto/command/DeviceTokenDto.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java diff --git a/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java b/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java new file mode 100644 index 00000000..bdccf0b4 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java @@ -0,0 +1,19 @@ +package org.runimo.runimo.user.service.dto.command; + +import org.runimo.runimo.user.domain.DevicePlatform; + +public record DeviceTokenDto(String token, DevicePlatform platform) { + + public static final DeviceTokenDto EMPTY = new DeviceTokenDto("", null); + + public static DeviceTokenDto of(String deviceToken, DevicePlatform devicePlatform) { + if (deviceToken == null || deviceToken.isEmpty()) { + return EMPTY; + } + return new DeviceTokenDto(deviceToken, devicePlatform); + } + + public boolean isEmpty() { + return this == EMPTY; + } +} From 64865612410c65825f1d781f2a9b88bfb8d14d24 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 02:40:06 +0900 Subject: [PATCH 06/10] :recycle: refactor : refactor saving device-token logic --- .../auth/service/SignUpUsecaseImpl.java | 22 +++++++++++++------ .../runimo/user/service/UserCreator.java | 14 +++++++----- .../user/service/UserRegisterService.java | 3 +-- .../dto/command/UserRegisterCommand.java | 4 +--- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java b/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java index fbc05f73..00b50013 100644 --- a/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/auth/service/SignUpUsecaseImpl.java @@ -19,6 +19,7 @@ import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.user.repository.AppleUserTokenRepository; import org.runimo.runimo.user.service.UserRegisterService; +import org.runimo.runimo.user.service.dto.command.DeviceTokenDto; import org.runimo.runimo.user.service.dto.command.UserRegisterCommand; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -43,13 +44,8 @@ public SignupUserResponse register(UserSignupCommand command) { SignupToken signupToken = findUnExpiredSignupToken(payload.token()); userRegisterService.validateExistingUser(payload.providerId(), payload.socialProvider()); String imgUrl = fileStorageService.storeFile(command.profileImage()); - User savedUser = userRegisterService.registerUser(new UserRegisterCommand( - command.nickname(), - imgUrl, - command.gender(), - payload.providerId(), - payload.socialProvider()) - ); + User savedUser = userRegisterService.registerUser( + mapToUserCreateCommand(payload, imgUrl, command)); if (payload.socialProvider() == SocialProvider.APPLE) { createAppleUserToken(savedUser.getId(), signupToken); } @@ -78,4 +74,16 @@ private void createAppleUserToken(Long userId, SignupToken signupToken) { ); appleUserTokenRepository.save(appleUserToken); } + + private UserRegisterCommand mapToUserCreateCommand(SignupTokenPayload payload, String imgUrl, + UserSignupCommand command) { + return new UserRegisterCommand( + command.nickname(), + imgUrl, + command.gender(), + payload.providerId(), + payload.socialProvider(), + DeviceTokenDto.of(command.deviceToken(), command.devicePlatform()) + ); + } } diff --git a/src/main/java/org/runimo/runimo/user/service/UserCreator.java b/src/main/java/org/runimo/runimo/user/service/UserCreator.java index dd0f1959..cc22a321 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserCreator.java +++ b/src/main/java/org/runimo/runimo/user/service/UserCreator.java @@ -1,7 +1,6 @@ package org.runimo.runimo.user.service; import lombok.RequiredArgsConstructor; -import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.LovePoint; import org.runimo.runimo.user.domain.OAuthInfo; import org.runimo.runimo.user.domain.SocialProvider; @@ -11,6 +10,7 @@ import org.runimo.runimo.user.repository.OAuthInfoRepository; import org.runimo.runimo.user.repository.UserDeviceTokenRepository; import org.runimo.runimo.user.repository.UserRepository; +import org.runimo.runimo.user.service.dto.command.DeviceTokenDto; import org.runimo.runimo.user.service.dto.command.UserCreateCommand; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -54,14 +54,16 @@ public LovePoint createLovePoint(Long userId) { } @Transactional - public UserDeviceToken createUserDeviceToken(User user, String deviceToken, - DevicePlatform platform) { + public void createUserDeviceToken(User user, DeviceTokenDto deviceTokenDto) { + if (deviceTokenDto.isEmpty()) { + return; + } UserDeviceToken userDeviceToken = UserDeviceToken.builder() .user(user) - .deviceToken(deviceToken) - .platform(platform) + .deviceToken(deviceTokenDto.token()) + .platform(deviceTokenDto.platform()) .notificationAllowed(true) .build(); - return userDeviceTokenRepository.save(userDeviceToken); + userDeviceTokenRepository.save(userDeviceToken); } } diff --git a/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java b/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java index da9d865f..9cfd1ed1 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java +++ b/src/main/java/org/runimo/runimo/user/service/UserRegisterService.java @@ -26,8 +26,7 @@ public User registerUser(UserRegisterCommand command) { userCreator.createUserOAuthInfo(savedUser, command.socialProvider(), command.providerId()); userCreator.createLovePoint(savedUser.getId()); userItemCreator.createAll(savedUser.getId()); - userCreator.createUserDeviceToken(savedUser, command.deviceToken(), - command.devicePlatform()); + userCreator.createUserDeviceToken(savedUser, command.deviceToken()); return savedUser; } diff --git a/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java b/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java index 32404faa..59557170 100644 --- a/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java +++ b/src/main/java/org/runimo/runimo/user/service/dto/command/UserRegisterCommand.java @@ -1,7 +1,6 @@ package org.runimo.runimo.user.service.dto.command; import jakarta.validation.constraints.NotNull; -import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; import org.runimo.runimo.user.domain.SocialProvider; @@ -11,8 +10,7 @@ public record UserRegisterCommand( Gender gender, @NotNull String providerId, @NotNull SocialProvider socialProvider, - String deviceToken, - DevicePlatform devicePlatform + DeviceTokenDto deviceToken ) { } From c23dc5a2b3da27d8f78ae50e19303cfcf6e54797 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 02:42:33 +0900 Subject: [PATCH 07/10] :white_check_mark: test : add test-codes for device-token --- .../auth/controller/AuthAcceptanceTest.java | 299 +++++++++++------- .../auth/controller/AuthControllerTest.java | 67 +++- 2 files changed, 239 insertions(+), 127 deletions(-) diff --git a/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java b/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java index fe9e4d56..c2399d12 100644 --- a/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/auth/controller/AuthAcceptanceTest.java @@ -3,6 +3,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; +import static org.runimo.runimo.user.domain.DevicePlatform.APNS; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -10,6 +11,7 @@ import io.restassured.http.ContentType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.runimo.runimo.CleanUpUtil; import org.runimo.runimo.auth.controller.request.AuthSignupRequest; @@ -33,127 +35,178 @@ @ActiveProfiles("test") class AuthAcceptanceTest { - @LocalServerPort - private int port; - @MockitoBean - private FileStorageService fileStorageService; - @MockitoBean - private KakaoTokenVerifier kakaoTokenVerifier; - @MockitoBean - private AppleTokenVerifier appleTokenVerifier; - - @Autowired - private SignupTokenRepository signupTokenRepository; - - @Autowired - private KakaoLoginHandler kakaoLoginHandler; - - private String token; - @Autowired - private ObjectMapper objectMapper; - @Autowired - private CleanUpUtil cleanUpUtil; - @Autowired - private JwtTokenFactory jwtTokenFactory; - - @BeforeEach - void setUp() { - RestAssured.port = port; - // Save a valid signup token in the database - SignupToken signupToken = new SignupToken( - "valid-token", - "provider-id", - "refresh-token", - SocialProvider.KAKAO - ); - token = jwtTokenFactory.generateSignupTemporalToken("provider-id", SocialProvider.KAKAO, - "valid-token"); - signupTokenRepository.save(signupToken); - } - - @AfterEach - void tearDown() { - cleanUpUtil.cleanUpUserInfos(); - } - - @Test - void 회원가입_성공_201응답() throws JsonProcessingException { - - AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); - - given() - .contentType(ContentType.MULTIPART) - .multiPart("request", objectMapper.writeValueAsString(request)) - .when() - .post("/api/v1/auth/signup") - .then() - .statusCode(HttpStatus.CREATED.value()) - .log().all() - .body("payload.nickname", equalTo("username")) - .body("payload.token_pair.access_token", notNullValue()) - .body("payload.token_pair.refresh_token", notNullValue()); - } - - @Test - void 토큰_오류_회원가입_실패_401응답() throws JsonProcessingException { - AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); - - given() - .contentType(ContentType.MULTIPART) - .multiPart("request", objectMapper.writeValueAsString(request)) - .when() - .post("/api/v1/auth/signup") - .then() - .statusCode(HttpStatus.CREATED.value()) - .log().all() - .body("payload.nickname", equalTo("username")) - .body("payload.token_pair.access_token", notNullValue()) - .body("payload.token_pair.refresh_token", notNullValue()); - - given() - .contentType(ContentType.MULTIPART) - .multiPart("request", objectMapper.writeValueAsString(request)) - .when() - .post("/api/v1/auth/signup") - .then() - .statusCode(HttpStatus.UNAUTHORIZED.value()); - } - - @Test - void 중복_유저_회원가입_409응답() throws JsonProcessingException { - AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); - - given() - .contentType(ContentType.MULTIPART) - .multiPart("request", objectMapper.writeValueAsString(request)) - .when() - .post("/api/v1/auth/signup") - .then() - .statusCode(HttpStatus.CREATED.value()) - .log().all() - .body("payload.nickname", equalTo("username")) - .body("payload.token_pair.access_token", notNullValue()) - .body("payload.token_pair.refresh_token", notNullValue()); - - SignupToken signupToken = new SignupToken( - "valid-token", - "provider-id", - "refresh-token", - SocialProvider.KAKAO - ); - token = jwtTokenFactory.generateSignupTemporalToken("provider-id", SocialProvider.KAKAO, - "valid-token"); - signupTokenRepository.save(signupToken); - request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); - - given() - .contentType(ContentType.MULTIPART) - .multiPart("request", objectMapper.writeValueAsString(request)) - .when() - .post("/api/v1/auth/signup") - .then() - .statusCode(HttpStatus.CONFLICT.value()); - - signupTokenRepository.delete(signupToken); - } + @LocalServerPort + private int port; + @MockitoBean + private FileStorageService fileStorageService; + @MockitoBean + private KakaoTokenVerifier kakaoTokenVerifier; + @MockitoBean + private AppleTokenVerifier appleTokenVerifier; + + @Autowired + private SignupTokenRepository signupTokenRepository; + + @Autowired + private KakaoLoginHandler kakaoLoginHandler; + + private String token; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private CleanUpUtil cleanUpUtil; + @Autowired + private JwtTokenFactory jwtTokenFactory; + + @BeforeEach + void setUp() { + RestAssured.port = port; + // Save a valid signup token in the database + SignupToken signupToken = new SignupToken( + "valid-token", + "provider-id", + "refresh-token", + SocialProvider.KAKAO + ); + token = jwtTokenFactory.generateSignupTemporalToken("provider-id", SocialProvider.KAKAO, + "valid-token"); + signupTokenRepository.save(signupToken); + } + + @AfterEach + void tearDown() { + cleanUpUtil.cleanUpUserInfos(); + signupTokenRepository.deleteAll(); + + } + + @Test + void 회원가입_성공_201응답() throws JsonProcessingException { + + AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CREATED.value()) + .log().all() + .body("payload.nickname", equalTo("username")) + .body("payload.token_pair.access_token", notNullValue()) + .body("payload.token_pair.refresh_token", notNullValue()); + } + + @Test + void 토큰_오류_회원가입_실패_401응답() throws JsonProcessingException { + AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CREATED.value()) + .log().all() + .body("payload.nickname", equalTo("username")) + .body("payload.token_pair.access_token", notNullValue()) + .body("payload.token_pair.refresh_token", notNullValue()); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } + + @Test + void 중복_유저_회원가입_409응답() throws JsonProcessingException { + AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CREATED.value()) + .log().all() + .body("payload.nickname", equalTo("username")) + .body("payload.token_pair.access_token", notNullValue()) + .body("payload.token_pair.refresh_token", notNullValue()); + + SignupToken signupToken = new SignupToken( + "valid-token", + "provider-id", + "refresh-token", + SocialProvider.KAKAO + ); + token = jwtTokenFactory.generateSignupTemporalToken("provider-id", SocialProvider.KAKAO, + "valid-token"); + signupTokenRepository.save(signupToken); + request = new AuthSignupRequest(token, "username", Gender.UNKNOWN); + + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CONFLICT.value()); + + signupTokenRepository.delete(signupToken); + } + + @Test + @DisplayName("디바이스 토큰을 포함하여 회원가입 성공 201응답") + void 회원가입_디바이스_토큰_포함_성공_201응답() throws JsonProcessingException { + AuthSignupRequest request = buildSignupRequest(token, "username", Gender.UNKNOWN); + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CREATED.value()) + .log().all() + .body("payload.nickname", equalTo("username")) + .body("payload.token_pair.access_token", notNullValue()) + .body("payload.token_pair.refresh_token", notNullValue()); + } + + @Test + @DisplayName("디바이스 토큰을 포함했지만 디바이스 플랫폼을 미포함하여 400응답") + void 회원가입_디바이스_토큰_포함_플랫폼_미포함_실패_400응답() throws JsonProcessingException { + AuthSignupRequest request = new AuthSignupRequest(token, "nickname", null, + "example_device_token", null); + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + @DisplayName("APNS_플랫폼으로_회원가입_성공_201응답") + void 회원가입_APNS_플랫폼으로_성공_201응답() throws JsonProcessingException { + AuthSignupRequest request = buildSignupRequest(token, "username", Gender.UNKNOWN); + given() + .contentType(ContentType.MULTIPART) + .multiPart("request", objectMapper.writeValueAsString(request)) + .when() + .post("/api/v1/auth/signup") + .then() + .statusCode(HttpStatus.CREATED.value()) + .log().all(); + } + + private AuthSignupRequest buildSignupRequest(String token, String nickname, Gender gender) { + return new AuthSignupRequest(token, nickname, gender, "example_device_token", APNS.name()); + } } diff --git a/src/test/java/org/runimo/runimo/auth/controller/AuthControllerTest.java b/src/test/java/org/runimo/runimo/auth/controller/AuthControllerTest.java index 465b548a..0784ed2f 100644 --- a/src/test/java/org/runimo/runimo/auth/controller/AuthControllerTest.java +++ b/src/test/java/org/runimo/runimo/auth/controller/AuthControllerTest.java @@ -18,6 +18,7 @@ import org.runimo.runimo.auth.service.TokenRefreshService; import org.runimo.runimo.auth.service.dto.AuthResult; import org.runimo.runimo.auth.service.dto.AuthStatus; +import org.runimo.runimo.auth.service.dto.SignupUserResponse; import org.runimo.runimo.auth.service.dto.TokenPair; import org.runimo.runimo.configs.ControllerTest; import org.runimo.runimo.user.UserFixtures; @@ -28,8 +29,8 @@ import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -@ControllerTest(controllers = {AuthController.class}) +@ControllerTest(controllers = {AuthController.class}) class AuthControllerTest { @Autowired @@ -137,12 +138,70 @@ class AuthControllerTest { // when & then mockMvc.perform( - multipart( - "/api/v1/auth/signup") - .param("request", "{\"registerToken\":\"invalid-token\", \"nickname\":\"RunimoUser\"}")) + multipart("/api/v1/auth/signup") + .param("request", + "{\"registerToken\":\"invalid-token\", \"nickname\":\"RunimoUser\"}")) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.code").value(UserHttpResponseCode.TOKEN_INVALID.getCode())) .andExpect( jsonPath("$.message").value(UserHttpResponseCode.TOKEN_INVALID.getClientMessage())); } + + @Test + @DisplayName("디바이스 토큰 없이 회원가입 요청시 201 응답 (구버전 앱 호환)") + void 회원가입_디바이스_토큰_없음_201응답() throws Exception { + // given - 디바이스 토큰 없는 회원가입도 성공해야 함 + given(signUpUsecase.register(any())).willReturn( + new SignupUserResponse( + 1L, "RunimoUser", "profile_url", new TokenPair("access_token", "refresh_token"), + "exmaple_egg_name", + "example_egg_type", + "example_egg_url" + ) + ); + + // when & then + mockMvc.perform( + multipart("/api/v1/auth/signup") + .param("request", + "{\"registerToken\":\"valid-token\", \"nickname\":\"RunimoUser\"}")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.code").value(UserHttpResponseCode.SIGNUP_SUCCESS.getCode())); + } + + @Test + @DisplayName("디바이스 토큰 있으나 플랫폼 없이 회원가입 요청시 400 응답") + void 회원가입_플랫폼_없음_400응답() throws Exception { + //given + given(signUpUsecase.register(any())) + .willThrow(new IllegalArgumentException("디바이스 토큰이 있으면 플랫폼도 필수입니다.")); + + mockMvc.perform( + multipart("/api/v1/auth/signup") + .param("request", + "{\"registerToken\":\"valid-token\", \"nickname\":\"RunimoUser\", \"deviceToken\":\"valid_device_token\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("정상적인 회원가입 요청시 201 응답") + void 회원가입_성공_201응답() throws Exception { + // given + given(signUpUsecase.register(any())).willReturn( + new SignupUserResponse( + 1L, "RunimoUser", "profile_url", new TokenPair("access_token", "refresh_token"), + "exmaple_egg_name", + "example_egg_type", + "example_egg_url" + ) + ); + + // when & then + mockMvc.perform( + multipart("/api/v1/auth/signup") + .param("request", + "{\"registerToken\":\"valid-token\", \"nickname\":\"RunimoUser\", \"deviceToken\":\"valid_device_token\", \"devicePlatform\":\"FCM\"}")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.code").value(UserHttpResponseCode.SIGNUP_SUCCESS.getCode())); + } } From b76ae79a1a13e0b94e340b4db2b04f0147a686fb Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 10:32:42 +0900 Subject: [PATCH 08/10] :white_check_mark: test : fix command-dto params --- .../user/service/usecases/UserRegisterServiceTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 41ec5fbf..8f02fd9d 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 @@ -13,12 +13,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; 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; import org.runimo.runimo.user.service.UserRegisterService; +import org.runimo.runimo.user.service.dto.command.DeviceTokenDto; import org.runimo.runimo.user.service.dto.command.UserRegisterCommand; class UserRegisterServiceTest { @@ -47,7 +49,8 @@ void setUp() { "https://test.com", Gender.UNKNOWN, providerId, - SocialProvider.KAKAO + SocialProvider.KAKAO, + DeviceTokenDto.of("example-device-token", DevicePlatform.APNS) ); User mockUser = mock(User.class); when(userCreator.createUser(any())).thenReturn(mockUser); From 9480acd29c36239f1dd23f5f146e3b9cf990f6a9 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 10:35:48 +0900 Subject: [PATCH 09/10] :bug: fix : add device-platform validation --- .../java/org/runimo/runimo/user/domain/DevicePlatform.java | 3 ++- .../runimo/user/service/dto/command/DeviceTokenDto.java | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java b/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java index a382ec89..0a98fdfc 100644 --- a/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java +++ b/src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java @@ -2,7 +2,8 @@ public enum DevicePlatform { FCM, - APNS; + APNS, + NONE; public static DevicePlatform fromString(String value) { diff --git a/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java b/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java index bdccf0b4..90da6112 100644 --- a/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java +++ b/src/main/java/org/runimo/runimo/user/service/dto/command/DeviceTokenDto.java @@ -4,10 +4,11 @@ public record DeviceTokenDto(String token, DevicePlatform platform) { - public static final DeviceTokenDto EMPTY = new DeviceTokenDto("", null); + public static final DeviceTokenDto EMPTY = new DeviceTokenDto("", DevicePlatform.NONE); public static DeviceTokenDto of(String deviceToken, DevicePlatform devicePlatform) { - if (deviceToken == null || deviceToken.isEmpty()) { + if (deviceToken == null || deviceToken.isEmpty() || devicePlatform == null + || devicePlatform == DevicePlatform.NONE) { return EMPTY; } return new DeviceTokenDto(deviceToken, devicePlatform); From 6b20ce0f00d3417a1840f0fa9d6216dc3a68009c Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 25 Jun 2025 10:38:22 +0900 Subject: [PATCH 10/10] :white_check_mark: test : update command params --- .../org/runimo/runimo/auth/service/SignUpUsecaseTest.java | 7 +++++-- src/test/java/org/runimo/runimo/rewards/RewardTest.java | 6 ++++-- .../org/runimo/runimo/user/api/UserItemAcceptanceTest.java | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java b/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java index fdb80bef..c9aadced 100644 --- a/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java +++ b/src/test/java/org/runimo/runimo/auth/service/SignUpUsecaseTest.java @@ -25,6 +25,7 @@ import org.runimo.runimo.item.EggFixtures; import org.runimo.runimo.rewards.service.eggs.EggGrantService; import org.runimo.runimo.user.UserFixtures; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.enums.UserHttpResponseCode; @@ -81,7 +82,8 @@ void setUp() { when(jwtTokenFactory.generateTokenPair(any())).thenReturn(UserFixtures.TEST_TOKEN_PAIR); SignupUserResponse response = sut - .register(new UserSignupCommand(registerToken, "nickname", null, Gender.UNKNOWN)); + .register(new UserSignupCommand(registerToken, "nickname", null, Gender.UNKNOWN, + "device_token", DevicePlatform.APNS)); assertEquals(1L, response.userId()); assertEquals(UserFixtures.TEST_USER_NICKNAME, response.nickname()); @@ -110,7 +112,8 @@ void setUp() { .validateExistingUser(payload.providerId(), payload.socialProvider()); assertThrows(SignUpException.class, () -> { - sut.register(new UserSignupCommand(registerToken, "nickname", null, Gender.UNKNOWN)); + sut.register(new UserSignupCommand(registerToken, "nickname", null, Gender.UNKNOWN, + "device_token", DevicePlatform.APNS)); }); } } diff --git a/src/test/java/org/runimo/runimo/rewards/RewardTest.java b/src/test/java/org/runimo/runimo/rewards/RewardTest.java index 59daf198..e7e36e23 100644 --- a/src/test/java/org/runimo/runimo/rewards/RewardTest.java +++ b/src/test/java/org/runimo/runimo/rewards/RewardTest.java @@ -23,6 +23,7 @@ import org.runimo.runimo.rewards.service.RewardService; import org.runimo.runimo.rewards.service.dto.RewardClaimCommand; import org.runimo.runimo.rewards.service.dto.RewardResponse; +import org.runimo.runimo.user.domain.DevicePlatform; import org.runimo.runimo.user.domain.Gender; import org.runimo.runimo.user.domain.SocialProvider; import org.runimo.runimo.user.domain.User; @@ -74,8 +75,9 @@ void setUp() { null, SocialProvider.KAKAO )); - UserSignupCommand command = new UserSignupCommand(registerToken, "name", null, - Gender.UNKNOWN); + UserSignupCommand command = new UserSignupCommand(registerToken, "nickname", null, + Gender.UNKNOWN, + "device_token", DevicePlatform.APNS); Long useId = signUpUsecaseImpl.register(command).userId(); savedUser = userRepository.findById(useId).orElse(null); } diff --git a/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java index 7a3cff68..d9418f2c 100644 --- a/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/UserItemAcceptanceTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.when; import static org.runimo.runimo.TestConsts.TEST_USER_UUID; +import static org.runimo.runimo.user.domain.DevicePlatform.APNS; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -153,7 +154,9 @@ void tearDown() { AuthSignupRequest request = new AuthSignupRequest( registerToken, "test-user", - Gender.FEMALE + Gender.FEMALE, + "device_token", + APNS.name() ); ValidatableResponse res = given()