diff --git a/backend/README.md b/backend/README.md index 53b0fa6df..9f127ecf8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -51,7 +51,7 @@ backend │   │   │   ├── request │   │   │   └── response │   │   ├── service -│   │   └── token +│   │   └── jwtToken │   ├── common │   │   ├── annotation │   │   ├── aop diff --git a/backend/src/main/java/com/ody/auth/JwtTokenProvider.java b/backend/src/main/java/com/ody/auth/JwtTokenProvider.java index ef9a14341..56a34cd24 100644 --- a/backend/src/main/java/com/ody/auth/JwtTokenProvider.java +++ b/backend/src/main/java/com/ody/auth/JwtTokenProvider.java @@ -1,6 +1,7 @@ package com.ody.auth; import com.ody.auth.token.AccessToken; +import com.ody.auth.token.JwtToken; import com.ody.auth.token.RefreshToken; import com.ody.common.exception.OdyBadRequestException; import com.ody.common.exception.OdyUnauthorizedException; @@ -9,20 +10,18 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; @Getter @Component +@RequiredArgsConstructor @EnableConfigurationProperties(AuthProperties.class) public class JwtTokenProvider { private final AuthProperties authProperties; - public JwtTokenProvider(AuthProperties authProperties) { - this.authProperties = authProperties; - } - public AccessToken createAccessToken(long memberId) { return new AccessToken(memberId, authProperties); } @@ -45,36 +44,17 @@ public long parseAccessToken(AccessToken accessToken) { } } - public void validate(AccessToken accessToken) { - if (!isUnexpired(accessToken)) { - throw new OdyUnauthorizedException("만료된 액세스 토큰입니다."); - } - } - - public void validate(RefreshToken refreshToken) { - if (!isUnexpired(refreshToken)) { - throw new OdyUnauthorizedException("만료된 리프레시 토큰입니다."); - } - } - - public boolean isUnexpired(AccessToken accessToken) { - try { - Jwts.parser() - .setSigningKey(authProperties.getAccessKey()) - .parseClaimsJws(accessToken.getValue()); - return true; - } catch (ExpiredJwtException exception) { - return false; - } catch (JwtException exception) { - throw new OdyBadRequestException(exception.getMessage()); + public void validate(JwtToken jwtToken) { + if (!isUnexpired(jwtToken)) { + throw new OdyUnauthorizedException("만료된 토큰입니다."); } } - public boolean isUnexpired(RefreshToken refreshToken) { + public boolean isUnexpired(JwtToken jwtToken) { try { Jwts.parser() - .setSigningKey(authProperties.getRefreshKey()) - .parseClaimsJws(refreshToken.getValue()); + .setSigningKey(jwtToken.getSecretKey(authProperties)) + .parseClaimsJws(jwtToken.getValue()); return true; } catch (ExpiredJwtException exception) { return false; diff --git a/backend/src/main/java/com/ody/auth/domain/Authorizer.java b/backend/src/main/java/com/ody/auth/domain/Authorizer.java new file mode 100644 index 000000000..ec9744110 --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/Authorizer.java @@ -0,0 +1,30 @@ +package com.ody.auth.domain; + +import com.ody.auth.domain.authpolicy.AuthPolicy; +import com.ody.common.exception.OdyUnauthorizedException; +import com.ody.member.domain.Member; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class Authorizer { + + private final List authPolicies; + + @Transactional + public Member authorize( + Optional sameDeviceMember, + Optional samePidMember, + Member requestMember + ) { + return authPolicies.stream() + .filter(type -> type.match(sameDeviceMember, samePidMember, requestMember)) + .findAny() + .orElseThrow(() -> new OdyUnauthorizedException("잘못된 인증 요청입니다.")) + .authorize(sameDeviceMember, samePidMember, requestMember); + } +} diff --git a/backend/src/main/java/com/ody/auth/domain/authpolicy/AuthPolicy.java b/backend/src/main/java/com/ody/auth/domain/authpolicy/AuthPolicy.java new file mode 100644 index 000000000..c6d13998d --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/authpolicy/AuthPolicy.java @@ -0,0 +1,19 @@ +package com.ody.auth.domain.authpolicy; + +import com.ody.member.domain.Member; +import java.util.Optional; + +public interface AuthPolicy { + + boolean match( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ); + + Member authorize( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ); +} diff --git a/backend/src/main/java/com/ody/auth/domain/authpolicy/ExistingUserForExistingDevice.java b/backend/src/main/java/com/ody/auth/domain/authpolicy/ExistingUserForExistingDevice.java new file mode 100644 index 000000000..319877b41 --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/authpolicy/ExistingUserForExistingDevice.java @@ -0,0 +1,21 @@ +package com.ody.auth.domain.authpolicy; + +import com.ody.member.domain.Member; +import java.util.Optional; +import org.springframework.stereotype.Component; + +@Component +public class ExistingUserForExistingDevice implements AuthPolicy { + + @Override + public boolean match(Optional sameDeviceMember, Optional sameProviderIdMember, Member requestMember) { + return sameDeviceMember.isPresent() + && sameProviderIdMember.isPresent() + && requestMember.isSame(sameDeviceMember.get()); + } + + @Override + public Member authorize(Optional sameDeviceMember, Optional sameProviderIdMember, Member requestMember) { + return sameProviderIdMember.get(); + } +} diff --git a/backend/src/main/java/com/ody/auth/domain/authpolicy/ExistingUserForNewDevice.java b/backend/src/main/java/com/ody/auth/domain/authpolicy/ExistingUserForNewDevice.java new file mode 100644 index 000000000..efb9d3158 --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/authpolicy/ExistingUserForNewDevice.java @@ -0,0 +1,26 @@ +package com.ody.auth.domain.authpolicy; + +import com.ody.member.domain.Member; +import java.util.Optional; +import org.springframework.stereotype.Component; + +@Component +public class ExistingUserForNewDevice implements AuthPolicy { + + @Override + public boolean match( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + return sameDeviceMember.isEmpty() + && sameProviderIdMember.isPresent(); + } + + @Override + public Member authorize(Optional sameDeviceMember, Optional sameProviderIdMember, Member requestMember) { + Member sameAuthProviderMember = sameProviderIdMember.get(); + sameAuthProviderMember.updateDeviceToken(requestMember.getDeviceToken()); + return sameAuthProviderMember; + } +} diff --git a/backend/src/main/java/com/ody/auth/domain/authpolicy/NewUserForExistingDevice.java b/backend/src/main/java/com/ody/auth/domain/authpolicy/NewUserForExistingDevice.java new file mode 100644 index 000000000..c5b913a1a --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/authpolicy/NewUserForExistingDevice.java @@ -0,0 +1,29 @@ +package com.ody.auth.domain.authpolicy; + +import com.ody.member.domain.Member; +import java.util.Optional; +import org.springframework.stereotype.Component; + +@Component +public class NewUserForExistingDevice implements AuthPolicy { + + @Override + public boolean match( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + return sameDeviceMember.isPresent() + && sameProviderIdMember.isEmpty(); + } + + @Override + public Member authorize( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + sameDeviceMember.get().updateDeviceTokenNull(); + return requestMember; + } +} diff --git a/backend/src/main/java/com/ody/auth/domain/authpolicy/NewUserForNewDevice.java b/backend/src/main/java/com/ody/auth/domain/authpolicy/NewUserForNewDevice.java new file mode 100644 index 000000000..f7665d7ed --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/authpolicy/NewUserForNewDevice.java @@ -0,0 +1,28 @@ +package com.ody.auth.domain.authpolicy; + +import com.ody.member.domain.Member; +import java.util.Optional; +import org.springframework.stereotype.Component; + +@Component +public class NewUserForNewDevice implements AuthPolicy { + + @Override + public boolean match( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + return sameDeviceMember.isEmpty() + && sameProviderIdMember.isEmpty(); + } + + @Override + public Member authorize( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + return requestMember; + } +} diff --git a/backend/src/main/java/com/ody/auth/domain/authpolicy/OtherUserForExistingDevice.java b/backend/src/main/java/com/ody/auth/domain/authpolicy/OtherUserForExistingDevice.java new file mode 100644 index 000000000..9072c078e --- /dev/null +++ b/backend/src/main/java/com/ody/auth/domain/authpolicy/OtherUserForExistingDevice.java @@ -0,0 +1,31 @@ +package com.ody.auth.domain.authpolicy; + +import com.ody.member.domain.Member; +import java.util.Optional; +import org.springframework.stereotype.Component; + +@Component +public class OtherUserForExistingDevice implements AuthPolicy { + + @Override + public boolean match( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + return sameDeviceMember.isPresent() + && sameProviderIdMember.isPresent() + && !requestMember.isSame(sameDeviceMember.get()); + } + + @Override + public Member authorize( + Optional sameDeviceMember, + Optional sameProviderIdMember, + Member requestMember + ) { + sameDeviceMember.get().updateDeviceTokenNull(); + sameProviderIdMember.get().updateDeviceToken(requestMember.getDeviceToken()); + return sameProviderIdMember.get(); + } +} diff --git a/backend/src/main/java/com/ody/auth/service/AuthService.java b/backend/src/main/java/com/ody/auth/service/AuthService.java index 0440af849..27e414545 100644 --- a/backend/src/main/java/com/ody/auth/service/AuthService.java +++ b/backend/src/main/java/com/ody/auth/service/AuthService.java @@ -2,6 +2,7 @@ import com.ody.auth.JwtTokenProvider; import com.ody.auth.domain.AuthorizationHeader; +import com.ody.auth.domain.Authorizer; import com.ody.auth.dto.request.AuthRequest; import com.ody.auth.dto.response.AuthResponse; import com.ody.auth.token.AccessToken; @@ -9,18 +10,20 @@ import com.ody.common.exception.OdyBadRequestException; import com.ody.member.domain.Member; import com.ody.member.service.MemberService; -import lombok.AllArgsConstructor; +import java.util.Optional; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Slf4j @Service -@AllArgsConstructor +@RequiredArgsConstructor public class AuthService { private final JwtTokenProvider jwtTokenProvider; private final MemberService memberService; + private final Authorizer authorizer; public Member parseAccessToken(String rawAccessToken) { AccessToken accessToken = new AccessToken(rawAccessToken); @@ -31,8 +34,16 @@ public Member parseAccessToken(String rawAccessToken) { @Transactional public AuthResponse issueTokens(AuthRequest authRequest) { - Member member = memberService.save(authRequest.toMember()); - return issueNewTokens(member.getId()); + Member requestMember = authRequest.toMember(); + Member authorizedMember = findAuthroizedMember(requestMember); + Member savedAuthorizedMember = memberService.save(authorizedMember) ; + return issueNewTokens(savedAuthorizedMember.getId()); + } + + private Member findAuthroizedMember(Member requestMember) { + Optional sameDeviceMember = memberService.findByDeviceToken(requestMember.getDeviceToken()); + Optional samePidMember = memberService.findByAuthProvider(requestMember.getAuthProvider()); + return authorizer.authorize(sameDeviceMember, samePidMember, requestMember); } @Transactional @@ -69,7 +80,6 @@ private AuthResponse issueNewTokens(long memberId) { public void logout(String rawAccessTokenValue) { AccessToken accessToken = new AccessToken(rawAccessTokenValue); jwtTokenProvider.validate(accessToken); - long memberId = jwtTokenProvider.parseAccessToken(accessToken); memberService.updateRefreshToken(memberId, null); } diff --git a/backend/src/main/java/com/ody/auth/token/AccessToken.java b/backend/src/main/java/com/ody/auth/token/AccessToken.java index 54603be2c..7c04976fe 100644 --- a/backend/src/main/java/com/ody/auth/token/AccessToken.java +++ b/backend/src/main/java/com/ody/auth/token/AccessToken.java @@ -8,10 +8,9 @@ import lombok.Getter; @Getter -public class AccessToken { +public class AccessToken implements JwtToken { private static final String ACCESS_TOKEN_PREFIX = "Bearer access-token="; - public static final String DELIMITER = " "; private final String value; @@ -38,4 +37,9 @@ private void validate(String value) { private String parseAccessToken(String rawValue) { return rawValue.substring(ACCESS_TOKEN_PREFIX.length()).trim(); } + + @Override + public String getSecretKey(AuthProperties authProperties) { + return authProperties.getAccessKey(); + } } diff --git a/backend/src/main/java/com/ody/auth/token/JwtToken.java b/backend/src/main/java/com/ody/auth/token/JwtToken.java new file mode 100644 index 000000000..321d97238 --- /dev/null +++ b/backend/src/main/java/com/ody/auth/token/JwtToken.java @@ -0,0 +1,10 @@ +package com.ody.auth.token; + +import com.ody.auth.AuthProperties; + +public interface JwtToken { + + String getSecretKey(AuthProperties authProperties); + + String getValue(); +} diff --git a/backend/src/main/java/com/ody/auth/token/RefreshToken.java b/backend/src/main/java/com/ody/auth/token/RefreshToken.java index d2596c183..6f0e2fbda 100644 --- a/backend/src/main/java/com/ody/auth/token/RefreshToken.java +++ b/backend/src/main/java/com/ody/auth/token/RefreshToken.java @@ -15,7 +15,7 @@ @Getter @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RefreshToken { +public class RefreshToken implements JwtToken { public static final String REFRESH_TOKEN_PREFIX = "refresh-token="; @@ -45,6 +45,11 @@ public RefreshToken(AuthProperties authProperties) { .compact(); } + @Override + public String getSecretKey(AuthProperties authProperties) { + return authProperties.getRefreshKey(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/backend/src/main/java/com/ody/member/domain/Member.java b/backend/src/main/java/com/ody/member/domain/Member.java index c9d63972b..a4e5065d7 100644 --- a/backend/src/main/java/com/ody/member/domain/Member.java +++ b/backend/src/main/java/com/ody/member/domain/Member.java @@ -58,8 +58,8 @@ public boolean isLogout() { return this.refreshToken == null; } - public boolean isSame(AuthProvider otherAuthProvider) { - return this.authProvider.equals(otherAuthProvider); + public boolean isSame(Member otherMember) { + return this.authProvider.equals(otherMember.getAuthProvider()); } public void updateRefreshToken(RefreshToken refreshToken) { diff --git a/backend/src/main/java/com/ody/member/service/MemberService.java b/backend/src/main/java/com/ody/member/service/MemberService.java index 0d5a07545..6fd494b1a 100644 --- a/backend/src/main/java/com/ody/member/service/MemberService.java +++ b/backend/src/main/java/com/ody/member/service/MemberService.java @@ -4,6 +4,8 @@ import com.ody.auth.token.RefreshToken; import com.ody.common.exception.OdyUnauthorizedException; import com.ody.mate.service.MateService; +import com.ody.member.domain.AuthProvider; +import com.ody.member.domain.DeviceToken; import com.ody.member.domain.Member; import com.ody.member.repository.MemberRepository; import java.util.Optional; @@ -21,27 +23,8 @@ public class MemberService { private final SocialAuthUnlinkClient socialAuthUnlinkClient; @Transactional - public Member save(Member requestMember) { - Optional findMember = memberRepository.findByDeviceToken(requestMember.getDeviceToken()); - - if (findMember.isPresent()) { - Member sameDeviceTokenMember = findMember.get(); - if (sameDeviceTokenMember.isSame(requestMember.getAuthProvider())) { - return sameDeviceTokenMember; - } - sameDeviceTokenMember.updateDeviceTokenNull(); - } - return saveOrUpdateByAuthProvider(requestMember); - } - - private Member saveOrUpdateByAuthProvider(Member requestMember) { - Optional findMember = memberRepository.findByAuthProvider(requestMember.getAuthProvider()); - if (findMember.isPresent()) { - Member sameAuthProviderMember = findMember.get(); - sameAuthProviderMember.updateDeviceToken(requestMember.getDeviceToken()); - return sameAuthProviderMember; - } - return memberRepository.save(requestMember); + public Member save(Member member) { + return memberRepository.save(member); } public Member findById(Long memberId) { @@ -50,6 +33,14 @@ public Member findById(Long memberId) { .orElseThrow(() -> new OdyUnauthorizedException("존재하지 않는 회원입니다.")); } + public Optional findByDeviceToken(DeviceToken deviceToken) { + return memberRepository.findByDeviceToken(deviceToken); + } + + public Optional findByAuthProvider(AuthProvider authProvider) { + return memberRepository.findByAuthProvider(authProvider); + } + @Transactional public void updateRefreshToken(long memberId, RefreshToken refreshToken) { Member member = findById(memberId); diff --git a/backend/src/test/java/com/ody/auth/domain/authorizeType/ExistingUserForExistingDeviceTest.java b/backend/src/test/java/com/ody/auth/domain/authorizeType/ExistingUserForExistingDeviceTest.java new file mode 100644 index 000000000..229c8f4a3 --- /dev/null +++ b/backend/src/test/java/com/ody/auth/domain/authorizeType/ExistingUserForExistingDeviceTest.java @@ -0,0 +1,38 @@ +package com.ody.auth.domain.authorizeType; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ody.auth.domain.authpolicy.ExistingUserForExistingDevice; +import com.ody.mate.domain.Nickname; +import com.ody.member.domain.DeviceToken; +import com.ody.member.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ExistingUserForExistingDeviceTest { + + @DisplayName("기존 동일 회원의 로그인 요청 시 조건이 만족된다") + @Test + void match() { + ExistingUserForExistingDevice authorizationType = new ExistingUserForExistingDevice(); + Member member = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("device-token")); + + boolean matched = authorizationType.match(Optional.of(member), Optional.of(member), member); + + assertThat(matched).isTrue(); + } + + @DisplayName("기존 동일 회원을 그대로 반환한다") + @Test + void authorize() { + ExistingUserForExistingDevice authorizationType = new ExistingUserForExistingDevice(); + Member member = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("device-token")); + + Member authorizedMember = authorizationType.authorize(Optional.of(member), Optional.of(member), member); + + assertThat(authorizedMember) + .usingRecursiveComparison() + .isEqualTo(member); + } +} diff --git a/backend/src/test/java/com/ody/auth/domain/authorizeType/ExistingUserForNewDeviceTest.java b/backend/src/test/java/com/ody/auth/domain/authorizeType/ExistingUserForNewDeviceTest.java new file mode 100644 index 000000000..1e59823bb --- /dev/null +++ b/backend/src/test/java/com/ody/auth/domain/authorizeType/ExistingUserForNewDeviceTest.java @@ -0,0 +1,44 @@ +package com.ody.auth.domain.authorizeType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.ody.auth.domain.authpolicy.ExistingUserForNewDevice; +import com.ody.mate.domain.Nickname; +import com.ody.member.domain.DeviceToken; +import com.ody.member.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ExistingUserForNewDeviceTest { + + @DisplayName("기존 유저가 새로운 기기로 접속했을 때 : 동일 기기 사용자 X, 동일 pid 사용자 O") + @Test + void match() { + ExistingUserForNewDevice authorizationType = new ExistingUserForNewDevice(); + Member member = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + Member requestMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("other_dt")); + + boolean matched = authorizationType.match(Optional.empty(), Optional.of(member), requestMember); + + assertThat(matched).isTrue(); + } + + @DisplayName("기존 유저의 디바이스 토큰을 새로운 디바이스 토큰으로 업데이트 한다") + @Test + void authorize() { + ExistingUserForNewDevice authorizationType = new ExistingUserForNewDevice(); + Member member = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + Member requestMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("other_dt")); + + Member authorizedMember = authorizationType.authorize(Optional.empty(), Optional.of(member), requestMember); + + assertAll( + () -> assertThat(authorizedMember.getDeviceToken().getValue()).isEqualTo("other_dt"), + () -> assertThat(authorizedMember) + .usingRecursiveComparison() + .isEqualTo(requestMember) + ); + } +} diff --git a/backend/src/test/java/com/ody/auth/domain/authorizeType/NewUserForExistingDeviceTest.java b/backend/src/test/java/com/ody/auth/domain/authorizeType/NewUserForExistingDeviceTest.java new file mode 100644 index 000000000..f6b873fb4 --- /dev/null +++ b/backend/src/test/java/com/ody/auth/domain/authorizeType/NewUserForExistingDeviceTest.java @@ -0,0 +1,52 @@ +package com.ody.auth.domain.authorizeType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.ody.auth.domain.authpolicy.NewUserForExistingDevice; +import com.ody.mate.domain.Nickname; +import com.ody.member.domain.DeviceToken; +import com.ody.member.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class NewUserForExistingDeviceTest { + + @DisplayName("신규 유저가 로그인 이력이 있는 기기로 로그인 시 : 동일 기기 사용자 O , 동일 pid 사용자 X") + @Test + void match() { + NewUserForExistingDevice authorizationType = new NewUserForExistingDevice(); + Member originalMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + Member requestMember = new Member("pid2", new Nickname("조조"), "imgUrl2", new DeviceToken("dt")); + + boolean matched = authorizationType.match( + Optional.of(originalMember), + Optional.empty(), + requestMember + ); + + assertThat(matched).isTrue(); + } + + @DisplayName("기존 기기 사용자의 디바이스 토큰을 null로 처리한다") + @Test + void authorize() { + NewUserForExistingDevice authorizationType = new NewUserForExistingDevice(); + Member originalMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + Member requestMember = new Member("pid2", new Nickname("조조"), "imgUrl2", new DeviceToken("dt")); + + Member authorizedMember = authorizationType.authorize( + Optional.of(originalMember), + Optional.empty(), + requestMember + ); + + assertAll( + () -> assertThat(originalMember.getDeviceToken()).isNull(), + () -> assertThat(authorizedMember) + .usingRecursiveComparison() + .isEqualTo(requestMember) + ); + } +} diff --git a/backend/src/test/java/com/ody/auth/domain/authorizeType/NewUserForNewDeviceTest.java b/backend/src/test/java/com/ody/auth/domain/authorizeType/NewUserForNewDeviceTest.java new file mode 100644 index 000000000..2815af1a0 --- /dev/null +++ b/backend/src/test/java/com/ody/auth/domain/authorizeType/NewUserForNewDeviceTest.java @@ -0,0 +1,46 @@ +package com.ody.auth.domain.authorizeType; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ody.auth.domain.authpolicy.NewUserForNewDevice; +import com.ody.mate.domain.Nickname; +import com.ody.member.domain.DeviceToken; +import com.ody.member.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class NewUserForNewDeviceTest { + + @DisplayName("신규 유저가 신규 기기로 로그인을 시도할 때 : 동일 기기 사용자 X, 동일 pid 사용자 X") + @Test + void match() { + NewUserForNewDevice authorizationType = new NewUserForNewDevice(); + Member requestMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + + boolean matched = authorizationType.match( + Optional.empty(), + Optional.empty(), + requestMember + ); + + assertThat(matched).isTrue(); + } + + @DisplayName("신규 유저를 인증 대상으로 반환한다") + @Test + void authorize() { + NewUserForNewDevice authorizationType = new NewUserForNewDevice(); + Member requestMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + + Member authorizedMember = authorizationType.authorize( + Optional.empty(), + Optional.empty(), + requestMember + ); + + assertThat(authorizedMember) + .usingRecursiveComparison() + .isEqualTo(requestMember); + } +} diff --git a/backend/src/test/java/com/ody/auth/domain/authorizeType/OtherUserForExistingDeviceTest.java b/backend/src/test/java/com/ody/auth/domain/authorizeType/OtherUserForExistingDeviceTest.java new file mode 100644 index 000000000..6cf23b207 --- /dev/null +++ b/backend/src/test/java/com/ody/auth/domain/authorizeType/OtherUserForExistingDeviceTest.java @@ -0,0 +1,53 @@ +package com.ody.auth.domain.authorizeType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.ody.auth.domain.authpolicy.OtherUserForExistingDevice; +import com.ody.mate.domain.Nickname; +import com.ody.member.domain.DeviceToken; +import com.ody.member.domain.Member; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class OtherUserForExistingDeviceTest { + + @DisplayName("로그인 이력이 있는 기기로 다른 사용자가 로그인 시도 시 : 동일 기기 사용자 != 동일 pid 사용자") + @Test + void match() { + OtherUserForExistingDevice authorizationType = new OtherUserForExistingDevice(); + Member originalMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + Member requestMember = new Member("pid2", new Nickname("제리"), "imgUrl2", new DeviceToken("dt")); + + boolean matched = authorizationType.match( + Optional.of(originalMember), + Optional.of(requestMember), + requestMember + ); + + assertThat(matched).isTrue(); + } + + + @DisplayName("기존 기기 사용자의 디바이스 토큰을 null로 업데이트하고, 로그인 요청 사용자의 기기 토큰을 요청한 기기 토큰으로 업데이트 한다") + @Test + void authorize() { + OtherUserForExistingDevice authorizationType = new OtherUserForExistingDevice(); + Member originalMember = new Member("pid", new Nickname("콜리"), "imgUrl", new DeviceToken("dt")); + Member requestMember = new Member("pid2", new Nickname("제리"), "imgUrl2", new DeviceToken("dt")); + + Member authroziedMember = authorizationType.authorize( + Optional.of(originalMember), + Optional.of(requestMember), + requestMember + ); + + assertAll( + () -> assertThat(originalMember.getDeviceToken()).isNull(), + () -> assertThat(authroziedMember) + .usingRecursiveComparison() + .isEqualTo(requestMember) + ); + } +} diff --git a/backend/src/test/java/com/ody/auth/service/AuthServiceTest.java b/backend/src/test/java/com/ody/auth/service/AuthServiceTest.java index f09b139cb..69783d22d 100644 --- a/backend/src/test/java/com/ody/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/ody/auth/service/AuthServiceTest.java @@ -1,14 +1,17 @@ package com.ody.auth.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import com.ody.auth.dto.request.AuthRequest; import com.ody.auth.token.AccessToken; import com.ody.auth.token.RefreshToken; import com.ody.common.BaseServiceTest; import com.ody.common.TokenFixture; -import com.ody.common.exception.OdyBadRequestException; import com.ody.common.exception.OdyUnauthorizedException; +import com.ody.member.domain.AuthProvider; import com.ody.member.domain.DeviceToken; import com.ody.member.domain.Member; import com.ody.member.repository.MemberRepository; @@ -25,6 +28,85 @@ class AuthServiceTest extends BaseServiceTest { @Autowired private MemberRepository memberRepository; + @DisplayName("멤버 인증 테스트") + @Nested + class AuthTest { + + @DisplayName("로그인 이력이 있는 기기로 비회원이 회원 생성을 시도하면 기기 이력을 삭제하고 회원을 생성한다.") + @Test + void saveMemberWhenNonMemberAttemptsWithLoggedInDevice() { + fixtureGenerator.generateSavedMember("pid", "deviceToken"); + Member sameDeivceFreshMember = fixtureGenerator.generateUnsavedMember("newPid", "deviceToken"); + AuthRequest sameDeviceFreshMemberRequest = dtoGenerator.generateAuthRequest(sameDeivceFreshMember); + + authService.issueTokens(sameDeviceFreshMemberRequest); + + assertAll( + () -> assertThat(getDeviceTokenByAuthProvider("pid")).isNull(), + () -> assertThat(getDeviceTokenByAuthProvider("newPid").getValue()).isEqualTo("deviceToken") + ); + } + + @DisplayName("로그인 이력이 있는 기기로 동일 회원이 회원 생성을 시도하면 회원을 생성하지 않는다.") + @Test + void saveMemberWhenMemberAttemptsWithLoggedInDevice() { + fixtureGenerator.generateSavedMember("pid", "deviceToken"); + Member sameMember = fixtureGenerator.generateUnsavedMember("pid", "deviceToken"); + AuthRequest sameMemberRequest = dtoGenerator.generateAuthRequest(sameMember); + + authService.issueTokens(sameMemberRequest); + + assertThat(getDeviceTokenByAuthProvider("pid").getValue()).isEqualTo("deviceToken"); + } + + @DisplayName("로그인 이력이 있는 기기로 타 회원이 회원 생성을 시도하면 기기 이력을 이전한다.") + @Test + void saveMemberWhenOtherMemberAttemptsWithLoggedInDevice() { + fixtureGenerator.generateSavedMember("pid", "deviceToken"); + fixtureGenerator.generateSavedMember("otherPid", "otherDeviceToken"); + Member otherPidSameDeviceUser = fixtureGenerator.generateUnsavedMember("otherPid", "deviceToken"); + AuthRequest otherPidSameDeviceUserRequest = dtoGenerator.generateAuthRequest(otherPidSameDeviceUser); + + authService.issueTokens(otherPidSameDeviceUserRequest); + + assertAll( + () -> assertThat(getDeviceTokenByAuthProvider("pid")).isNull(), + () -> assertThat(getDeviceTokenByAuthProvider("otherPid").getValue()).isEqualTo("deviceToken") + ); + } + + @DisplayName("로그인 이력이 없는 기기로 비회원이 회원 생성을 시도하면 회원을 생성한다.") + @Test + void saveMemberWhenNonMemberAttemptsWithUnloggedDevice() { + fixtureGenerator.generateSavedMember("pid", "deviceToken"); + Member freshDeivceFreshPidMember = fixtureGenerator.generateUnsavedMember("newPid", "newDeviceToken"); + AuthRequest freshDeviceFreshPidMemberRequest = dtoGenerator.generateAuthRequest(freshDeivceFreshPidMember); + + authService.issueTokens(freshDeviceFreshPidMemberRequest); + + assertAll( + () -> assertThat(getDeviceTokenByAuthProvider("pid").getValue()).isEqualTo("deviceToken"), + () -> assertThat(getDeviceTokenByAuthProvider("newPid").getValue()).isEqualTo("newDeviceToken") + ); + } + + @DisplayName("로그인 이력이 없는 기기로 회원이 회원 생성을 시도하면 기기 이력을 변경한다.") + @Test + void saveMemberWhenMemberAttemptsWithUnloggedDevice() { + fixtureGenerator.generateSavedMember("pid", "deviceToken"); + Member freshDeivceSamePidMember = fixtureGenerator.generateUnsavedMember("pid", "newDeviceToken"); + AuthRequest freshDeviceSamePidRequest = dtoGenerator.generateAuthRequest(freshDeivceSamePidMember); + + authService.issueTokens(freshDeviceSamePidRequest); + + assertThat(getDeviceTokenByAuthProvider("pid").getValue()).isEqualTo("newDeviceToken"); + } + + private DeviceToken getDeviceTokenByAuthProvider(String providerId) { + return memberRepository.findByAuthProvider(new AuthProvider(providerId)).get().getDeviceToken(); + } + } + @DisplayName("로그아웃 테스트") @Nested class LogoutTest { diff --git a/backend/src/test/java/com/ody/common/DtoGenerator.java b/backend/src/test/java/com/ody/common/DtoGenerator.java index 94accb8c6..9580a69a3 100644 --- a/backend/src/test/java/com/ody/common/DtoGenerator.java +++ b/backend/src/test/java/com/ody/common/DtoGenerator.java @@ -6,10 +6,15 @@ import com.ody.meeting.domain.Location; import com.ody.meeting.domain.Meeting; import com.ody.meeting.dto.request.MeetingSaveRequestV1; +import com.ody.member.domain.Member; import java.time.LocalDateTime; public class DtoGenerator { + public AuthRequest generateAuthRequest(Member member) { + return generateAuthRequest(member.getAuthProvider().getProviderId(), member.getDeviceToken().getValue()); + } + public AuthRequest generateAuthRequest(String providerId, String deviceToken) { return new AuthRequest(deviceToken, providerId, "nickname", "imageUrl"); } diff --git a/backend/src/test/java/com/ody/member/service/MemberServiceTest.java b/backend/src/test/java/com/ody/member/service/MemberServiceTest.java index ea78e361a..0516301f0 100644 --- a/backend/src/test/java/com/ody/member/service/MemberServiceTest.java +++ b/backend/src/test/java/com/ody/member/service/MemberServiceTest.java @@ -43,96 +43,15 @@ class MemberServiceTest extends BaseServiceTest { @MockBean private KakaoAuthUnlinkClient kakaoAuthUnlinkClient; - @DisplayName("회원을 생성한다.") - @Nested - class saveMember { - - @DisplayName("로그인 이력이 있는 기기로 비회원이 회원 생성을 시도하면 기기 이력을 삭제하고 회원을 생성한다.") - @Test - void saveMemberWhenNonMemberAttemptsWithLoggedInDevice() { - fixtureGenerator.generateSavedMember("pid", "deviceToken"); - Member sameDeivceFreshMember = fixtureGenerator.generateUnsavedMember("newPid", "deviceToken"); - - memberService.save(sameDeivceFreshMember); - - assertAll( - () -> assertThat(getDeviceTokenByAuthProvider("pid")).isNull(), - () -> assertThat(getDeviceTokenByAuthProvider("newPid").getValue()).isEqualTo("deviceToken") - ); - } - - @DisplayName("로그인 이력이 있는 기기로 동일 회원이 회원 생성을 시도하면 회원을 생성하지 않는다.") - @Test - void saveMemberWhenMemberAttemptsWithLoggedInDevice() { - fixtureGenerator.generateSavedMember("pid", "deviceToken"); - Member sameMember = fixtureGenerator.generateUnsavedMember("pid", "deviceToken"); - - memberService.save(sameMember); - - assertThat(getDeviceTokenByAuthProvider("pid").getValue()).isEqualTo("deviceToken"); - } - - @DisplayName("로그인 이력이 있는 기기로 타 회원이 회원 생성을 시도하면 기기 이력을 이전한다.") - @Test - void saveMemberWhenOtherMemberAttemptsWithLoggedInDevice() { - fixtureGenerator.generateSavedMember("pid", "deviceToken"); - fixtureGenerator.generateSavedMember("otherPid", "otherDeviceToken"); - Member otherPidSameDeviceUser = fixtureGenerator.generateUnsavedMember("otherPid", "deviceToken"); - - memberService.save(otherPidSameDeviceUser); - - assertAll( - () -> assertThat(getDeviceTokenByAuthProvider("pid")).isNull(), - () -> assertThat(getDeviceTokenByAuthProvider("otherPid").getValue()).isEqualTo("deviceToken") - ); - } - - @DisplayName("로그인 이력이 없는 기기로 비회원이 회원 생성을 시도하면 회원을 생성한다.") - @Test - void saveMemberWhenNonMemberAttemptsWithUnloggedDevice() { - fixtureGenerator.generateSavedMember("pid", "deviceToken"); - Member freshDeivceFreshPidMember = fixtureGenerator.generateUnsavedMember("newPid", "newDeviceToken"); - - memberService.save(freshDeivceFreshPidMember); - - assertAll( - () -> assertThat(getDeviceTokenByAuthProvider("pid").getValue()).isEqualTo("deviceToken"), - () -> assertThat(getDeviceTokenByAuthProvider("newPid").getValue()).isEqualTo("newDeviceToken") - ); - } - - @DisplayName("로그인 이력이 없는 기기로 회원이 회원 생성을 시도하면 기기 이력을 변경한다.") - @Test - void saveMemberWhenMemberAttemptsWithUnloggedDevice() { - fixtureGenerator.generateSavedMember("pid", "deviceToken"); - Member freshDeivceSamePidMember = fixtureGenerator.generateUnsavedMember("pid", "newDeviceToken"); - - memberService.save(freshDeivceSamePidMember); - - assertThat(getDeviceTokenByAuthProvider("pid").getValue()).isEqualTo("newDeviceToken"); - } - - @DisplayName("특정 회원의 리프레시 토큰을 삭제할 수 있다") - @Test - void removeMemberRefreshToken() { - Member member = saveMember("pid", "deviceToken", "refresh-token=token"); - - memberService.updateRefreshToken(member.getId(), null); - - Member findMember = memberService.findById(member.getId()); - assertThat(findMember.getRefreshToken()).isNull(); - } - - private DeviceToken getDeviceTokenByAuthProvider(String providerId) { - return memberRepository.findByAuthProvider(new AuthProvider(providerId)).get().getDeviceToken(); - } - - public Member saveMember(String providerId, String rawDeviceToken, String rawRefreshToken) { - Member member = fixtureGenerator.generateSavedMember(providerId, rawDeviceToken); - RefreshToken refreshToken = new RefreshToken(rawRefreshToken); - member.updateRefreshToken(refreshToken); - return memberRepository.save(member); - } + @DisplayName("특정 회원의 리프레시 토큰을 삭제할 수 있다") + @Test + void removeMemberRefreshToken() { + Member member = saveMember("pid", "deviceToken", "refresh-token=token"); + + memberService.updateRefreshToken(member.getId(), null); + + Member findMember = memberService.findById(member.getId()); + assertThat(findMember.getRefreshToken()).isNull(); } @DisplayName("회원을 삭제한다.") @@ -159,4 +78,11 @@ void findDeletedMemberById() { assertThatThrownBy(() -> memberService.findById(member.getId())) .isInstanceOf(OdyUnauthorizedException.class); } + + public Member saveMember(String providerId, String rawDeviceToken, String rawRefreshToken) { + Member member = fixtureGenerator.generateSavedMember(providerId, rawDeviceToken); + RefreshToken refreshToken = new RefreshToken(rawRefreshToken); + member.updateRefreshToken(refreshToken); + return memberRepository.save(member); + } } diff --git a/backend/src/test/java/com/ody/route/repository/ApiCallRepositoryTest.java b/backend/src/test/java/com/ody/route/repository/ApiCallRepositoryTest.java index 723901a87..39145a975 100644 --- a/backend/src/test/java/com/ody/route/repository/ApiCallRepositoryTest.java +++ b/backend/src/test/java/com/ody/route/repository/ApiCallRepositoryTest.java @@ -47,7 +47,7 @@ void findFirstByDateBetweenAndClientType() { apiCallRepository.save(secondDateApiCall); apiCallRepository.save(firstDateApiCall); - Optional actual = apiCallRepository.findFirstByDateBetweenAndClientType(firstDay, now, clientType); + Optional actual = apiCallRepository.findFirstByDateBetweenAndClientType(firstDay, firstDay.plusDays(10), clientType); assertThat(actual).isPresent() .get().extracting(ApiCall::getDate).isEqualTo(firstDateApiCall.getDate());