From 8b028dfe46844a03c1021cbe8b97729416162e72 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:57:27 +0900 Subject: [PATCH 01/20] feat: add handle_owner method in device_token entity --- .../domains/device/domain/DeviceToken.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java index f2d9d1602..a8b822733 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java @@ -62,7 +62,19 @@ public Boolean isActivated() { return activated && lastSignedInAt.plusDays(7).isAfter(now); } + /** + * 디바이스 토큰이 만료되었는지 확인한다. + * + * @return 토큰이 갱신된지 7일이 지났으면 true, 그렇지 않으면 false + */ + public boolean isExpired() { + LocalDateTime now = LocalDateTime.now(); + + return lastSignedInAt.plusDays(7).isBefore(now); + } + public void activate() { + lastSignedInAt = LocalDateTime.now(); this.activated = Boolean.TRUE; } @@ -75,14 +87,17 @@ public void updateLastSignedInAt() { } /** - * 디바이스 토큰이 만료되었는지 확인한다. - * - * @return 토큰이 갱신된지 7일이 지났으면 true, 그렇지 않으면 false + * 토큰의 소유자를 확인하고 필요한 상태 변경을 수행합니다. + * 다른 소유자인 경우 소유자를 갱신하고, 같은 소유자인 경우 활성화만 수행합니다. */ - public boolean isExpired() { - LocalDateTime now = LocalDateTime.now(); + public void handleOwner(User newUser) { + Objects.requireNonNull(newUser, "user는 null이 될 수 없습니다."); - return lastSignedInAt.plusDays(7).isBefore(now); + if (!this.user.equals(newUser)) { + this.user = newUser; + } + + this.activate(); } @Override From e71f64d5ddd95e267cab414216f5983c59ee0c8b Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:02:28 +0900 Subject: [PATCH 02/20] feat: device_token_regiter_service in domain-service module --- .../service/DeviceTokenRegisterService.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java diff --git a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java new file mode 100644 index 000000000..3eacb47ac --- /dev/null +++ b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java @@ -0,0 +1,79 @@ +package kr.co.pennyway.domain.context.account.service; + +import kr.co.pennyway.common.annotation.DomainService; +import kr.co.pennyway.domain.domains.device.domain.DeviceToken; +import kr.co.pennyway.domain.domains.device.service.DeviceTokenRdbService; +import kr.co.pennyway.domain.domains.user.domain.User; +import kr.co.pennyway.domain.domains.user.exception.UserErrorCode; +import kr.co.pennyway.domain.domains.user.exception.UserErrorException; +import kr.co.pennyway.domain.domains.user.service.UserRdbService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@DomainService +@RequiredArgsConstructor +public class DeviceTokenRegisterService { + private final UserRdbService userRdbService; + private final DeviceTokenRdbService deviceTokenRdbService; + + /** + * 사용자의 디바이스 토큰을 생성하거나 갱신한다. + * + *
+     * [비즈니스 규칙]
+     * - 같은 {userId, deviceId}에 대해 새로운 토큰이 발급될 수 있지만, 활성화된 토큰은 하나여야 합니다.
+     * - {deviceId, token} 조합은 시스템 전체에서 유일해야 합니다.
+     * - device token이 이미 등록된 경우, 소유자 정보를 갱신하고 마지막 로그인 시간을 갱신한다.
+     * - device token이 등록되지 않은 경우, 새로운 device token을 생성한다.
+     * 
+ * + * @param userId 사용자 식별자 + * @param deviceId 디바이스 식별자 + * @param deviceName 디바이스 이름 + * @param deviceToken 디바이스 토큰 + * @return {@link DeviceToken} 사용자의 기기로 등록된 Device 정보 + */ + @Transactional + public DeviceToken execute(Long userId, String deviceId, String deviceName, String deviceToken) { + User user = userRdbService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND)); + + return getOrCreateDevice(user, deviceId, deviceName, deviceToken); + } + + /** + * 사용자의 디바이스 토큰을 생성합니다. + * 만약, 이미 등록된 디바이스 토큰이 존재한다면, 해당 토큰을 갱신하고 반환합니다. + */ + private DeviceToken getOrCreateDevice(User user, String deviceId, String deviceName, String deviceToken) { + Optional device = deviceTokenRdbService.readByUserIdAndToken(user.getId(), deviceToken); + + if (device.isPresent()) { + DeviceToken deviceTokenEntity = device.get(); + deviceTokenEntity.handleOwner(user); + return deviceTokenEntity; + } else { + deactivateExistingTokens(user.getId(), deviceId); + + DeviceToken newDeviceToken = DeviceToken.of(deviceToken, deviceId, deviceName, user); + + return deviceTokenRdbService.createDevice(newDeviceToken); + } + } + + /** + * 특정 사용자의 디바이스에 대한 기존 활성 토큰들을 비활성화합니다. + * 새로운 토큰 등록 시 호출되어 하나의 디바이스에 하나의 활성 토큰만 존재하도록 보장합니다. + */ + private void deactivateExistingTokens(Long userId, String deviceId) { + List userDeviceTokens = deviceTokenRdbService.readByUserIdAndDeviceId(userId, deviceId); + + userDeviceTokens.stream() + .filter(DeviceToken::isActivated) + .forEach(DeviceToken::deactivate); + } +} From 7502232f453fe01799877d26290b018e449e509b Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:36:31 +0900 Subject: [PATCH 03/20] feat: add two type of device_token search query --- .../domains/device/repository/DeviceTokenRepository.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java index 4e2ace8b4..913a0ce55 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java @@ -15,6 +15,10 @@ public interface DeviceTokenRepository extends JpaRepository List findAllByUser_Id(Long userId); + Optional findByDeviceIdAndToken(String deviceId, String token); + + List findAllByUser_IdAndDeviceId(Long userId, String deviceId); + @Modifying(clearAutomatically = true) @Transactional @Query("UPDATE DeviceToken d SET d.activated = false WHERE d.user.id = :userId") From c475ee2c5162bafa371435d9012681a0968f1352 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:37:11 +0900 Subject: [PATCH 04/20] feat: add device-token-rdb-service --- .../domains/device/service/DeviceTokenRdbService.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java index 2a3be1078..50e7e7604 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java @@ -29,6 +29,16 @@ public Optional readDeviceByUserIdAndToken(Long userId, String toke return deviceTokenRepository.findByUser_IdAndToken(userId, token); } + @Transactional(readOnly = true) + public Optional readByDeviceIdAndToken(String deviceId, String token) { + return deviceTokenRepository.findByDeviceIdAndToken(deviceId, token); + } + + @Transactional(readOnly = true) + public List readByUserIdAndDeviceId(Long userId, String deviceId) { + return deviceTokenRepository.findAllByUser_IdAndDeviceId(userId, deviceId); + } + /** * @return 비활성화된 디바이스 토큰 정보를 포함합니다. */ From f262cfa46ccf3b58d7f2c63ae9eae43e03e624f9 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:38:34 +0900 Subject: [PATCH 05/20] fix: change read-by-token to read-by-device-id-and-token --- .../context/account/service/DeviceTokenRegisterService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java index 3eacb47ac..632d1f888 100644 --- a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java +++ b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java @@ -50,7 +50,7 @@ public DeviceToken execute(Long userId, String deviceId, String deviceName, Stri * 만약, 이미 등록된 디바이스 토큰이 존재한다면, 해당 토큰을 갱신하고 반환합니다. */ private DeviceToken getOrCreateDevice(User user, String deviceId, String deviceName, String deviceToken) { - Optional device = deviceTokenRdbService.readByUserIdAndToken(user.getId(), deviceToken); + Optional device = deviceTokenRdbService.readByDeviceIdAndToken(deviceId, deviceToken); if (device.isPresent()) { DeviceToken deviceTokenEntity = device.get(); From bcfbc86c7f4816495ed3d42a166105786e142721 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:21:01 +0900 Subject: [PATCH 06/20] fix: add activated condition in to device_token entity's is_expired --- .../co/pennyway/domain/domains/device/domain/DeviceToken.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java index a8b822733..a48ab8008 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java @@ -65,12 +65,12 @@ public Boolean isActivated() { /** * 디바이스 토큰이 만료되었는지 확인한다. * - * @return 토큰이 갱신된지 7일이 지났으면 true, 그렇지 않으면 false + * @return 토큰이 갱신된지 7일이 지났거나 토큰이 비활성화 되었다면 true, 그렇지 않으면 false */ public boolean isExpired() { LocalDateTime now = LocalDateTime.now(); - return lastSignedInAt.plusDays(7).isBefore(now); + return !activated || lastSignedInAt.plusDays(7).isBefore(now); } public void activate() { From d519be6ca04ecdce326c8404b99285ab2e32ef0f Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:09:24 +0900 Subject: [PATCH 07/20] feat: add device token duplicated error code --- .../domains/device/exception/DeviceTokenErrorCode.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/exception/DeviceTokenErrorCode.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/exception/DeviceTokenErrorCode.java index 32d05abf9..56bc99628 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/exception/DeviceTokenErrorCode.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/exception/DeviceTokenErrorCode.java @@ -9,7 +9,11 @@ @RequiredArgsConstructor public enum DeviceTokenErrorCode implements BaseErrorCode { /* 404 NOT_FOUND */ - NOT_FOUND_DEVICE(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "디바이스를 찾을 수 없습니다."); + NOT_FOUND_DEVICE(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "디바이스를 찾을 수 없습니다."), + + /* 409 CONFLICT */ + DUPLICATED_DEVICE_TOKEN(StatusCode.CONFLICT, ReasonCode.RESOURCE_ALREADY_EXISTS, "이미 등록된 디바이스 토큰입니다."), + ; private final StatusCode statusCode; private final ReasonCode reasonCode; From b8dd1413fcf994529342887ac21f1630608c308b Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:15:44 +0900 Subject: [PATCH 08/20] fix: add conflict error handling when create_device in device_rdb_service --- .../device/service/DeviceTokenRdbService.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java index 50e7e7604..9e8325cdc 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java @@ -2,9 +2,12 @@ import kr.co.pennyway.common.annotation.DomainService; import kr.co.pennyway.domain.domains.device.domain.DeviceToken; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorCode; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException; import kr.co.pennyway.domain.domains.device.repository.DeviceTokenRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -16,9 +19,18 @@ public class DeviceTokenRdbService { private final DeviceTokenRepository deviceTokenRepository; + /** + * @throws DeviceTokenErrorException 중복된 디바이스 토큰이 이미 존재하는 경우 + */ @Transactional public DeviceToken createDevice(DeviceToken deviceToken) { - return deviceTokenRepository.save(deviceToken); + try { + return deviceTokenRepository.save(deviceToken); + } catch (DataIntegrityViolationException e) { + log.error("DeviceToken 등록 중 중복 에러가 발생했습니다. deviceToken: {}", deviceToken); + + throw new DeviceTokenErrorException(DeviceTokenErrorCode.DUPLICATED_DEVICE_TOKEN); + } } /** From 5b3c94c47b31ea6debb6ccd8e0f8198c28b5a79e Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:20:48 +0900 Subject: [PATCH 09/20] fix: convert from by-device-id-and-token to by-token for unique --- .../domains/device/repository/DeviceTokenRepository.java | 2 +- .../domain/domains/device/service/DeviceTokenRdbService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java index 913a0ce55..492b9a19e 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/repository/DeviceTokenRepository.java @@ -15,7 +15,7 @@ public interface DeviceTokenRepository extends JpaRepository List findAllByUser_Id(Long userId); - Optional findByDeviceIdAndToken(String deviceId, String token); + Optional findByToken(String token); List findAllByUser_IdAndDeviceId(Long userId, String deviceId); diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java index 9e8325cdc..e3f69b999 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/service/DeviceTokenRdbService.java @@ -42,8 +42,8 @@ public Optional readDeviceByUserIdAndToken(Long userId, String toke } @Transactional(readOnly = true) - public Optional readByDeviceIdAndToken(String deviceId, String token) { - return deviceTokenRepository.findByDeviceIdAndToken(deviceId, token); + public Optional readDeviceByToken(String token) { + return deviceTokenRepository.findByToken(token); } @Transactional(readOnly = true) From fc15a5a7914e3d18fff993c33f62689e68d1007a Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:22:18 +0900 Subject: [PATCH 10/20] fix: add validation logic for token value's uniqueness and pair of device-id and token --- .../account/service/DeviceTokenRegisterService.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java index 632d1f888..bbdeef28b 100644 --- a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java +++ b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java @@ -2,6 +2,8 @@ import kr.co.pennyway.common.annotation.DomainService; import kr.co.pennyway.domain.domains.device.domain.DeviceToken; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorCode; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException; import kr.co.pennyway.domain.domains.device.service.DeviceTokenRdbService; import kr.co.pennyway.domain.domains.user.domain.User; import kr.co.pennyway.domain.domains.user.exception.UserErrorCode; @@ -50,10 +52,15 @@ public DeviceToken execute(Long userId, String deviceId, String deviceName, Stri * 만약, 이미 등록된 디바이스 토큰이 존재한다면, 해당 토큰을 갱신하고 반환합니다. */ private DeviceToken getOrCreateDevice(User user, String deviceId, String deviceName, String deviceToken) { - Optional device = deviceTokenRdbService.readByDeviceIdAndToken(deviceId, deviceToken); + Optional device = deviceTokenRdbService.readDeviceByToken(deviceToken); if (device.isPresent()) { DeviceToken deviceTokenEntity = device.get(); + + if (!deviceTokenEntity.getDeviceId().equals(deviceId)) { + throw new DeviceTokenErrorException(DeviceTokenErrorCode.DUPLICATED_DEVICE_TOKEN); + } + deviceTokenEntity.handleOwner(user); return deviceTokenEntity; } else { From 0fabada96b9f67e556f1dc737fabbfbbba61464e Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:37:33 +0900 Subject: [PATCH 11/20] fix: add device_id param on the handle-owner in device-token-entity --- .../pennyway/domain/domains/device/domain/DeviceToken.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java index a48ab8008..00a3ab025 100644 --- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java +++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/device/domain/DeviceToken.java @@ -90,13 +90,18 @@ public void updateLastSignedInAt() { * 토큰의 소유자를 확인하고 필요한 상태 변경을 수행합니다. * 다른 소유자인 경우 소유자를 갱신하고, 같은 소유자인 경우 활성화만 수행합니다. */ - public void handleOwner(User newUser) { + public void handleOwner(User newUser, String newDeviceId) { Objects.requireNonNull(newUser, "user는 null이 될 수 없습니다."); + Objects.requireNonNull(newDeviceId, "deviceId는 null이 될 수 없습니다."); if (!this.user.equals(newUser)) { this.user = newUser; } + if (!this.deviceId.equals(newDeviceId)) { + this.deviceId = newDeviceId; + } + this.activate(); } From 87a79295afbcdfacc22149147501afa88df32142 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:38:48 +0900 Subject: [PATCH 12/20] fix: add conflict condition if different device-id but expired, it will be passed --- .../context/account/service/DeviceTokenRegisterService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java index bbdeef28b..522ce2223 100644 --- a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java +++ b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java @@ -57,11 +57,11 @@ private DeviceToken getOrCreateDevice(User user, String deviceId, String deviceN if (device.isPresent()) { DeviceToken deviceTokenEntity = device.get(); - if (!deviceTokenEntity.getDeviceId().equals(deviceId)) { + if (!deviceTokenEntity.getDeviceId().equals(deviceId) && deviceTokenEntity.isActivated()) { throw new DeviceTokenErrorException(DeviceTokenErrorCode.DUPLICATED_DEVICE_TOKEN); } - deviceTokenEntity.handleOwner(user); + deviceTokenEntity.handleOwner(user, deviceId); return deviceTokenEntity; } else { deactivateExistingTokens(user.getId(), deviceId); From ad94f60b4eb98f6320c991b4b170ed466bd7f3a1 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:39:16 +0900 Subject: [PATCH 13/20] test: device-token-register-service-unit test --- .../DeviceTokenRegisterServiceTest.java | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterServiceTest.java diff --git a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterServiceTest.java b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterServiceTest.java new file mode 100644 index 000000000..38e119118 --- /dev/null +++ b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterServiceTest.java @@ -0,0 +1,259 @@ +package kr.co.pennyway.domain.context.account.service; + +import kr.co.pennyway.domain.context.common.fixture.UserFixture; +import kr.co.pennyway.domain.domains.device.domain.DeviceToken; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException; +import kr.co.pennyway.domain.domains.device.service.DeviceTokenRdbService; +import kr.co.pennyway.domain.domains.user.domain.User; +import kr.co.pennyway.domain.domains.user.exception.UserErrorException; +import kr.co.pennyway.domain.domains.user.service.UserRdbService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class DeviceTokenRegisterServiceTest { + @Mock + private UserRdbService userRdbService; + + @Mock + private DeviceTokenRdbService deviceTokenRdbService; + + @InjectMocks + private DeviceTokenRegisterService deviceTokenRegisterService; + + @Test + @DisplayName("이미 소유 중인 토큰인 경우 마지막 로그인 날짜만 갱신합니다") + void shouldUpdateOwnerWhenTokenExists() { + // given + User user = UserFixture.GENERAL_USER.toUser(); + String expectedToken = "token1"; + String expectedDeviceId = "device1"; + String expectedDeviceName = "Android"; + DeviceToken existingToken = DeviceToken.of(expectedToken, expectedDeviceId, expectedDeviceName, user); + + given(userRdbService.readUser(1L)).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(expectedToken)).willReturn(Optional.of(existingToken)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(1L, expectedDeviceId, expectedDeviceName, expectedToken); + + // then + assertEquals(existingToken, result); + assertEquals(user, result.getUser()); + assertTrue(existingToken.isActivated()); + + verify(deviceTokenRdbService, never()).createDevice(any()); + } + + @Test + @DisplayName("새로운 토큰 등록 시 기존 활성 토큰들은 비활성화되고, 새로 등록된 토큰이 반환됩니다") + void shouldDeactivateExistingTokensWhenRegisteringNew() { + // given + User user = UserFixture.GENERAL_USER.toUserWithCustomSetting(1L, "jayang", "Yang", UserFixture.GENERAL_USER.getNotifySetting()); + String expectedToken = "oldToken"; + String expectedDeviceId = "device1"; + String expectedDeviceName = "Android"; + String expectedNewToken = "newToken"; + DeviceToken originToken = DeviceToken.of(expectedToken, expectedDeviceId, expectedDeviceName, user); + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(expectedNewToken)).willReturn(Optional.empty()); + given(deviceTokenRdbService.readByUserIdAndDeviceId(user.getId(), expectedDeviceId)).willReturn(List.of(originToken)); + given(deviceTokenRdbService.createDevice(any())).willAnswer(invocation -> invocation.getArgument(0)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(user.getId(), expectedDeviceId, expectedDeviceName, expectedNewToken); + + // then + assertTrue(originToken.isExpired()); + assertEquals(expectedNewToken, result.getToken()); + } + + @Test + @DisplayName("사용자가 존재하지 않으면 예외가 발생합니다") + void shouldThrowExceptionWhenUserNotFound() { + // given + given(userRdbService.readUser(1L)).willReturn(Optional.empty()); + + // when & then + assertThrows(UserErrorException.class, () -> + deviceTokenRegisterService.execute(1L, "device1", "Android", "token1")); + } + + @Test + @DisplayName("새로운 토큰 등록 시 올바른 정보로 생성됩니다") + void shouldCreateNewTokenWithCorrectInformation() { + // given + User user = UserFixture.GENERAL_USER.toUserWithCustomSetting(1L, "jayang", "Yang", UserFixture.GENERAL_USER.getNotifySetting()); + String expectedToken = "token1"; + String expectedDeviceId = "device1"; + String expectedDeviceName = "Android"; + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(expectedToken)).willReturn(Optional.empty()); + given(deviceTokenRdbService.readByUserIdAndDeviceId(user.getId(), expectedDeviceId)).willReturn(List.of()); + given(deviceTokenRdbService.createDevice(any())).willAnswer(invocation -> invocation.getArgument(0)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(user.getId(), expectedDeviceId, expectedDeviceName, expectedToken); + + // then + assertEquals(expectedToken, result.getToken()); + assertEquals(expectedDeviceId, result.getDeviceId()); + assertEquals(expectedDeviceName, result.getDeviceName()); + assertEquals(user, result.getUser()); + } + + @Test + @DisplayName("기기의 토큰이 다른 소유자에게 등록되어 있지만 deviceId가 같은 경우, 소유자 정보를 갱신합니다") + void shouldUpdateOwnerWhenTokenExistsWithDifferentUser() { + // given + User user = UserFixture.GENERAL_USER.toUserWithCustomSetting(1L, "jayang", "Yang", UserFixture.GENERAL_USER.getNotifySetting()); + User anotherUser = UserFixture.GENERAL_USER.toUserWithCustomSetting(2L, "another", "User", UserFixture.GENERAL_USER.getNotifySetting()); + String expectedToken = "token1"; + String expectedDeviceId = "device1"; + String expectedDeviceName = "Android"; + DeviceToken existingToken = DeviceToken.of(expectedToken, expectedDeviceId, expectedDeviceName, anotherUser); + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(expectedToken)).willReturn(Optional.of(existingToken)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(user.getId(), expectedDeviceId, expectedDeviceName, expectedToken); + + // then + assertEquals(existingToken, result); + assertEquals(user, result.getUser()); + assertTrue(existingToken.isActivated()); + + verify(deviceTokenRdbService, never()).createDevice(any()); + } + + @Test + @DisplayName("다른 디바이스에서 이미 사용 중인 토큰으로 등록을 시도하면 예외가 발생합니다") + void shouldThrowExceptionWhenTokenExistsForDifferentDevice() { + // given + User user = UserFixture.GENERAL_USER.toUser(); + String token = "token1"; + String deviceId = "device2"; + DeviceToken existingToken = DeviceToken.of(token, "device1", "Android", user); + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(token)).willReturn(Optional.of(existingToken)); + + // when & then + assertThrows(DeviceTokenErrorException.class, () -> + deviceTokenRegisterService.execute(user.getId(), deviceId, "Android", token)); + } + + @Test + @DisplayName("다른 디바이스에 이미 등록된 토큰이지만 비활성화된 경우 소유권을 갱신하고 활성화합니다") + void shouldUpdateOwnerAndActivateWhenTokenExistsButDeactivated() { + // given + User user = UserFixture.GENERAL_USER.toUser(); + String token = "token1"; + String oldDeviceId = "device1"; + String newDeviceId = "device2"; + DeviceToken existingToken = DeviceToken.of(token, oldDeviceId, "Android", user); + existingToken.deactivate(); + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(token)).willReturn(Optional.of(existingToken)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(user.getId(), newDeviceId, "Android", token); + + // then + assertEquals(existingToken, result); + assertEquals(user, result.getUser()); + assertTrue(result.isActivated()); + assertEquals(newDeviceId, result.getDeviceId()); + + verify(deviceTokenRdbService, never()).createDevice(any()); + } + + @Test + @DisplayName("같은 사용자가 여러 기기에 토큰을 등록할 수 있습니다") + void shouldAllowSameUserToRegisterMultipleDevices() { + // given + User user = UserFixture.GENERAL_USER.toUser(); + String device1Token = "token1"; + String device2Token = "token2"; + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(device1Token)).willReturn(Optional.empty()); + given(deviceTokenRdbService.readDeviceByToken(device2Token)).willReturn(Optional.empty()); + given(deviceTokenRdbService.createDevice(any())).willAnswer(invocation -> invocation.getArgument(0)); + + // when + DeviceToken result1 = deviceTokenRegisterService.execute(user.getId(), "device1", "Android", device1Token); + DeviceToken result2 = deviceTokenRegisterService.execute(user.getId(), "device2", "iPhone", device2Token); + + // then + assertTrue(result1.isActivated()); + assertTrue(result2.isActivated()); + assertNotEquals(result1.getDeviceId(), result2.getDeviceId()); + } + + @Test + @DisplayName("이미 등록된 토큰에 대해 같은 디바이스로 재등록을 시도하면 소유자 정보만 갱신됩니다") + void shouldUpdateOwnerWhenTokenExistsForSameDevice() { + // given + User user = UserFixture.GENERAL_USER.toUser(); + String token = "token1"; + String deviceId = "device1"; + User previousOwner = UserFixture.GENERAL_USER.toUserWithCustomSetting(2L, "other", "User", UserFixture.GENERAL_USER.getNotifySetting()); + DeviceToken existingToken = DeviceToken.of(token, deviceId, "Android", previousOwner); + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(token)).willReturn(Optional.of(existingToken)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(user.getId(), deviceId, "Android", token); + + // then + assertEquals(existingToken, result); + assertEquals(user, result.getUser()); + assertTrue(result.isActivated()); + verify(deviceTokenRdbService, never()).createDevice(any()); + } + + @Test + @DisplayName("같은 사용자가 같은 디바이스에 대해 새로운 토큰을 등록하면 기존 토큰은 비활성화됩니다") + void shouldDeactivateExistingTokensWhenRegisteringNewTokenForSameDevice() { + // given + User user = UserFixture.GENERAL_USER.toUser(); + String oldToken = "oldToken"; + String newToken = "newToken"; + String deviceId = "device1"; + DeviceToken existingToken = DeviceToken.of(oldToken, deviceId, "Android", user); + existingToken.activate(); + + given(userRdbService.readUser(user.getId())).willReturn(Optional.of(user)); + given(deviceTokenRdbService.readDeviceByToken(newToken)).willReturn(Optional.empty()); + given(deviceTokenRdbService.readByUserIdAndDeviceId(user.getId(), deviceId)) + .willReturn(List.of(existingToken)); + given(deviceTokenRdbService.createDevice(any())).willAnswer(invocation -> invocation.getArgument(0)); + + // when + DeviceToken result = deviceTokenRegisterService.execute(user.getId(), deviceId, "Android", newToken); + + // then + assertFalse(existingToken.isActivated()); + assertTrue(result.isActivated()); + assertEquals(newToken, result.getToken()); + } +} From 9ab3006c5946d61418db84a0a358a52052f72197 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:41:11 +0900 Subject: [PATCH 14/20] fix: update device-token-register-service path in usecase and delete original service --- .../service/DeviceTokenRegisterService.java | 47 ------- .../users/usecase/UserAccountUseCase.java | 1 + .../DeviceTokenRegisterServiceTest.java | 125 ------------------ 3 files changed, 1 insertion(+), 172 deletions(-) delete mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/DeviceTokenRegisterService.java delete mode 100644 pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/DeviceTokenRegisterServiceTest.java diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/DeviceTokenRegisterService.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/DeviceTokenRegisterService.java deleted file mode 100644 index d48b9e91c..000000000 --- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/DeviceTokenRegisterService.java +++ /dev/null @@ -1,47 +0,0 @@ -package kr.co.pennyway.api.apis.users.service; - -import kr.co.pennyway.domain.context.account.service.DeviceTokenService; -import kr.co.pennyway.domain.context.account.service.UserService; -import kr.co.pennyway.domain.domains.device.domain.DeviceToken; -import kr.co.pennyway.domain.domains.user.domain.User; -import kr.co.pennyway.domain.domains.user.exception.UserErrorCode; -import kr.co.pennyway.domain.domains.user.exception.UserErrorException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class DeviceTokenRegisterService { - private final UserService userService; - private final DeviceTokenService deviceTokenService; - - @Transactional - public DeviceToken execute(Long userId, String deviceId, String deviceName, String token) { - User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND)); - - return getOrCreateDevice(user, deviceId, deviceName, token); - } - - /** - * 사용자의 디바이스 토큰을 가져오거나 생성한다. - *

- * 이미 등록된 디바이스 토큰인 경우 마지막 로그인 시간을 갱신한다. - */ - private DeviceToken getOrCreateDevice(User user, String deviceId, String deviceName, String token) { - Optional deviceToken = deviceTokenService.readDeviceTokenByUserIdAndToken(user.getId(), token); - - if (deviceToken.isPresent()) { - DeviceToken device = deviceToken.get(); - device.activate(); - device.updateLastSignedInAt(); - return device; - } else { - return deviceTokenService.createDeviceToken(DeviceToken.of(token, deviceId, deviceName, user)); - } - } -} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/usecase/UserAccountUseCase.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/usecase/UserAccountUseCase.java index 33f38ef8f..7fbbca104 100644 --- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/usecase/UserAccountUseCase.java +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/usecase/UserAccountUseCase.java @@ -8,6 +8,7 @@ import kr.co.pennyway.api.apis.users.service.*; import kr.co.pennyway.api.common.storage.AwsS3Adapter; import kr.co.pennyway.common.annotation.UseCase; +import kr.co.pennyway.domain.context.account.service.DeviceTokenRegisterService; import kr.co.pennyway.domain.domains.device.domain.DeviceToken; import kr.co.pennyway.domain.domains.oauth.domain.Oauth; import kr.co.pennyway.domain.domains.user.domain.NotifySetting; diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/DeviceTokenRegisterServiceTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/DeviceTokenRegisterServiceTest.java deleted file mode 100644 index 8b00c86cb..000000000 --- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/DeviceTokenRegisterServiceTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package kr.co.pennyway.api.apis.users.usecase; - -import kr.co.pennyway.api.apis.users.service.DeviceTokenRegisterService; -import kr.co.pennyway.api.config.fixture.DeviceTokenFixture; -import kr.co.pennyway.api.config.fixture.UserFixture; -import kr.co.pennyway.domain.context.account.service.DeviceTokenService; -import kr.co.pennyway.domain.context.account.service.UserService; -import kr.co.pennyway.domain.domains.device.domain.DeviceToken; -import kr.co.pennyway.domain.domains.user.domain.User; -import kr.co.pennyway.domain.domains.user.exception.UserErrorCode; -import kr.co.pennyway.domain.domains.user.exception.UserErrorException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -public class DeviceTokenRegisterServiceTest { - @InjectMocks - private DeviceTokenRegisterService deviceTokenRegisterService; - - @Mock - private UserService userService; - - @Mock - private DeviceTokenService deviceTokenService; - - private User requestUser; - - @BeforeEach - void setUp() { - requestUser = UserFixture.GENERAL_USER.toUser(); - } - - @Test - @DisplayName("token 등록 요청이 들어왔을 때, 새로운 디바이스 토큰을 등록한다.") - void registerNewDevice() { - // given - given(userService.readUser(requestUser.getId())).willReturn(Optional.of(requestUser)); - - DeviceToken expectedDeviceToken = DeviceTokenFixture.INIT.toDevice(requestUser); - given(deviceTokenService.readDeviceTokenByUserIdAndToken(requestUser.getId(), expectedDeviceToken.getToken())) - .willReturn(Optional.empty()); - given(deviceTokenService.createDeviceToken(any(DeviceToken.class))) - .willReturn(expectedDeviceToken); - - // when - DeviceToken response = deviceTokenRegisterService.execute(requestUser.getId(), expectedDeviceToken.getDeviceId(), expectedDeviceToken.getDeviceName(), expectedDeviceToken.getToken()); - - // then - verify(deviceTokenService).readDeviceTokenByUserIdAndToken(requestUser.getId(), expectedDeviceToken.getToken()); - verify(deviceTokenService).createDeviceToken(any(DeviceToken.class)); - - assertEquals(expectedDeviceToken.getToken(), response.getToken()); - assertEquals(requestUser.getId(), response.getUser().getId()); - } - - @Test - @DisplayName("token에 대한 활성화 디바이스 토큰이 이미 존재하는 경우 기존 데이터를 반환한다.") - void registerNewDeviceWhenDeviceIsAlreadyExists() { - // given - given(userService.readUser(requestUser.getId())).willReturn(Optional.of(requestUser)); - - DeviceToken originDeviceToken = DeviceTokenFixture.INIT.toDevice(requestUser); - given(deviceTokenService.readDeviceTokenByUserIdAndToken(requestUser.getId(), originDeviceToken.getToken())) - .willReturn(Optional.of(originDeviceToken)); - - // when - DeviceToken result = deviceTokenRegisterService.execute(requestUser.getId(), originDeviceToken.getDeviceId(), originDeviceToken.getDeviceName(), originDeviceToken.getToken()); - - // then - verify(deviceTokenService).readDeviceTokenByUserIdAndToken(requestUser.getId(), originDeviceToken.getToken()); - verify(deviceTokenService, never()).createDeviceToken(any(DeviceToken.class)); - - assertTrue(result.getActivated()); - assertNotNull(result.getLastSignedInAt()); - } - - @Test - @DisplayName("token 등록 요청이 들어왔을 때, 활성화되지 않은 디바이스 토큰이 존재하는 경우 토큰을 활성화 상태로 변경한다.") - void activateInactiveDevice() { - /// given - given(userService.readUser(requestUser.getId())).willReturn(Optional.of(requestUser)); - - DeviceToken inactiveToken = DeviceTokenFixture.INIT.toDevice(requestUser); - inactiveToken.deactivate(); - given(deviceTokenService.readDeviceTokenByUserIdAndToken(requestUser.getId(), inactiveToken.getToken())) - .willReturn(Optional.of(inactiveToken)); - - // when - DeviceToken result = deviceTokenRegisterService.execute(requestUser.getId(), inactiveToken.getDeviceId(), inactiveToken.getDeviceName(), inactiveToken.getToken()); - - // then - verify(deviceTokenService).readDeviceTokenByUserIdAndToken(requestUser.getId(), inactiveToken.getToken()); - verify(deviceTokenService, never()).createDeviceToken(any(DeviceToken.class)); - - assertTrue(result.getActivated()); - assertNotNull(result.getLastSignedInAt()); - } - - @Test - @DisplayName("존재하지 않는 사용자의 경우 예외를 발생시킨다") - void throwExceptionForNonExistentUser() { - // given - given(userService.readUser(999L)).willReturn(Optional.empty()); - - // when & then - UserErrorException exception = assertThrows(UserErrorException.class, - () -> deviceTokenRegisterService.execute(999L, "deviceId", "deviceName", "token")); - - assertEquals(UserErrorCode.NOT_FOUND, exception.getBaseErrorCode()); - verify(userService).readUser(999L); - verifyNoInteractions(deviceTokenService); - } -} From 97110b93e8af1286e6fdb027cbc4d490c79408f5 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:00:40 +0900 Subject: [PATCH 15/20] style: divide create and update logic --- .../service/DeviceTokenRegisterService.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java index 522ce2223..2aead673b 100644 --- a/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java +++ b/pennyway-domain/domain-service/src/main/java/kr/co/pennyway/domain/context/account/service/DeviceTokenRegisterService.java @@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Optional; @Slf4j @DomainService @@ -52,24 +51,26 @@ public DeviceToken execute(Long userId, String deviceId, String deviceName, Stri * 만약, 이미 등록된 디바이스 토큰이 존재한다면, 해당 토큰을 갱신하고 반환합니다. */ private DeviceToken getOrCreateDevice(User user, String deviceId, String deviceName, String deviceToken) { - Optional device = deviceTokenRdbService.readDeviceByToken(deviceToken); + return deviceTokenRdbService.readDeviceByToken(deviceToken) + .map(originalDeviceToken -> updateDevice(user, deviceId, originalDeviceToken)) + .orElseGet(() -> createDevice(user, deviceId, deviceName, deviceToken)); + } - if (device.isPresent()) { - DeviceToken deviceTokenEntity = device.get(); + private DeviceToken updateDevice(User user, String deviceId, DeviceToken originalDeviceToken) { + if (!originalDeviceToken.getDeviceId().equals(deviceId) && originalDeviceToken.isActivated()) { + throw new DeviceTokenErrorException(DeviceTokenErrorCode.DUPLICATED_DEVICE_TOKEN); + } - if (!deviceTokenEntity.getDeviceId().equals(deviceId) && deviceTokenEntity.isActivated()) { - throw new DeviceTokenErrorException(DeviceTokenErrorCode.DUPLICATED_DEVICE_TOKEN); - } + originalDeviceToken.handleOwner(user, deviceId); + return originalDeviceToken; + } - deviceTokenEntity.handleOwner(user, deviceId); - return deviceTokenEntity; - } else { - deactivateExistingTokens(user.getId(), deviceId); + private DeviceToken createDevice(User user, String deviceId, String deviceName, String deviceToken) { + deactivateExistingTokens(user.getId(), deviceId); - DeviceToken newDeviceToken = DeviceToken.of(deviceToken, deviceId, deviceName, user); + DeviceToken newDeviceToken = DeviceToken.of(deviceToken, deviceId, deviceName, user); - return deviceTokenRdbService.createDevice(newDeviceToken); - } + return deviceTokenRdbService.createDevice(newDeviceToken); } /** From df72e3b1f6de544af5fdeb883094505a4f15b320 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:27:35 +0900 Subject: [PATCH 16/20] chore: add testcontainer in domain-service module --- .../config/DomainServiceTestInfraConfig.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceTestInfraConfig.java diff --git a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceTestInfraConfig.java b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceTestInfraConfig.java new file mode 100644 index 000000000..c71a3b850 --- /dev/null +++ b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceTestInfraConfig.java @@ -0,0 +1,45 @@ +package kr.co.pennyway.domain.config; + +import com.redis.testcontainers.RedisContainer; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +public abstract class DomainServiceTestInfraConfig { + private static final String REDIS_CONTAINER_IMAGE = "redis:7.4"; + private static final String MYSQL_CONTAINER_IMAGE = "mysql:8.0.26"; + + private static final RedisContainer REDIS_CONTAINER; + private static final MySQLContainer MYSQL_CONTAINER; + + static { + REDIS_CONTAINER = + new RedisContainer(DockerImageName.parse(REDIS_CONTAINER_IMAGE)) + .withExposedPorts(6379) + .withCommand("redis-server", "--requirepass testpass") + .withReuse(true); + MYSQL_CONTAINER = + new MySQLContainer<>(DockerImageName.parse(MYSQL_CONTAINER_IMAGE)) + .withDatabaseName("pennyway") + .withUsername("root") + .withPassword("testpass") + .withCommand("--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION") + .withReuse(true); + + REDIS_CONTAINER.start(); + MYSQL_CONTAINER.start(); + } + + @DynamicPropertySource + public static void setRedisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost); + registry.add("spring.data.redis.port", () -> String.valueOf(REDIS_CONTAINER.getMappedPort(6379))); + registry.add("spring.data.redis.password", () -> "testpass"); + registry.add("spring.datasource.url", () -> String.format("jdbc:mysql://%s:%s/pennyway?serverTimezone=Asia/Seoul&characterEncoding=utf8&postfileSQL=true&logger=Slf4JLogger&rewriteBatchedStatements=true", MYSQL_CONTAINER.getHost(), MYSQL_CONTAINER.getMappedPort(3306))); + registry.add("spring.datasource.username", () -> "root"); + registry.add("spring.datasource.password", () -> "testpass"); + } +} From 61dcebad34226c2ed3cd77b99401f5ae36932981 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:27:56 +0900 Subject: [PATCH 17/20] chore: add profile-resolver in domain-service module --- .../DomainServiceIntegrationProfileResolver.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java diff --git a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java new file mode 100644 index 000000000..9b2451670 --- /dev/null +++ b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java @@ -0,0 +1,12 @@ +package kr.co.pennyway.domain.config; + +import org.springframework.lang.NonNull; +import org.springframework.test.context.ActiveProfilesResolver; + +public class DomainServiceIntegrationProfileResolver implements ActiveProfilesResolver { + @Override + @NonNull + public String[] resolve(@NonNull Class testClass) { + return new String[]{"common", "infra", "domain-rdb", "domain-redis"}; + } +} From 8d090eb9494dfbc8c35e49b41bfb73c26976db89 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:27:38 +0900 Subject: [PATCH 18/20] chore: add jpa-test-config in test package in the domain-service-module --- .../pennyway/domain/config/JpaTestConfig.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/JpaTestConfig.java diff --git a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/JpaTestConfig.java b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/JpaTestConfig.java new file mode 100644 index 000000000..da6585354 --- /dev/null +++ b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/JpaTestConfig.java @@ -0,0 +1,27 @@ +package kr.co.pennyway.domain.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.querydsl.sql.MySQLTemplates; +import com.querydsl.sql.SQLTemplates; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@TestConfiguration +@EnableJpaAuditing +public class JpaTestConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } + + @Bean + public SQLTemplates sqlTemplates() { + return new MySQLTemplates(); + } +} \ No newline at end of file From 37657b55966139c88d96f6f61f8f614414b06214 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:43:00 +0900 Subject: [PATCH 19/20] fix: add 'test' profile in domain-service-integration-profile-resolver --- .../domain/config/DomainServiceIntegrationProfileResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java index 9b2451670..cdd3a1f9d 100644 --- a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java +++ b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/config/DomainServiceIntegrationProfileResolver.java @@ -7,6 +7,6 @@ public class DomainServiceIntegrationProfileResolver implements ActiveProfilesRe @Override @NonNull public String[] resolve(@NonNull Class testClass) { - return new String[]{"common", "infra", "domain-rdb", "domain-redis"}; + return new String[]{"test", "common", "infra", "domain-rdb", "domain-redis"}; } } From 14db037161ae8bf33b7781ee8adccfbc8999158c Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:48:26 +0900 Subject: [PATCH 20/20] test: device-token-register-service integration test --- ...ceTokenRegisterServiceIntegrationTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/integration/DeviceTokenRegisterServiceIntegrationTest.java diff --git a/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/integration/DeviceTokenRegisterServiceIntegrationTest.java b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/integration/DeviceTokenRegisterServiceIntegrationTest.java new file mode 100644 index 000000000..d9383d712 --- /dev/null +++ b/pennyway-domain/domain-service/src/test/java/kr/co/pennyway/domain/context/account/integration/DeviceTokenRegisterServiceIntegrationTest.java @@ -0,0 +1,106 @@ +package kr.co.pennyway.domain.context.account.integration; + +import kr.co.pennyway.domain.common.repository.ExtendedRepositoryFactory; +import kr.co.pennyway.domain.config.DomainServiceIntegrationProfileResolver; +import kr.co.pennyway.domain.config.DomainServiceTestInfraConfig; +import kr.co.pennyway.domain.config.JpaTestConfig; +import kr.co.pennyway.domain.context.account.service.DeviceTokenRegisterService; +import kr.co.pennyway.domain.context.common.fixture.UserFixture; +import kr.co.pennyway.domain.domains.device.domain.DeviceToken; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorCode; +import kr.co.pennyway.domain.domains.device.exception.DeviceTokenErrorException; +import kr.co.pennyway.domain.domains.device.repository.DeviceTokenRepository; +import kr.co.pennyway.domain.domains.device.service.DeviceTokenRdbService; +import kr.co.pennyway.domain.domains.user.domain.User; +import kr.co.pennyway.domain.domains.user.repository.UserRepository; +import kr.co.pennyway.domain.domains.user.service.UserRdbService; +import lombok.extern.slf4j.Slf4j; +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.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@EnableAutoConfiguration +@SpringBootTest(classes = {DeviceTokenRegisterService.class, UserRdbService.class, DeviceTokenRdbService.class}) +@EntityScan(basePackageClasses = {User.class, DeviceToken.class}) +@EnableJpaRepositories(basePackageClasses = {UserRepository.class, DeviceTokenRepository.class}, repositoryFactoryBeanClass = ExtendedRepositoryFactory.class) +@ActiveProfiles(resolver = DomainServiceIntegrationProfileResolver.class) +@Import(value = {JpaTestConfig.class}) +public class DeviceTokenRegisterServiceIntegrationTest extends DomainServiceTestInfraConfig { + @Autowired + private DeviceTokenRegisterService deviceTokenRegisterService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private DeviceTokenRepository deviceTokenRepository; + + private User savedUser; + + @BeforeEach + void setUp() { + savedUser = userRepository.save(UserFixture.GENERAL_USER.toUser()); + } + + @Test + @Transactional + @DisplayName("디바이스 토큰 등록 시 기존 활성 토큰은 비활성화됩니다") + void shouldDeactivateExistingTokensWhenRegisteringNew() { + // given + String deviceId = "device1"; + + // when + DeviceToken firstToken = deviceTokenRegisterService.execute(savedUser.getId(), deviceId, "Android", "token1"); + DeviceToken secondToken = deviceTokenRegisterService.execute(savedUser.getId(), deviceId, "Android", "token2"); + + // then + assertFalse(firstToken.isActivated()); + assertTrue(secondToken.isActivated()); + } + + @Test + @Transactional + @DisplayName("활성화된 토큰이 다른 디바이스에서 사용되면 예외가 발생합니다") + void shouldThrowExceptionWhenActiveTokenIsUsedOnDifferentDevice() { + // given + String token = "token1"; + deviceTokenRegisterService.execute(savedUser.getId(), "device1", "Android", token); + + // when & then + DeviceTokenErrorException exception = assertThrowsExactly( + DeviceTokenErrorException.class, + () -> deviceTokenRegisterService.execute(savedUser.getId(), "device2", "iPhone", token) + ); + assertEquals(DeviceTokenErrorCode.DUPLICATED_DEVICE_TOKEN, exception.getBaseErrorCode()); + } + + @Test + @DisplayName("같은 deviceId, token / 다른 사용자 갱신 요청이라면, 디바이스 토큰의 소유권이 다른 사용자에게 이전됩니다") + void shouldTransferTokenOwnership() { + // given + User anotherUser = userRepository.save(UserFixture.GENERAL_USER.toUser()); + + String deviceId = "device1"; + String token = "token1"; + + // when + DeviceToken firstUserToken = deviceTokenRegisterService.execute(savedUser.getId(), deviceId, "Android", token); + DeviceToken secondUserToken = deviceTokenRegisterService.execute(anotherUser.getId(), deviceId, "Android", token); + + // then + assertEquals(firstUserToken.getId(), secondUserToken.getId()); + assertEquals(anotherUser.getId(), secondUserToken.getUser().getId()); + assertTrue(secondUserToken.isActivated()); + } +}