diff --git a/src/main/java/in/koreatech/koin/domain/user/dto/StudentRegisterRequest.java b/src/main/java/in/koreatech/koin/domain/user/dto/StudentRegisterRequest.java index a274456d0..8a671e9f3 100644 --- a/src/main/java/in/koreatech/koin/domain/user/dto/StudentRegisterRequest.java +++ b/src/main/java/in/koreatech/koin/domain/user/dto/StudentRegisterRequest.java @@ -81,28 +81,4 @@ public record StudentRegisterRequest( String phoneNumber ) { - public Student toStudent(PasswordEncoder passwordEncoder, Clock clock) { - User user = User.builder() - .password(passwordEncoder.encode(password)) - .email(email) - .name(name) - .nickname(nickname) - .gender(gender) - .phoneNumber(phoneNumber) - .isAuthed(false) - .isDeleted(false) - .userType(UserType.STUDENT) - .authToken(UUID.randomUUID().toString()) - .authExpiredAt(LocalDateTime.now(clock).plusHours(10)) - .build(); - - return Student.builder() - .user(user) - .anonymousNickname("익명_" + (System.currentTimeMillis())) - .isGraduated(isGraduated) - .userIdentity(UserIdentity.UNDERGRADUATE) - .department(department) - .studentNumber(studentNumber) - .build(); - } } diff --git a/src/main/java/in/koreatech/koin/domain/user/model/AuthResult.java b/src/main/java/in/koreatech/koin/domain/user/model/AuthResult.java deleted file mode 100644 index 2b94b7180..000000000 --- a/src/main/java/in/koreatech/koin/domain/user/model/AuthResult.java +++ /dev/null @@ -1,45 +0,0 @@ -package in.koreatech.koin.domain.user.model; - -import java.time.Clock; -import java.time.LocalDateTime; -import java.util.Optional; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.web.servlet.ModelAndView; - -public class AuthResult { - - private final Optional user; - private final ApplicationEventPublisher eventPublisher; - private final Clock clock; - - public AuthResult(Optional user, ApplicationEventPublisher eventPublisher, Clock clock) { - this.user = user; - this.eventPublisher = eventPublisher; - this.clock = clock; - } - - public ModelAndView toModelAndViewForStudent() { - return user.map(user -> { - if (user.getAuthExpiredAt().isBefore(LocalDateTime.now(clock))) { - return createErrorModelAndView("이미 만료된 토큰입니다."); - } - if (!user.isAuthed()) { - user.auth(); - eventPublisher.publishEvent(new StudentRegisterEvent(user.getEmail())); - return createSuccessModelAndView(); - } - return createErrorModelAndView("이미 인증된 사용자입니다."); - }).orElseGet(() -> createErrorModelAndView("토큰에 해당하는 사용자를 찾을 수 없습니다.")); - } - - private ModelAndView createErrorModelAndView(String errorMessage) { - ModelAndView modelAndView = new ModelAndView("error_config"); - modelAndView.addObject("errorMessage", errorMessage); - return modelAndView; - } - - private ModelAndView createSuccessModelAndView() { - return new ModelAndView("success_register_config"); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/user/model/redis/StudentTemporaryStatus.java b/src/main/java/in/koreatech/koin/domain/user/model/redis/StudentTemporaryStatus.java new file mode 100644 index 000000000..27d4ecc88 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/model/redis/StudentTemporaryStatus.java @@ -0,0 +1,88 @@ +package in.koreatech.koin.domain.user.model.redis; + +import in.koreatech.koin.domain.user.dto.StudentRegisterRequest; +import in.koreatech.koin.domain.user.model.*; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Getter +@RedisHash(value = "StudentTemporaryStatus") +public class StudentTemporaryStatus { + + private static final long CACHE_EXPIRE_SECOND = 60 * 60 * 10L; + + @Id + @Indexed + private String email; + + @Indexed + private String authToken; + + @Indexed + private String nickname; + + private String name; + + private String password; + + private UserGender gender; + + private boolean isGraduated; + + private String department; + + private String studentNumber; + + private String phoneNumber; + + @TimeToLive + private Long expiration; + + public StudentTemporaryStatus(String email, String authToken, String nickname, String name, String password, + UserGender gender, boolean isGraduated, String department, String studentNumber, String phoneNumber) { + this.email = email; + this.authToken = authToken; + this.nickname = nickname; + this.name = name; + this.password = password; + this.gender = gender; + this.isGraduated = isGraduated; + this.department = department; + this.studentNumber = studentNumber; + this.phoneNumber = phoneNumber; + this.expiration = CACHE_EXPIRE_SECOND; + } + + public static StudentTemporaryStatus of(StudentRegisterRequest request, String authToken) { + return new StudentTemporaryStatus(request.email(), authToken, request.nickname(), request.name(), request.password(), request.gender(), + request.isGraduated(), request.department(), request.studentNumber(), request.phoneNumber()); + } + + public Student toStudent(PasswordEncoder passwordEncoder) { + User user = User.builder() + .password(passwordEncoder.encode(password)) + .email(email) + .name(name) + .nickname(nickname) + .gender(gender) + .phoneNumber(phoneNumber) + .isAuthed(true) + .isDeleted(false) + .userType(UserType.STUDENT) + .authToken(authToken) + .build(); + + return Student.builder() + .user(user) + .anonymousNickname("익명_" + (System.currentTimeMillis())) + .isGraduated(isGraduated) + .userIdentity(UserIdentity.UNDERGRADUATE) + .department(department) + .studentNumber(studentNumber) + .build(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/user/repository/StudentRedisRepository.java b/src/main/java/in/koreatech/koin/domain/user/repository/StudentRedisRepository.java new file mode 100644 index 000000000..20ecf6721 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/repository/StudentRedisRepository.java @@ -0,0 +1,19 @@ +package in.koreatech.koin.domain.user.repository; + +import in.koreatech.koin.domain.user.model.redis.StudentTemporaryStatus; +import org.springframework.data.repository.Repository; + +import java.util.Optional; + +public interface StudentRedisRepository extends Repository { + + StudentTemporaryStatus save(StudentTemporaryStatus studentTemporaryStatus); + + Optional findById(String email); + + Optional findByNickname(String nickname); + + Optional findByAuthToken(String authToken); + + void deleteById(String email); +} diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index 900476852..4ca069153 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -4,9 +4,11 @@ import java.util.Optional; import java.util.UUID; +import in.koreatech.koin.domain.user.model.*; +import in.koreatech.koin.domain.user.model.redis.StudentTemporaryStatus; +import in.koreatech.koin.domain.user.repository.StudentRedisRepository; import org.joda.time.LocalDateTime; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,13 +26,6 @@ import in.koreatech.koin.domain.user.exception.DuplicationNicknameException; import in.koreatech.koin.domain.user.exception.StudentDepartmentNotValidException; import in.koreatech.koin.domain.user.exception.StudentNumberNotValidException; -import in.koreatech.koin.domain.user.model.AuthResult; -import in.koreatech.koin.domain.user.model.Student; -import in.koreatech.koin.domain.user.model.StudentDepartment; -import in.koreatech.koin.domain.user.model.StudentEmailRequestEvent; -import in.koreatech.koin.domain.user.model.User; -import in.koreatech.koin.domain.user.model.UserGender; -import in.koreatech.koin.domain.user.model.UserToken; import in.koreatech.koin.domain.user.repository.StudentRepository; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.domain.user.repository.UserTokenRepository; @@ -50,6 +45,7 @@ public class StudentService { private final StudentRepository studentRepository; + private final StudentRedisRepository studentRedisRepository; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final MailService mailService; @@ -66,12 +62,13 @@ public StudentResponse getStudent(Integer userId) { @Transactional public StudentLoginResponse studentLogin(StudentLoginRequest request) { User user = userRepository.getByEmail(request.email()); + Optional studentTemporaryStatus = studentRedisRepository.findById(request.email()); if (!user.isSamePassword(passwordEncoder, request.password())) { throw new KoinIllegalArgumentException("비밀번호가 틀렸습니다."); } - if (!user.isAuthed()) { + if (studentTemporaryStatus.isPresent()) { throw new AuthorizationException("미인증 상태입니다. 아우누리에서 인증메일을 확인해주세요"); } @@ -114,45 +111,66 @@ public void checkDepartmentValid(String department) { @Transactional public ModelAndView authenticate(AuthTokenRequest request) { - Optional user = userRepository.findByAuthToken(request.authToken()); - return new AuthResult(user, eventPublisher, clock).toModelAndViewForStudent(); + Optional studentTemporaryStatus = studentRedisRepository.findByAuthToken(request.authToken()); + + if (studentTemporaryStatus.isEmpty()) { + ModelAndView modelAndView = new ModelAndView("error_config"); + modelAndView.addObject("errorMessage", "토큰이 유효하지 않습니다."); + return modelAndView; + } + + Student student = studentTemporaryStatus.get().toStudent(passwordEncoder); + + studentRepository.save(student); + userRepository.save(student.getUser()); + + studentRedisRepository.deleteById(student.getUser().getEmail()); + eventPublisher.publishEvent(new StudentRegisterEvent(student.getUser().getEmail())); + + return new ModelAndView("success_register_config"); } @Transactional public void studentRegister(StudentRegisterRequest request, String serverURL) { - Student student = request.toStudent(passwordEncoder, clock); - try { - validateStudentRegister(student); - studentRepository.save(student); - userRepository.save(student.getUser()); - mailService.sendMail(request.email(), new StudentRegistrationData(serverURL, student.getUser().getAuthToken())); - eventPublisher.publishEvent(new StudentEmailRequestEvent(request.email())); - } catch (DataIntegrityViolationException e) { - // 동시성 문제를 처리하기 위한 코드 - throw KoinIllegalArgumentException.withDetail("요청이 너무 빠릅니다."); - } + + validateStudentRegister(request); + String authToken = UUID.randomUUID().toString(); + + StudentTemporaryStatus studentTemporaryStatus = StudentTemporaryStatus.of(request, authToken); + studentRedisRepository.save(studentTemporaryStatus); + + mailService.sendMail(request.email(), new StudentRegistrationData(serverURL, authToken)); + eventPublisher.publishEvent(new StudentEmailRequestEvent(request.email())); } - private void validateStudentRegister(Student student) { - EmailAddress emailAddress = EmailAddress.from(student.getUser().getEmail()); + private void validateStudentRegister(StudentRegisterRequest request) { + EmailAddress emailAddress = EmailAddress.from(request.email()); emailAddress.validateKoreatechEmail(); - validateDataExist(student); - validateStudentNumber(student.getStudentNumber()); - checkDepartmentValid(student.getDepartment()); + validateDataExist(request); + validateStudentNumber(request.studentNumber()); + checkDepartmentValid(request.department()); } - private void validateDataExist(Student student) { - userRepository.findByEmail(student.getUser().getEmail()) - .ifPresent(user -> { - throw DuplicationEmailException.withDetail("email: " + student.getUser().getEmail()); - }); - - if (student.getUser().getNickname() != null) { - userRepository.findByNickname(student.getUser().getNickname()) + private void validateDataExist(StudentRegisterRequest request) { + userRepository.findByEmail(request.email()) .ifPresent(user -> { - throw DuplicationNicknameException.withDetail("nickname: " + student.getUser().getNickname()); + throw DuplicationEmailException.withDetail("email: " + request.email()); + }); + studentRedisRepository.findById(request.email()) + .ifPresent(studentTemporaryStatus -> { + throw DuplicationEmailException.withDetail("email: " + request.email()); }); + + if (request.nickname() != null) { + userRepository.findByNickname(request.nickname()) + .ifPresent(user -> { + throw DuplicationNicknameException.withDetail("nickname: " + request.nickname()); + }); + studentRedisRepository.findByNickname(request.nickname()) + .ifPresent(studentTemporaryStatus -> { + throw DuplicationNicknameException.withDetail("nickname: " + request.nickname()); + }); } } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java index e0e73d27f..1c31fcfc6 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/UserService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/UserService.java @@ -2,8 +2,11 @@ import java.time.LocalDateTime; import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import in.koreatech.koin.domain.user.model.redis.StudentTemporaryStatus; +import in.koreatech.koin.domain.user.repository.StudentRedisRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -42,6 +45,7 @@ public class UserService { private final JwtProvider jwtProvider; private final UserRepository userRepository; private final StudentRepository studentRepository; + private final StudentRedisRepository studentRedisRepository; private final OwnerRepository ownerRepository; private final PasswordEncoder passwordEncoder; private final UserTokenRepository userTokenRepository; @@ -51,12 +55,13 @@ public class UserService { @Transactional public UserLoginResponse login(UserLoginRequest request) { User user = userRepository.getByEmail(request.email()); + Optional studentTemporaryStatus = studentRedisRepository.findById(request.email()); if (!user.isSamePassword(passwordEncoder, request.password())) { throw new KoinIllegalArgumentException("비밀번호가 틀렸습니다."); } - if (!user.isAuthed()) { + if (studentTemporaryStatus.isPresent()) { throw new AuthorizationException("미인증 상태입니다. 아우누리에서 인증메일을 확인해주세요"); } diff --git a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java index a37da80ee..ab2f859b4 100644 --- a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java @@ -7,8 +7,11 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisKeyValueAdapter; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; @@ -19,6 +22,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @Configuration +@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP) +@Profile("!test") public class RedisConfig { @Bean diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java index bd1779dc6..41a1eb1d2 100644 --- a/src/test/java/in/koreatech/koin/AcceptanceTest.java +++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java @@ -20,6 +20,7 @@ import org.testcontainers.utility.DockerImageName; import in.koreatech.koin.config.TestJpaConfiguration; +import in.koreatech.koin.config.TestRedisConfiguration; import in.koreatech.koin.config.TestTimeConfig; import in.koreatech.koin.domain.bus.util.CityBusClient; import in.koreatech.koin.domain.bus.util.CityBusRouteClient; @@ -33,7 +34,7 @@ import jakarta.persistence.EntityManager; @SpringBootTest(webEnvironment = RANDOM_PORT) -@Import({DBInitializer.class, TestJpaConfiguration.class, TestTimeConfig.class}) +@Import({DBInitializer.class, TestJpaConfiguration.class, TestTimeConfig.class, TestRedisConfiguration.class}) @ActiveProfiles("test") public abstract class AcceptanceTest { @@ -96,18 +97,18 @@ private static void configureProperties(final DynamicPropertyRegistry registry) static { mySqlContainer = (MySQLContainer)new MySQLContainer("mysql:8.0.29") - .withDatabaseName("test") - .withUsername(ROOT) - .withPassword(ROOT_PASSWORD) - .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); + .withDatabaseName("test") + .withUsername(ROOT) + .withPassword(ROOT_PASSWORD) + .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); redisContainer = new GenericContainer<>( - DockerImageName.parse("redis:7.0.9")) - .withExposedPorts(6379); + DockerImageName.parse("redis:7.0.9")) + .withExposedPorts(6379); mongoContainer = new GenericContainer<>( - DockerImageName.parse("mongo:6.0.14")) - .withExposedPorts(27017); + DockerImageName.parse("mongo:6.0.14")) + .withExposedPorts(27017); mySqlContainer.start(); redisContainer.start(); diff --git a/src/test/java/in/koreatech/koin/KoinApplicationTest.java b/src/test/java/in/koreatech/koin/KoinApplicationTest.java index b9d1c1877..df10741e6 100644 --- a/src/test/java/in/koreatech/koin/KoinApplicationTest.java +++ b/src/test/java/in/koreatech/koin/KoinApplicationTest.java @@ -6,11 +6,12 @@ import org.springframework.test.context.ActiveProfiles; import in.koreatech.koin.config.TestJpaConfiguration; +import in.koreatech.koin.config.TestRedisConfiguration; import in.koreatech.koin.config.TestTimeConfig; @SpringBootTest @ActiveProfiles("test") -@Import({TestJpaConfiguration.class, TestTimeConfig.class}) +@Import({TestJpaConfiguration.class, TestTimeConfig.class, TestRedisConfiguration.class}) class KoinApplicationTest { @Test diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index bda450b83..54a78c1cd 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -7,10 +7,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import in.koreatech.koin.domain.user.model.redis.StudentTemporaryStatus; +import in.koreatech.koin.domain.user.repository.StudentRedisRepository; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -38,12 +41,14 @@ import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") -@ExtendWith(OutputCaptureExtension.class) class UserApiTest extends AcceptanceTest { @Autowired private StudentRepository studentRepository; + @Autowired + private StudentRedisRepository studentRedisRepository; + @Autowired private UserRepository userRepository; @@ -595,11 +600,11 @@ void getAuth() { } @Test - @DisplayName("학생 회원가입 후 학교 이메일요청 이벤트가 발생한다.") + @DisplayName("학생 회원가입 후 학교 이메일요청 이벤트가 발생하고 Redis에 저장된다.") void studentRegister() { - RestAssured - .given() - .body(""" + var response = RestAssured + .given() + .body(""" { "major": "컴퓨터공학부", "email": "koko123@koreatech.ac.kr", @@ -612,43 +617,40 @@ void studentRegister() { "phone_number": "01000000000" } """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.OK.value()); + .contentType(ContentType.JSON) + .when() + .post("/user/student/register") + .then() + .statusCode(HttpStatus.OK.value()) + .extract(); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { - User user = userRepository.getByEmail("koko123@koreatech.ac.kr"); - Student student = studentRepository.getById(user.getId()); + Optional student = studentRedisRepository.findById("koko123@koreatech.ac.kr"); assertSoftly( - softly -> { - softly.assertThat(student).isNotNull(); - softly.assertThat(student.getUser().getNickname()).isEqualTo("koko"); - softly.assertThat(student.getUser().getName()).isEqualTo("김철수"); - softly.assertThat(student.getUser().getPhoneNumber()).isEqualTo("01000000000"); - softly.assertThat(student.getUser().getUserType()).isEqualTo(STUDENT); - softly.assertThat(student.getUser().getEmail()).isEqualTo("koko123@koreatech.ac.kr"); - softly.assertThat(student.getUser().isAuthed()).isEqualTo(false); - softly.assertThat(student.getStudentNumber()).isEqualTo("2021136012"); - softly.assertThat(student.getDepartment()).isEqualTo(Dept.COMPUTER_SCIENCE.getName()); - softly.assertThat(student.getAnonymousNickname()).isNotNull(); - verify(studentEventListener).onStudentEmailRequest(any()); - } + softly -> { + softly.assertThat(student).isNotNull(); + softly.assertThat(student.get().getNickname()).isEqualTo("koko"); + softly.assertThat(student.get().getName()).isEqualTo("김철수"); + softly.assertThat(student.get().getPhoneNumber()).isEqualTo("01000000000"); + softly.assertThat(student.get().getEmail()).isEqualTo("koko123@koreatech.ac.kr"); + softly.assertThat(student.get().getStudentNumber()).isEqualTo("2021136012"); + softly.assertThat(student.get().getDepartment()).isEqualTo(Dept.COMPUTER_SCIENCE.getName()); + verify(studentEventListener).onStudentEmailRequest(any()); + } ); } }); } @Test - @DisplayName("이메일 요청을 확인 후 회원가입 이벤트가 발생한다.") + @DisplayName("이메일 요청을 확인 후 회원가입 이벤트가 발생하고 Redis에 저장된 정보가 삭제된다.") void authenticate() { RestAssured - .given() - .body(""" + .given() + .body(""" { "major": "컴퓨터공학부", "email": "koko123@koreatech.ac.kr", @@ -661,23 +663,26 @@ void authenticate() { "phone_number": "01000000000" } """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.OK.value()); + .contentType(ContentType.JSON) + .when() + .post("/user/student/register") + .then() + .statusCode(HttpStatus.OK.value()) + .extract(); - User user = userRepository.getByEmail("koko123@koreatech.ac.kr"); + Optional student = studentRedisRepository.findById("koko123@koreatech.ac.kr"); RestAssured - .given() - .param("auth_token", user.getAuthToken()) - .when() - .get("/user/authenticate"); + .given() + .param("auth_token", student.get().getAuthToken()) + .when() + .get("/user/authenticate") + .then(); - User user1 = userRepository.getByEmail("koko123@koreatech.ac.kr"); + User user = userRepository.getByEmail("koko123@koreatech.ac.kr"); + assertThat(studentRedisRepository.findById("koko123@koreatech.ac.kr")).isEmpty(); - assertThat(user1.isAuthed()).isTrue(); + assertThat(user.isAuthed()).isTrue(); verify(studentEventListener).onStudentRegister(any()); } @@ -777,42 +782,6 @@ void studentRegisterStudentNumberInvalid() { .statusCode(HttpStatus.BAD_REQUEST.value()); } - @Test - @DisplayName("회원가입시 동시성 발생 예외를 적절하게 처리하는지 체크한다.") - void concurrencyStudentRegister(CapturedOutput capturedOutput) throws InterruptedException { - int threads = 2; - CountDownLatch doneSignal = new CountDownLatch(threads); - ExecutorService executorService = Executors.newFixedThreadPool(threads); - - for (int i = 0; i < threads; i++) { - executorService.execute(() -> { - RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": "koko123@koreatech.ac.kr", - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "2022136012", - "phone_number": "01000000000" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register"); - doneSignal.countDown(); - }); - } - doneSignal.await(); - executorService.shutdown(); - - assertThat(capturedOutput.toString()).contains("요청이 너무 빠릅니다."); - } - @Test @DisplayName("사용자가 비밀번호를 통해 자신이 맞는지 인증한다.") void userCheckPassword() { diff --git a/src/test/java/in/koreatech/koin/config/TestRedisConfiguration.java b/src/test/java/in/koreatech/koin/config/TestRedisConfiguration.java new file mode 100644 index 000000000..918acf8fd --- /dev/null +++ b/src/test/java/in/koreatech/koin/config/TestRedisConfiguration.java @@ -0,0 +1,42 @@ +package in.koreatech.koin.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@TestConfiguration +public class TestRedisConfiguration { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper); + template.setValueSerializer(serializer); + template.setConnectionFactory(connectionFactory); + return template; + } + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder. + requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())). + setConnectTimeout(Duration.ofMillis(5000)) + .setReadTimeout(Duration.ofMillis(5000)) + .additionalMessageConverters(new StringHttpMessageConverter(UTF_8)).build(); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index e7b7c8ba1..bfefc208e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -21,6 +21,12 @@ spring: max-file-size: 10MB max-request-size: 10MB + data: + redis: + repositories: + enabled: true + keyspace-events: ON_STARTUP + server: tomcat: max-http-form-post-size: 10MB