Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface SignupTokenRepository extends JpaRepository<SignupToken, String
"SELECT st FROM SignupToken st WHERE st.token = :token AND st.createdAt > :createdAtAfter"
)
Optional<SignupToken> findByIdAndCreatedAtAfter(String token, LocalDateTime createdAtAfter);

void deleteByToken(String token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.auth.domain.SignupToken;
import org.runimo.runimo.auth.exceptions.SignUpException;
import org.runimo.runimo.auth.jwt.JwtResolver;
import org.runimo.runimo.auth.jwt.JwtTokenFactory;
import org.runimo.runimo.auth.jwt.SignupTokenPayload;
Expand All @@ -13,6 +14,7 @@
import org.runimo.runimo.user.domain.AppleUserToken;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.runimo.runimo.user.repository.AppleUserTokenRepository;
import org.runimo.runimo.user.service.UserRegisterService;
import org.runimo.runimo.user.service.dto.command.UserRegisterCommand;
Expand All @@ -36,6 +38,7 @@ public class SignUpUsecaseImpl implements SignUpUsecase {
public SignupUserResponse register(UserSignupCommand command) {
SignupTokenPayload payload = jwtResolver.getSignupTokenPayload(command.registerToken());
SignupToken signupToken = findUnExpiredSignupToken(payload.token());
userRegisterService.validateExistingUser(payload.providerId(), payload.socialProvider());
String imgUrl = fileStorageService.storeFile(command.profileImage());
User savedUser = userRegisterService.registerUser(new UserRegisterCommand(
command.nickname(),
Expand All @@ -47,14 +50,19 @@ public SignupUserResponse register(UserSignupCommand command) {
if (payload.socialProvider() == SocialProvider.APPLE) {
createAppleUserToken(savedUser.getId(), signupToken);
}
removeSignupToken(payload.token());
return new SignupUserResponse(savedUser, jwtTokenFactory.generateTokenPair(savedUser));
}

private void removeSignupToken(String token) {
signupTokenRepository.deleteByToken(token);
}

private SignupToken findUnExpiredSignupToken(String token) {
LocalDateTime cutOffTime = LocalDateTime.now().minusMinutes(REGISTER_CUTOFF_MIN);
return signupTokenRepository.
findByIdAndCreatedAtAfter(token, cutOffTime)
.orElseThrow(IllegalAccessError::new);
.orElseThrow(() -> new SignUpException(UserHttpResponseCode.TOKEN_INVALID));
}

private void createAppleUserToken(Long userId, SignupToken signupToken) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.runimo.runimo.user.repository;

import jakarta.validation.constraints.NotNull;
import java.util.Optional;
import org.runimo.runimo.user.domain.OAuthInfo;
import org.runimo.runimo.user.domain.SocialProvider;
Expand All @@ -11,17 +12,20 @@
@Repository
public interface OAuthInfoRepository extends JpaRepository<OAuthInfo, Long> {

@Query("""
SELECT o FROM OAuthInfo o
WHERE o.deletedAt is null AND o.provider = :provider AND o.providerId = :providerId
""")
Optional<OAuthInfo> findByProviderAndProviderId(SocialProvider provider, String providerId);
@Query("""
SELECT o FROM OAuthInfo o
WHERE o.deletedAt is null AND o.provider = :provider AND o.providerId = :providerId
""")
Optional<OAuthInfo> findByProviderAndProviderId(SocialProvider provider, String providerId);

SocialProvider user(User user);
SocialProvider user(User user);

@Query("""
select o from OAuthInfo o
where o.deletedAt is null AND o.user.id = :userId
""")
Optional<OAuthInfo> findByUserId(Long userId);
@Query("""
select o from OAuthInfo o
where o.deletedAt is null AND o.user.id = :userId
""")
Optional<OAuthInfo> findByUserId(Long userId);

boolean existsByProviderIdAndProvider(@NotNull String providerId,
@NotNull SocialProvider socialProvider);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.runimo.runimo.user.service;

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.auth.exceptions.SignUpException;
import org.runimo.runimo.rewards.service.eggs.EggGrantService;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.runimo.runimo.user.repository.OAuthInfoRepository;
import org.runimo.runimo.user.service.dto.command.UserCreateCommand;
import org.runimo.runimo.user.service.dto.command.UserRegisterCommand;
import org.springframework.stereotype.Service;
Expand All @@ -15,6 +19,7 @@ public class UserRegisterService {
private final UserCreator userCreator;
private final UserItemCreator userItemCreator;
private final EggGrantService eggGrantService;
private final OAuthInfoRepository oAuthInfoRepository;

@Transactional
public User registerUser(UserRegisterCommand command) {
Expand All @@ -26,4 +31,11 @@ public User registerUser(UserRegisterCommand command) {
eggGrantService.grantGreetingEggToUser(savedUser);
return savedUser;
}

public void validateExistingUser(String providerId, SocialProvider socialProvider) {
if(oAuthInfoRepository.existsByProviderIdAndProvider(
providerId, socialProvider)) {
throw new SignUpException(UserHttpResponseCode.SIGNIN_FAIL_ALREADY_EXIST);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.runimo.runimo.auth.controller;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.runimo.runimo.CleanUpUtil;
import org.runimo.runimo.auth.controller.request.AuthSignupRequest;
import org.runimo.runimo.auth.domain.SignupToken;
import org.runimo.runimo.auth.repository.SignupTokenRepository;

import org.runimo.runimo.auth.service.apple.AppleTokenVerifier;
import org.runimo.runimo.auth.service.kakao.KakaoLoginHandler;
import org.runimo.runimo.auth.service.kakao.KakaoTokenVerifier;
import org.runimo.runimo.external.FileStorageService;
import org.runimo.runimo.user.domain.Gender;
import org.runimo.runimo.user.domain.SocialProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class AuthAcceptanceTest {

@LocalServerPort
private int port;
@MockitoBean
private FileStorageService fileStorageService;
@MockitoBean
private KakaoTokenVerifier kakaoTokenVerifier;
@MockitoBean
private AppleTokenVerifier appleTokenVerifier;

@Autowired
private SignupTokenRepository signupTokenRepository;

@Autowired
private KakaoLoginHandler kakaoLoginHandler;

private String token;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private CleanUpUtil cleanUpUtil;

@BeforeEach
void setUp() {
RestAssured.port = port;
// Save a valid signup token in the database
SignupToken signupToken = new SignupToken(
"valid-token",
"refresh-token",
"provider-id",
org.runimo.runimo.user.domain.SocialProvider.KAKAO
);
token = kakaoLoginHandler.createTemporalSignupToken("temp-id", SocialProvider.KAKAO);
signupTokenRepository.save(signupToken);
}

@AfterEach
void tearDown() {
cleanUpUtil.cleanUpUserInfos();
}

@Test
void 회원가입_성공_201응답() throws JsonProcessingException {

AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN);

given()
.contentType(ContentType.MULTIPART)
.multiPart("request", objectMapper.writeValueAsString(request))
.when()
.post("/api/v1/auth/signup")
.then()
.statusCode(HttpStatus.CREATED.value())
.log().all()
.body("payload.nickname", equalTo("username"))
.body("payload.token_pair.access_token", notNullValue())
.body("payload.token_pair.refresh_token", notNullValue());
}

@Test
void 토큰_오류_회원가입_실패_401응답() throws JsonProcessingException {
AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN);

given()
.contentType(ContentType.MULTIPART)
.multiPart("request", objectMapper.writeValueAsString(request))
.when()
.post("/api/v1/auth/signup")
.then()
.statusCode(HttpStatus.CREATED.value())
.log().all()
.body("payload.nickname", equalTo("username"))
.body("payload.token_pair.access_token", notNullValue())
.body("payload.token_pair.refresh_token", notNullValue());

given()
.contentType(ContentType.MULTIPART)
.multiPart("request", objectMapper.writeValueAsString(request))
.when()
.post("/api/v1/auth/signup")
.then()
.statusCode(HttpStatus.UNAUTHORIZED.value());
}

@Test
void 중복_유저_회원가입_409응답() throws JsonProcessingException {
AuthSignupRequest request = new AuthSignupRequest(token, "username", Gender.UNKNOWN);

given()
.contentType(ContentType.MULTIPART)
.multiPart("request", objectMapper.writeValueAsString(request))
.when()
.post("/api/v1/auth/signup")
.then()
.statusCode(HttpStatus.CREATED.value())
.log().all()
.body("payload.nickname", equalTo("username"))
.body("payload.token_pair.access_token", notNullValue())
.body("payload.token_pair.refresh_token", notNullValue());

token = kakaoLoginHandler.createTemporalSignupToken("temp-id", SocialProvider.KAKAO);
request = new AuthSignupRequest(token, "username", Gender.UNKNOWN);

given()
.contentType(ContentType.MULTIPART)
.multiPart("request", objectMapper.writeValueAsString(request))
.when()
.post("/api/v1/auth/signup")
.then()
.statusCode(HttpStatus.CONFLICT.value());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.runimo.runimo.auth.service;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;

import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.runimo.runimo.auth.domain.SignupToken;
import org.runimo.runimo.auth.exceptions.SignUpException;
import org.runimo.runimo.auth.jwt.JwtResolver;
import org.runimo.runimo.auth.jwt.JwtTokenFactory;
import org.runimo.runimo.auth.jwt.SignupTokenPayload;
import org.runimo.runimo.auth.repository.SignupTokenRepository;
import org.runimo.runimo.auth.service.dto.UserSignupCommand;
import org.runimo.runimo.external.FileStorageService;
import org.runimo.runimo.user.domain.Gender;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.runimo.runimo.user.repository.AppleUserTokenRepository;
import org.runimo.runimo.user.service.UserRegisterService;

class SignUpUsecaseTest {

@Mock
private UserRegisterService userRegisterService;
@Mock
private FileStorageService fileStorageService;
@Mock
private JwtTokenFactory jwtTokenFactory;
@Mock
private SignupTokenRepository signupTokenRepository;
@Mock
private AppleUserTokenRepository appleUserTokenRepository;
@Mock
private JwtResolver jwtResolver;

private SignUpUsecaseImpl sut;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
sut = new SignUpUsecaseImpl(
userRegisterService,
fileStorageService,
jwtTokenFactory,
signupTokenRepository,
appleUserTokenRepository,
jwtResolver
);
}

@Test
void 유저가_이미_존재하면_실패() {
// given
String registerToken = "dummy.token.value";
SignupTokenPayload payload = new SignupTokenPayload(registerToken, "socialId123",
SocialProvider.KAKAO);

when(jwtResolver.getSignupTokenPayload(registerToken)).thenReturn(payload);
when(signupTokenRepository.findByIdAndCreatedAtAfter(eq(registerToken), any()))
.thenReturn(Optional.of(new SignupToken(
registerToken, "refresh", "refresh", SocialProvider.KAKAO)));

doThrow(new org.runimo.runimo.auth.exceptions.SignUpException(
UserHttpResponseCode.SIGNIN_FAIL_ALREADY_EXIST))
.when(userRegisterService)
.validateExistingUser(payload.providerId(), payload.socialProvider());

assertThrows(SignUpException.class, () -> {
sut.register(new UserSignupCommand(registerToken, "nickname", null, Gender.UNKNOWN));
});
}
}