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());
+ }
+}