From 5df569c1ed7c2100caf16aa11fa621830252b991 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Tue, 26 Sep 2023 22:20:56 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20StudentCode=20unique=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/festago/student/domain/StudentCode.java | 11 +++++++++++ .../db/migration/V6__studnet_code__unique.sql | 3 +++ 2 files changed, 14 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V6__studnet_code__unique.sql diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index 947e58a61..c810e3671 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -11,11 +11,22 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import org.springframework.util.StringUtils; @Entity +@Table( + uniqueConstraints = { + @UniqueConstraint( + name = "unique_username__school", + columnNames = {"username", "school_id"} + ) + } +) public class StudentCode extends BaseTimeEntity { @Id diff --git a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql new file mode 100644 index 000000000..c94ee3ace --- /dev/null +++ b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql @@ -0,0 +1,3 @@ +alter table student_code + add constraint unique_username__school unique (username, school_id); + From 10ec56ed06eab2fcd6ae6801b9276b09b40c09fe Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Tue, 26 Sep 2023 23:32:53 +0900 Subject: [PATCH 02/16] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20setUp=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festago/ticketing/application/TicketingServiceTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/test/java/com/festago/ticketing/application/TicketingServiceTest.java b/backend/src/test/java/com/festago/ticketing/application/TicketingServiceTest.java index 87a62e0e1..fdb338b5c 100644 --- a/backend/src/test/java/com/festago/ticketing/application/TicketingServiceTest.java +++ b/backend/src/test/java/com/festago/ticketing/application/TicketingServiceTest.java @@ -19,7 +19,6 @@ import com.festago.ticketing.repository.MemberTicketRepository; import java.time.Clock; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -55,11 +54,6 @@ class TicketingServiceTest { @InjectMocks TicketingService ticketingService; - @BeforeEach - void setUp() { - - } - @Test void 재학생용_티켓인데_학생인증이_되지_않았으면_예외() { // given From d781540d850da944a66ab95f29901f44589cd1c7 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 00:05:51 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20StudnetCode=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=20=EC=9D=BC=EC=9E=90=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=EC=9E=AC=EB=B0=9C=ED=96=89=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festago/student/domain/StudentCode.java | 24 ++++++++++++++++--- .../db/migration/V6__studnet_code__unique.sql | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index c810e3671..6c1190a57 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -1,5 +1,7 @@ package com.festago.student.domain; +import static java.time.temporal.ChronoUnit.SECONDS; + import com.festago.common.domain.BaseTimeEntity; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.InternalServerException; @@ -11,11 +13,12 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import java.time.LocalDateTime; +import org.springframework.data.annotation.CreatedDate; import org.springframework.util.StringUtils; @Entity @@ -29,6 +32,8 @@ ) public class StudentCode extends BaseTimeEntity { + private static final int MIN_REQUEST_TERM_SECONDS = 30; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -44,20 +49,25 @@ public class StudentCode extends BaseTimeEntity { private String username; + @CreatedDate + private LocalDateTime issuedAt; + protected StudentCode() { } public StudentCode(VerificationCode code, School school, Member member, String username) { - this(null, code, school, member, username); + this(null, code, school, member, username, null); } - public StudentCode(Long id, VerificationCode code, School school, Member member, String username) { + public StudentCode(Long id, VerificationCode code, School school, Member member, String username, + LocalDateTime issuedAt) { validate(username); this.id = id; this.code = code; this.school = school; this.member = member; this.username = username; + this.issuedAt = issuedAt; } private void validate(String username) { @@ -70,6 +80,10 @@ private void validate(String username) { } } + public boolean canReissue(LocalDateTime currentTime) { + return SECONDS.between(issuedAt, currentTime) > MIN_REQUEST_TERM_SECONDS; + } + public Long getId() { return id; } @@ -89,4 +103,8 @@ public Member getMember() { public String getUsername() { return username; } + + public LocalDateTime getIssuedAt() { + return issuedAt; + } } diff --git a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql index c94ee3ace..63c4929cd 100644 --- a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql +++ b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql @@ -1,3 +1,5 @@ alter table student_code add constraint unique_username__school unique (username, school_id); +alter table student_code + add column issued_at datetime(6); From e0789079fb3c8e01b07a08b1d1bf4a7d57d82446 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 00:06:55 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=AC=EC=A0=84=EC=86=A1=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festago/common/exception/ErrorCode.java | 2 + .../student/application/StudentService.java | 47 +++-- .../repository/StudentCodeRepository.java | 2 + .../application/StudentServiceTest.java | 166 ++++++++++++++++++ .../student/domain/StudentCodeTest.java | 56 ++++++ .../festago/support/StudentCodeFixture.java | 58 ++++++ 6 files changed, 317 insertions(+), 14 deletions(-) create mode 100644 backend/src/test/java/com/festago/student/application/StudentServiceTest.java create mode 100644 backend/src/test/java/com/festago/student/domain/StudentCodeTest.java create mode 100644 backend/src/test/java/com/festago/support/StudentCodeFixture.java diff --git a/backend/src/main/java/com/festago/common/exception/ErrorCode.java b/backend/src/main/java/com/festago/common/exception/ErrorCode.java index bd3708054..a3e290422 100644 --- a/backend/src/main/java/com/festago/common/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/common/exception/ErrorCode.java @@ -25,6 +25,8 @@ public enum ErrorCode { DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."), TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."), INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."), + TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."), + // 401 EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."), diff --git a/backend/src/main/java/com/festago/student/application/StudentService.java b/backend/src/main/java/com/festago/student/application/StudentService.java index 0ae184eb3..64b4aed8c 100644 --- a/backend/src/main/java/com/festago/student/application/StudentService.java +++ b/backend/src/main/java/com/festago/student/application/StudentService.java @@ -15,6 +15,8 @@ import com.festago.student.dto.StudentVerificateRequest; import com.festago.student.repository.StudentCodeRepository; import com.festago.student.repository.StudentRepository; +import java.time.Clock; +import java.time.LocalDateTime; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,29 +30,55 @@ public class StudentService { private final SchoolRepository schoolRepository; private final MemberRepository memberRepository; private final StudentRepository studentRepository; + private final Clock clock; public StudentService(MailClient mailClient, VerificationCodeProvider codeProvider, StudentCodeRepository studentCodeRepository, SchoolRepository schoolRepository, - MemberRepository memberRepository, StudentRepository studentRepository) { + MemberRepository memberRepository, StudentRepository studentRepository, Clock clock) { this.mailClient = mailClient; this.codeProvider = codeProvider; this.studentCodeRepository = studentCodeRepository; this.schoolRepository = schoolRepository; this.memberRepository = memberRepository; this.studentRepository = studentRepository; + this.clock = clock; } public void sendVerificationMail(Long memberId, StudentSendMailRequest request) { - validateStudent(memberId); - validateDuplicateEmail(request); Member member = findMember(memberId); School school = findSchool(request.schoolId()); + validate(memberId, request); VerificationCode code = codeProvider.provide(); studentCodeRepository.deleteByMember(member); studentCodeRepository.save(new StudentCode(code, school, member, request.username())); mailClient.send(new VerificationMailPayload(code, request.username(), school.getDomain())); } + private Member findMember(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + } + + private School findSchool(Long schoolId) { + return schoolRepository.findById(schoolId) + .orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND)); + } + + private void validate(Long memberId, StudentSendMailRequest request) { + validateFrequentRequest(memberId); + validateStudent(memberId); + validateDuplicateEmail(request); + } + + private void validateFrequentRequest(Long memberId) { + studentCodeRepository.findByMemberId(memberId) + .ifPresent(code -> { + if (!code.canReissue(LocalDateTime.now(clock))) { + throw new BadRequestException(ErrorCode.TOO_FREQUENT_REQUESTS); + } + }); + } + private void validateStudent(Long memberId) { if (studentRepository.existsByMemberId(memberId)) { throw new BadRequestException(ErrorCode.ALREADY_STUDENT_VERIFIED); @@ -63,20 +91,11 @@ private void validateDuplicateEmail(StudentSendMailRequest request) { } } - private Member findMember(Long memberId) { - return memberRepository.findById(memberId) - .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - } - - private School findSchool(Long schoolId) { - return schoolRepository.findById(schoolId) - .orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND)); - } - public void verificate(Long memberId, StudentVerificateRequest request) { validateStudent(memberId); Member member = findMember(memberId); - StudentCode studentCode = studentCodeRepository.findByCodeAndMember(new VerificationCode(request.code()), member) + StudentCode studentCode = studentCodeRepository.findByCodeAndMember(new VerificationCode(request.code()), + member) .orElseThrow(() -> new BadRequestException(ErrorCode.INVALID_STUDENT_VERIFICATION_CODE)); studentRepository.save(new Student(member, studentCode.getSchool(), studentCode.getUsername())); studentCodeRepository.deleteByMember(member); diff --git a/backend/src/main/java/com/festago/student/repository/StudentCodeRepository.java b/backend/src/main/java/com/festago/student/repository/StudentCodeRepository.java index 6afe6cbd2..3c1f0ee0e 100644 --- a/backend/src/main/java/com/festago/student/repository/StudentCodeRepository.java +++ b/backend/src/main/java/com/festago/student/repository/StudentCodeRepository.java @@ -11,4 +11,6 @@ public interface StudentCodeRepository extends JpaRepository void deleteByMember(Member member); Optional findByCodeAndMember(VerificationCode code, Member member); + + Optional findByMemberId(Long memberId); } diff --git a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java new file mode 100644 index 000000000..26ddcf9da --- /dev/null +++ b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java @@ -0,0 +1,166 @@ +package com.festago.student.application; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; + +import com.festago.common.exception.BadRequestException; +import com.festago.common.exception.NotFoundException; +import com.festago.member.domain.Member; +import com.festago.member.repository.MemberRepository; +import com.festago.school.domain.School; +import com.festago.school.repository.SchoolRepository; +import com.festago.student.domain.StudentCode; +import com.festago.student.dto.StudentSendMailRequest; +import com.festago.student.infrastructure.MockMailClient; +import com.festago.student.infrastructure.RandomVerificationCodeProvider; +import com.festago.student.repository.StudentCodeRepository; +import com.festago.student.repository.StudentRepository; +import com.festago.support.MemberFixture; +import com.festago.support.SchoolFixture; +import com.festago.support.StudentCodeFixture; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class StudentServiceTest { + + @Spy + MailClient mailClient = new MockMailClient(); + + @Spy + VerificationCodeProvider codeProvider = new RandomVerificationCodeProvider(); + + @Mock + StudentCodeRepository studentCodeRepository; + + @Mock + SchoolRepository schoolRepository; + + @Mock + MemberRepository memberRepository; + + @Mock + StudentRepository studentRepository; + + @Spy + Clock clock = Clock.systemDefaultZone(); + + @InjectMocks + StudentService studentService; + + @Nested + class 학생_인증_메일_전송_요청 { + + StudentSendMailRequest request; + + @BeforeEach + void setUp() { + request = new StudentSendMailRequest("ash", 1L); + Member member = MemberFixture.member().id(1L).build(); + School school = SchoolFixture.school().id(1L).build(); + + lenient() + .when(memberRepository.findById(anyLong())) + .thenReturn(Optional.of(member)); + lenient() + .when(schoolRepository.findById(anyLong())) + .thenReturn(Optional.of(school)); + lenient() + .when(studentCodeRepository.findByMemberId(anyLong())) + .thenReturn(Optional.empty()); + lenient() + .when(studentRepository.existsByMemberId(anyLong())) + .thenReturn(false); + lenient() + .when(studentRepository.existsByUsernameAndSchoolId(anyString(), anyLong())) + .thenReturn(false); + } + + @Test + void 존재하지_않는_멤버이면_예외() { + // given + given(memberRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> studentService.sendVerificationMail(1L, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 멤버입니다."); + } + + @Test + void 존재하지_않는_학교면_예외() { + // given + given(schoolRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> studentService.sendVerificationMail(1L, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 학교입니다."); + } + + @Test + void 너무_잦은_요청이면_예외() { + // given + StudentSendMailRequest request = new StudentSendMailRequest("ash", 1L); + LocalDateTime currentTime = LocalDateTime.now(clock); + LocalDateTime issuedAt = currentTime.minusSeconds(30); + StudentCode studentCode = StudentCodeFixture.studentCode().issuedAt(issuedAt).build(); + given(studentCodeRepository.findByMemberId(anyLong())) + .willReturn(Optional.of(studentCode)); + + // when + assertThatThrownBy(() -> studentService.sendVerificationMail(1L, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."); + } + + @Test + void 이미_존재하는_학생이면_예외() { + // given + given(studentRepository.existsByMemberId(anyLong())) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> studentService.sendVerificationMail(1L, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 학교 인증이 완료된 사용자입니다."); + } + + @Test + void 이미_존재하는_이메일이면_예외() { + // given + given(studentRepository.existsByUsernameAndSchoolId(anyString(), anyLong())) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> studentService.sendVerificationMail(1L, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 인증된 이메일입니다."); + } + + @Test + void 인증메일_전송() { + assertThatNoException() + .isThrownBy(() -> studentService.sendVerificationMail(1L, request)); + } + } +} diff --git a/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java b/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java new file mode 100644 index 000000000..bcca7fa93 --- /dev/null +++ b/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java @@ -0,0 +1,56 @@ +package com.festago.student.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.festago.member.domain.Member; +import com.festago.school.domain.School; +import com.festago.support.MemberFixture; +import com.festago.support.SchoolFixture; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class StudentCodeTest { + + @Nested + class 재발급_가능한지_확인 { + + @Test + void 발급한지_30초_이내면_거짓() { + // given + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime issuedAt = currentTime.minusSeconds(30); + School school = SchoolFixture.school().build(); + Member member = MemberFixture.member().build(); + StudentCode studentCode = new StudentCode(1L, new VerificationCode("123456"), + school, member, "ash", issuedAt); + + // when + boolean actual = studentCode.canReissue(currentTime); + + // then + assertThat(actual).isEqualTo(false); + } + + @Test + void 발급한지_30초_이후면_참() { + // given + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime issuedAt = currentTime.minusSeconds(31); + School school = SchoolFixture.school().build(); + Member member = MemberFixture.member().build(); + StudentCode studentCode = new StudentCode(1L, new VerificationCode("123456"), + school, member, "ash", issuedAt); + + // when + boolean actual = studentCode.canReissue(currentTime); + + // then + assertThat(actual).isEqualTo(true); + } + } +} diff --git a/backend/src/test/java/com/festago/support/StudentCodeFixture.java b/backend/src/test/java/com/festago/support/StudentCodeFixture.java new file mode 100644 index 000000000..3fe3c1bfa --- /dev/null +++ b/backend/src/test/java/com/festago/support/StudentCodeFixture.java @@ -0,0 +1,58 @@ +package com.festago.support; + +import com.festago.member.domain.Member; +import com.festago.school.domain.School; +import com.festago.student.domain.StudentCode; +import com.festago.student.domain.VerificationCode; +import java.time.LocalDateTime; + +public class StudentCodeFixture { + + private Long id; + private VerificationCode code = new VerificationCode("123456"); + private School school = SchoolFixture.school().build(); + private Member member = MemberFixture.member().build(); + private String username = "ash"; + private LocalDateTime issuedAt = LocalDateTime.now(); + + private StudentCodeFixture() { + } + + public static StudentCodeFixture studentCode() { + return new StudentCodeFixture(); + } + + public StudentCodeFixture id(Long id) { + this.id = id; + return this; + } + + public StudentCodeFixture code(VerificationCode code) { + this.code = code; + return this; + } + + public StudentCodeFixture school(School school) { + this.school = school; + return this; + } + + public StudentCodeFixture member(Member member) { + this.member = member; + return this; + } + + public StudentCodeFixture username(String username) { + this.username = username; + return this; + } + + public StudentCodeFixture issuedAt(LocalDateTime issuedAt) { + this.issuedAt = issuedAt; + return this; + } + + public StudentCode build() { + return new StudentCode(id, code, school, member, username, issuedAt); + } +} From 1356bbc96a7f544dffad5fc085a023e8e6af43b7 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 00:26:24 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20duplicated=20key=20exception?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20insert=EA=B0=80=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/festago/student/application/StudentService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/festago/student/application/StudentService.java b/backend/src/main/java/com/festago/student/application/StudentService.java index 64b4aed8c..be29c2bcf 100644 --- a/backend/src/main/java/com/festago/student/application/StudentService.java +++ b/backend/src/main/java/com/festago/student/application/StudentService.java @@ -49,8 +49,7 @@ public void sendVerificationMail(Long memberId, StudentSendMailRequest request) School school = findSchool(request.schoolId()); validate(memberId, request); VerificationCode code = codeProvider.provide(); - studentCodeRepository.deleteByMember(member); - studentCodeRepository.save(new StudentCode(code, school, member, request.username())); + saveStudentCode(request, member, code, school); mailClient.send(new VerificationMailPayload(code, request.username(), school.getDomain())); } @@ -91,6 +90,12 @@ private void validateDuplicateEmail(StudentSendMailRequest request) { } } + private void saveStudentCode(StudentSendMailRequest request, Member member, VerificationCode code, School school) { + studentCodeRepository.deleteByMember(member); + studentRepository.flush(); + studentCodeRepository.save(new StudentCode(code, school, member, request.username())); + } + public void verificate(Long memberId, StudentVerificateRequest request) { validateStudent(memberId); Member member = findMember(memberId); From af0550cc5dcfcfa75fc07a706712188983b065cd Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 00:40:36 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=99=80=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=91=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/StudentServiceTest.java | 231 ------------------ .../application/StudentServiceTest.java | 58 +++++ 2 files changed, 58 insertions(+), 231 deletions(-) delete mode 100644 backend/src/test/java/com/festago/application/StudentServiceTest.java diff --git a/backend/src/test/java/com/festago/application/StudentServiceTest.java b/backend/src/test/java/com/festago/application/StudentServiceTest.java deleted file mode 100644 index 41e94d388..000000000 --- a/backend/src/test/java/com/festago/application/StudentServiceTest.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.festago.application; - -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; - -import com.festago.common.exception.BadRequestException; -import com.festago.common.exception.NotFoundException; -import com.festago.member.domain.Member; -import com.festago.member.repository.MemberRepository; -import com.festago.school.domain.School; -import com.festago.school.repository.SchoolRepository; -import com.festago.student.application.MailClient; -import com.festago.student.application.StudentService; -import com.festago.student.application.VerificationCodeProvider; -import com.festago.student.domain.StudentCode; -import com.festago.student.domain.VerificationCode; -import com.festago.student.dto.StudentSendMailRequest; -import com.festago.student.dto.StudentVerificateRequest; -import com.festago.student.repository.StudentCodeRepository; -import com.festago.student.repository.StudentRepository; -import com.festago.support.MemberFixture; -import java.util.Optional; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Nested; -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; - -@DisplayNameGeneration(ReplaceUnderscores.class) -@SuppressWarnings("NonAsciiCharacters") -@ExtendWith(MockitoExtension.class) -class StudentServiceTest { - - @InjectMocks - StudentService studentService; - - @Mock - MailClient mailClient; - - @Mock - VerificationCodeProvider codeProvider; - - @Mock - StudentCodeRepository studentCodeRepository; - - @Mock - SchoolRepository schoolRepository; - - @Mock - MemberRepository memberRepository; - - @Mock - StudentRepository studentRepository; - - @Nested - class 인증_메일_전송 { - - @Test - void 이미_학생인증정보가_존재하면_예외() { - // given - Long memberId = 1L; - Long schoolId = 1L; - String username = "user"; - StudentSendMailRequest request = new StudentSendMailRequest(username, schoolId); - given(studentRepository.existsByMemberId(memberId)) - .willReturn(true); - - // when & then - assertThatThrownBy(() -> studentService.sendVerificationMail(memberId, request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("이미 학교 인증이 완료된 사용자입니다."); - } - - @Test - void 중복되는_이메일이면_예외() { - // given - Long memberId = 1L; - Long schoolId = 1L; - String username = "user"; - StudentSendMailRequest request = new StudentSendMailRequest(username, schoolId); - - given(studentRepository.existsByMemberId(memberId)) - .willReturn(false); - given(studentRepository.existsByUsernameAndSchoolId(username, schoolId)) - .willReturn(true); - - // when & then - assertThatThrownBy(() -> studentService.sendVerificationMail(memberId, request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("이미 인증된 이메일입니다."); - } - - @Test - void 멤버가_없으면_예외() { - // given - Long memberId = 1L; - Long schoolId = 1L; - String username = "user"; - StudentSendMailRequest request = new StudentSendMailRequest(username, schoolId); - - given(studentRepository.existsByMemberId(memberId)) - .willReturn(false); - given(studentRepository.existsByUsernameAndSchoolId(username, schoolId)) - .willReturn(false); - given(memberRepository.findById(memberId)) - .willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> studentService.sendVerificationMail(memberId, request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 멤버입니다."); - } - - @Test - void 학교가_없으면_예외() { - // given - Long memberId = 1L; - Long schoolId = 1L; - String username = "user"; - Member member = MemberFixture.member() - .id(memberId) - .build(); - StudentSendMailRequest request = new StudentSendMailRequest(username, schoolId); - - given(studentRepository.existsByMemberId(memberId)) - .willReturn(false); - given(studentRepository.existsByUsernameAndSchoolId(username, schoolId)) - .willReturn(false); - given(memberRepository.findById(memberId)) - .willReturn(Optional.empty()); - given(memberRepository.findById(memberId)) - .willReturn(Optional.of(member)); - given(schoolRepository.findById(schoolId)) - .willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> studentService.sendVerificationMail(memberId, request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 학교입니다."); - } - - - @Test - void 성공() { - // given - Long memberId = 1L; - Long schoolId = 1L; - String username = "user"; - Member member = MemberFixture.member() - .id(memberId) - .build(); - StudentSendMailRequest request = new StudentSendMailRequest(username, schoolId); - - given(studentRepository.existsByMemberId(memberId)) - .willReturn(false); - given(studentRepository.existsByUsernameAndSchoolId(username, schoolId)) - .willReturn(false); - given(memberRepository.findById(memberId)) - .willReturn(Optional.of(member)); - given(schoolRepository.findById(schoolId)) - .willReturn(Optional.of(new School(schoolId, "festago.ac.kr", "페스타고대학교"))); - given(codeProvider.provide()) - .willReturn(new VerificationCode("123456")); - - // when & then - assertThatNoException() - .isThrownBy(() -> studentService.sendVerificationMail(memberId, request)); - } - } - - @Nested - class 학생_인증 { - - @Test - void 이미_학생인증정보가_존재하면_예외() { - // given - Long memberId = 1L; - StudentVerificateRequest request = new StudentVerificateRequest("123456"); - given(studentRepository.existsByMemberId(memberId)) - .willReturn(true); - - // when & then - assertThatThrownBy(() -> studentService.verificate(memberId, request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("이미 학교 인증이 완료된 사용자입니다."); - } - - @Test - void 인증_코드가_존재하지_않으면_예외() { - // given - Long memberId = 1L; - StudentVerificateRequest request = new StudentVerificateRequest("123456"); - given(memberRepository.findById(anyLong())) - .willReturn(Optional.of(MemberFixture.member().build())); - given(studentCodeRepository.findByCodeAndMember(any(), any())) - .willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> studentService.verificate(memberId, request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("올바르지 않은 학생 인증 코드입니다."); - } - - @Test - void 성공() { - // given - Long memberId = 1L; - StudentVerificateRequest request = new StudentVerificateRequest("123456"); - Member member = MemberFixture.member().build(); - given(memberRepository.findById(anyLong())) - .willReturn(Optional.of(member)); - given(studentCodeRepository.findByCodeAndMember(any(), any())) - .willReturn(Optional.of(new StudentCode( - new VerificationCode("123456"), - new School("snu.ac.kr", "서울대학교"), - member, - "ohs" - ))); - - // when & then - assertThatNoException() - .isThrownBy(() -> studentService.verificate(memberId, request)); - } - } -} diff --git a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java index 26ddcf9da..79b559214 100644 --- a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java +++ b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -14,7 +15,9 @@ import com.festago.school.domain.School; import com.festago.school.repository.SchoolRepository; import com.festago.student.domain.StudentCode; +import com.festago.student.domain.VerificationCode; import com.festago.student.dto.StudentSendMailRequest; +import com.festago.student.dto.StudentVerificateRequest; import com.festago.student.infrastructure.MockMailClient; import com.festago.student.infrastructure.RandomVerificationCodeProvider; import com.festago.student.repository.StudentCodeRepository; @@ -163,4 +166,59 @@ void setUp() { .isThrownBy(() -> studentService.sendVerificationMail(1L, request)); } } + + @Nested + class 학생_인증 { + + @Test + void 이미_학생인증정보가_존재하면_예외() { + // given + Long memberId = 1L; + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + given(studentRepository.existsByMemberId(memberId)) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> studentService.verificate(memberId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 학교 인증이 완료된 사용자입니다."); + } + + @Test + void 인증_코드가_존재하지_않으면_예외() { + // given + Long memberId = 1L; + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(MemberFixture.member().build())); + given(studentCodeRepository.findByCodeAndMember(any(), any())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> studentService.verificate(memberId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("올바르지 않은 학생 인증 코드입니다."); + } + + @Test + void 성공() { + // given + Long memberId = 1L; + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + Member member = MemberFixture.member().build(); + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(member)); + given(studentCodeRepository.findByCodeAndMember(any(), any())) + .willReturn(Optional.of(new StudentCode( + new VerificationCode("123456"), + new School("snu.ac.kr", "서울대학교"), + member, + "ohs" + ))); + + // when & then + assertThatNoException() + .isThrownBy(() -> studentService.verificate(memberId, request)); + } + } } From e7460e83a9e2ea520d554ca7b721511440bf52ba Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 01:14:21 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20SetUpMockito=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/StudentServiceTest.java | 32 +++++++++---------- .../com/festago/support/SetUpMockito.java | 25 +++++++++++++++ 2 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 backend/src/test/java/com/festago/support/SetUpMockito.java diff --git a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java index 79b559214..eb01f81cd 100644 --- a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java +++ b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java @@ -6,7 +6,6 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.lenient; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.NotFoundException; @@ -24,6 +23,7 @@ import com.festago.student.repository.StudentRepository; import com.festago.support.MemberFixture; import com.festago.support.SchoolFixture; +import com.festago.support.SetUpMockito; import com.festago.support.StudentCodeFixture; import java.time.Clock; import java.time.LocalDateTime; @@ -79,21 +79,21 @@ void setUp() { Member member = MemberFixture.member().id(1L).build(); School school = SchoolFixture.school().id(1L).build(); - lenient() - .when(memberRepository.findById(anyLong())) - .thenReturn(Optional.of(member)); - lenient() - .when(schoolRepository.findById(anyLong())) - .thenReturn(Optional.of(school)); - lenient() - .when(studentCodeRepository.findByMemberId(anyLong())) - .thenReturn(Optional.empty()); - lenient() - .when(studentRepository.existsByMemberId(anyLong())) - .thenReturn(false); - lenient() - .when(studentRepository.existsByUsernameAndSchoolId(anyString(), anyLong())) - .thenReturn(false); + SetUpMockito + .given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(member)); + SetUpMockito + .given(schoolRepository.findById(anyLong())) + .willReturn(Optional.of(school)); + SetUpMockito + .given(studentCodeRepository.findByMemberId(anyLong())) + .willReturn(Optional.empty()); + SetUpMockito + .given(studentRepository.existsByMemberId(anyLong())) + .willReturn(false); + SetUpMockito + .given(studentRepository.existsByUsernameAndSchoolId(anyString(), anyLong())) + .willReturn(false); } @Test diff --git a/backend/src/test/java/com/festago/support/SetUpMockito.java b/backend/src/test/java/com/festago/support/SetUpMockito.java new file mode 100644 index 000000000..98357391d --- /dev/null +++ b/backend/src/test/java/com/festago/support/SetUpMockito.java @@ -0,0 +1,25 @@ +package com.festago.support; + +import org.mockito.Mockito; +import org.mockito.stubbing.OngoingStubbing; + +public class SetUpMockito { + + public static Given given(T methodCall) { + OngoingStubbing ongoingStubbing = Mockito.lenient().when(methodCall); + return new Given<>(ongoingStubbing); + } + + public static class Given { + + private final OngoingStubbing ongoingStubbing; + + public Given(OngoingStubbing ongoingStubbing) { + this.ongoingStubbing = ongoingStubbing; + } + + public void willReturn(T value) { + ongoingStubbing.thenReturn(value); + } + } +} From 20877906d95ee015b9385a1e8ad3c1dad82801a8 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 14:49:53 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20studentCode=20unique=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/festago/student/domain/StudentCode.java | 12 ++---------- .../db/migration/V6__studnet_code__unique.sql | 11 +++++++++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index 6c1190a57..581e01b09 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -13,23 +13,14 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; import java.time.LocalDateTime; import org.springframework.data.annotation.CreatedDate; import org.springframework.util.StringUtils; @Entity -@Table( - uniqueConstraints = { - @UniqueConstraint( - name = "unique_username__school", - columnNames = {"username", "school_id"} - ) - } -) public class StudentCode extends BaseTimeEntity { private static final int MIN_REQUEST_TERM_SECONDS = 30; @@ -45,6 +36,7 @@ public class StudentCode extends BaseTimeEntity { private School school; @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(unique = true) private Member member; private String username; diff --git a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql index 63c4929cd..555b97a7f 100644 --- a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql +++ b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql @@ -1,5 +1,12 @@ -alter table student_code - add constraint unique_username__school unique (username, school_id); +create index temp_member_index + on student_code (member_id); + +drop index fk_student_code__member on student_code; + +create unique index fk_student_code__member + on student_code (member_id); + +drop index temp_member_index on student_code; alter table student_code add column issued_at datetime(6); From 2df5dc3d0b09890266011e3f4db7b9467bd250aa Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 16:04:35 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20StudentCode=EC=97=90=EC=84=9C?= =?UTF-8?q?=20BaseTimeEntity=20=EC=83=81=EC=86=8D=EB=B0=9B=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/festago/student/domain/StudentCode.java | 3 +-- .../resources/db/migration/V6__studnet_code__unique.sql | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index 581e01b09..112e9f8df 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -2,7 +2,6 @@ import static java.time.temporal.ChronoUnit.SECONDS; -import com.festago.common.domain.BaseTimeEntity; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.InternalServerException; import com.festago.member.domain.Member; @@ -21,7 +20,7 @@ import org.springframework.util.StringUtils; @Entity -public class StudentCode extends BaseTimeEntity { +public class StudentCode { private static final int MIN_REQUEST_TERM_SECONDS = 30; diff --git a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql index 555b97a7f..2a18d6b61 100644 --- a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql +++ b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql @@ -10,3 +10,9 @@ drop index temp_member_index on student_code; alter table student_code add column issued_at datetime(6); + +alter table student_code + drop column created_at; + +alter table student_code + drop column updated_at; From b906f9b149108274376e432f144b7de43a07fba2 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 19:07:03 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20username=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EB=A1=9C=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/festago/student/application/StudentService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/festago/student/application/StudentService.java b/backend/src/main/java/com/festago/student/application/StudentService.java index be29c2bcf..a99213151 100644 --- a/backend/src/main/java/com/festago/student/application/StudentService.java +++ b/backend/src/main/java/com/festago/student/application/StudentService.java @@ -49,7 +49,7 @@ public void sendVerificationMail(Long memberId, StudentSendMailRequest request) School school = findSchool(request.schoolId()); validate(memberId, request); VerificationCode code = codeProvider.provide(); - saveStudentCode(request, member, code, school); + saveStudentCode(code, member, school, request.username()); mailClient.send(new VerificationMailPayload(code, request.username(), school.getDomain())); } @@ -90,10 +90,10 @@ private void validateDuplicateEmail(StudentSendMailRequest request) { } } - private void saveStudentCode(StudentSendMailRequest request, Member member, VerificationCode code, School school) { + private void saveStudentCode(VerificationCode code, Member member, School school, String username) { studentCodeRepository.deleteByMember(member); studentRepository.flush(); - studentCodeRepository.save(new StudentCode(code, school, member, request.username())); + studentCodeRepository.save(new StudentCode(code, school, member, username)); } public void verificate(Long memberId, StudentVerificateRequest request) { From 0556b14bea7c17721c68bd8abc4ee6faf07ab1ce Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 19:32:52 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor:=20studentCode=20issued=5Fat=20n?= =?UTF-8?q?ot=20null=20=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../festago/student/domain/StudentCode.java | 6 +++++ .../db/migration/V6__studnet_code__unique.sql | 25 ++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index 112e9f8df..eeb7dc5ee 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -8,6 +8,7 @@ import com.festago.school.domain.School; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -15,11 +16,15 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.util.StringUtils; + @Entity +@EntityListeners(AuditingEntityListener.class) public class StudentCode { private static final int MIN_REQUEST_TERM_SECONDS = 30; @@ -40,6 +45,7 @@ public class StudentCode { private String username; + @NotNull @CreatedDate private LocalDateTime issuedAt; diff --git a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql index 2a18d6b61..7fbcba355 100644 --- a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql +++ b/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql @@ -1,3 +1,19 @@ +-- issued_at 칼럼 추가 (NOT NULL) +alter table student_code + add column issued_at datetime(6) not null + default '1999-12-31 00:00:00'; + +alter table student_code + alter column issued_at drop default; + +-- 기존 created_at updated_at 삭제 +alter table student_code + drop column created_at; + +alter table student_code + drop column updated_at; + +-- StudentCode의 member_id UNIQUE 제약조건 추가 create index temp_member_index on student_code (member_id); @@ -7,12 +23,3 @@ create unique index fk_student_code__member on student_code (member_id); drop index temp_member_index on student_code; - -alter table student_code - add column issued_at datetime(6); - -alter table student_code - drop column created_at; - -alter table student_code - drop column updated_at; From 7d56a085b521965b6d9d96d91a63a2def2013109 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Wed, 27 Sep 2023 20:32:09 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refactor:=20=ED=95=99=EC=83=9D=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EB=A1=9C=EC=A7=81=20update=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../student/application/StudentService.java | 8 +++-- .../festago/student/domain/StudentCode.java | 10 ++++-- .../student/domain/StudentCodeTest.java | 32 +++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/festago/student/application/StudentService.java b/backend/src/main/java/com/festago/student/application/StudentService.java index a99213151..0b0b306ea 100644 --- a/backend/src/main/java/com/festago/student/application/StudentService.java +++ b/backend/src/main/java/com/festago/student/application/StudentService.java @@ -91,9 +91,11 @@ private void validateDuplicateEmail(StudentSendMailRequest request) { } private void saveStudentCode(VerificationCode code, Member member, School school, String username) { - studentCodeRepository.deleteByMember(member); - studentRepository.flush(); - studentCodeRepository.save(new StudentCode(code, school, member, username)); + studentCodeRepository.findByMemberId(member.getId()) + .ifPresentOrElse( + studentCode -> studentCode.reissue(code, school, username), + () -> studentCodeRepository.save(new StudentCode(code, school, member, username)) + ); } public void verificate(Long memberId, StudentVerificateRequest request) { diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index eeb7dc5ee..c74678408 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -18,7 +18,7 @@ import jakarta.persistence.OneToOne; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; -import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.util.StringUtils; @@ -46,7 +46,7 @@ public class StudentCode { private String username; @NotNull - @CreatedDate + @LastModifiedDate private LocalDateTime issuedAt; protected StudentCode() { @@ -81,6 +81,12 @@ public boolean canReissue(LocalDateTime currentTime) { return SECONDS.between(issuedAt, currentTime) > MIN_REQUEST_TERM_SECONDS; } + public void reissue(VerificationCode code, School school, String username) { + this.code = code; + this.school = school; + this.username = username; + } + public Long getId() { return id; } diff --git a/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java b/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java index bcca7fa93..146ca1456 100644 --- a/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java +++ b/backend/src/test/java/com/festago/student/domain/StudentCodeTest.java @@ -1,6 +1,7 @@ package com.festago.student.domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.festago.member.domain.Member; import com.festago.school.domain.School; @@ -53,4 +54,35 @@ class 재발급_가능한지_확인 { assertThat(actual).isEqualTo(true); } } + + @Test + void 학생인증코드_재발급() { + // given + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime issuedAt = currentTime.minusSeconds(31); + + VerificationCode oldCode = new VerificationCode("111111"); + VerificationCode newCode = new VerificationCode("222222"); + + School oldSchool = SchoolFixture.school().build(); + School newSchool = SchoolFixture.school().build(); + + String oldUsername = "ash"; + String newUsername = "pooh"; + + Member member = MemberFixture.member().build(); + + StudentCode studentCode = new StudentCode(1L, oldCode, oldSchool, member, oldUsername, issuedAt); + + // when + studentCode.reissue(newCode, newSchool, newUsername); + + // then + assertSoftly(softly -> { + softly.assertThat(studentCode.getCode()).isEqualTo(newCode); + softly.assertThat(studentCode.getSchool()).isEqualTo(newSchool); + softly.assertThat(studentCode.getUsername()).isEqualTo(newUsername); + softly.assertThat(studentCode.getMember()).isEqualTo(member); + }); + } } From 8a9bd4397dda454415e850b57fbe853a6e4b5a7b Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Mon, 2 Oct 2023 16:42:04 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20429=20Too=20Many=20Request=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/festago/common/exception/ErrorCode.java | 4 +++- .../common/exception/TooManyRequestException.java | 8 ++++++++ .../festago/presentation/GlobalExceptionHandler.java | 10 +++++++++- .../festago/student/application/StudentService.java | 3 ++- .../student/application/StudentServiceTest.java | 6 ++++-- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/festago/common/exception/TooManyRequestException.java diff --git a/backend/src/main/java/com/festago/common/exception/ErrorCode.java b/backend/src/main/java/com/festago/common/exception/ErrorCode.java index a3e290422..d2e3b556f 100644 --- a/backend/src/main/java/com/festago/common/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/common/exception/ErrorCode.java @@ -25,7 +25,6 @@ public enum ErrorCode { DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."), TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."), INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."), - TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."), // 401 @@ -48,6 +47,9 @@ public enum ErrorCode { TICKET_NOT_FOUND("존재하지 않는 티켓입니다."), SCHOOL_NOT_FOUND("존재하지 않는 학교입니다."), + // 429 + TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."), + // 500 INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."), INVALID_ENTRY_CODE_PERIOD("올바르지 않은 입장코드 유효기간입니다."), diff --git a/backend/src/main/java/com/festago/common/exception/TooManyRequestException.java b/backend/src/main/java/com/festago/common/exception/TooManyRequestException.java new file mode 100644 index 000000000..5c02f7955 --- /dev/null +++ b/backend/src/main/java/com/festago/common/exception/TooManyRequestException.java @@ -0,0 +1,8 @@ +package com.festago.common.exception; + +public class TooManyRequestException extends FestaGoException { + + public TooManyRequestException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java b/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java index 1bc3b023b..08e328fb9 100644 --- a/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/festago/presentation/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import com.festago.common.exception.ForbiddenException; import com.festago.common.exception.InternalServerException; import com.festago.common.exception.NotFoundException; +import com.festago.common.exception.TooManyRequestException; import com.festago.common.exception.UnauthorizedException; import com.festago.common.exception.dto.ErrorResponse; import com.festago.presentation.auth.AuthenticateContext; @@ -89,6 +90,12 @@ public ResponseEntity handle(NotFoundException e, HttpServletRequ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e)); } + @ExceptionHandler(TooManyRequestException.class) + public ResponseEntity handle(TooManyRequestException e, HttpServletRequest request) { + logInfo(e, request); + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(ErrorResponse.from(e)); + } + @ExceptionHandler(InternalServerException.class) public ResponseEntity handle(InternalServerException e, HttpServletRequest request) { logWarn(e, request); @@ -104,7 +111,8 @@ public ResponseEntity handle(Exception e, HttpServletRequest requ } @Override - protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException e, + HttpHeaders headers, HttpStatusCode status, WebRequest request) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ErrorResponse.from(ErrorCode.INVALID_REQUEST_ARGUMENT)); diff --git a/backend/src/main/java/com/festago/student/application/StudentService.java b/backend/src/main/java/com/festago/student/application/StudentService.java index 0b0b306ea..6f7068647 100644 --- a/backend/src/main/java/com/festago/student/application/StudentService.java +++ b/backend/src/main/java/com/festago/student/application/StudentService.java @@ -3,6 +3,7 @@ import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.NotFoundException; +import com.festago.common.exception.TooManyRequestException; import com.festago.member.domain.Member; import com.festago.member.repository.MemberRepository; import com.festago.school.domain.School; @@ -73,7 +74,7 @@ private void validateFrequentRequest(Long memberId) { studentCodeRepository.findByMemberId(memberId) .ifPresent(code -> { if (!code.canReissue(LocalDateTime.now(clock))) { - throw new BadRequestException(ErrorCode.TOO_FREQUENT_REQUESTS); + throw new TooManyRequestException(ErrorCode.TOO_FREQUENT_REQUESTS); } }); } diff --git a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java index eb01f81cd..b643fb3d2 100644 --- a/backend/src/test/java/com/festago/student/application/StudentServiceTest.java +++ b/backend/src/test/java/com/festago/student/application/StudentServiceTest.java @@ -1,5 +1,6 @@ package com.festago.student.application; +import static com.festago.common.exception.ErrorCode.TOO_FREQUENT_REQUESTS; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -9,6 +10,7 @@ import com.festago.common.exception.BadRequestException; import com.festago.common.exception.NotFoundException; +import com.festago.common.exception.TooManyRequestException; import com.festago.member.domain.Member; import com.festago.member.repository.MemberRepository; import com.festago.school.domain.School; @@ -132,8 +134,8 @@ void setUp() { // when assertThatThrownBy(() -> studentService.sendVerificationMail(1L, request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."); + .isInstanceOf(TooManyRequestException.class) + .hasMessage(TOO_FREQUENT_REQUESTS.getMessage()); } @Test From f896daf49f8efbdcca2fb7cb1b524d90a9a48adb Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Fri, 6 Oct 2023 14:32:46 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20flyway=20version=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...{V6__studnet_code__unique.sql => V7__studnet_code__unique.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/main/resources/db/migration/{V6__studnet_code__unique.sql => V7__studnet_code__unique.sql} (100%) diff --git a/backend/src/main/resources/db/migration/V6__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V7__studnet_code__unique.sql similarity index 100% rename from backend/src/main/resources/db/migration/V6__studnet_code__unique.sql rename to backend/src/main/resources/db/migration/V7__studnet_code__unique.sql From 5d463db0eaa6077842ca90c6e71d4bbefd813f0a Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Fri, 6 Oct 2023 14:41:24 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor:=20StudentCode=20BaseTimeEntity?= =?UTF-8?q?=20=EC=83=81=EC=86=8D=EB=B0=9B=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/festago/student/domain/StudentCode.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index 7a93d8a2d..899993530 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -2,7 +2,6 @@ import static java.time.temporal.ChronoUnit.SECONDS; -import com.festago.common.domain.BaseTimeEntity; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.InternalServerException; import com.festago.member.domain.Member; @@ -28,7 +27,7 @@ @Entity @EntityListeners(AuditingEntityListener.class) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class StudentCode extends BaseTimeEntity { +public class StudentCode { private static final int MIN_REQUEST_TERM_SECONDS = 30; From d02283845fe37ce32260486b38937902f7ef6042 Mon Sep 17 00:00:00 2001 From: xxeol2 Date: Fri, 6 Oct 2023 14:42:42 +0900 Subject: [PATCH 16/16] =?UTF-8?q?refactor:=20student=5Fcode=20UNIQUE=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20fly?= =?UTF-8?q?way=20script=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/V7__studnet_code__unique.sql | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/backend/src/main/resources/db/migration/V7__studnet_code__unique.sql b/backend/src/main/resources/db/migration/V7__studnet_code__unique.sql index 7fbcba355..79340f120 100644 --- a/backend/src/main/resources/db/migration/V7__studnet_code__unique.sql +++ b/backend/src/main/resources/db/migration/V7__studnet_code__unique.sql @@ -14,12 +14,6 @@ alter table student_code drop column updated_at; -- StudentCode의 member_id UNIQUE 제약조건 추가 -create index temp_member_index - on student_code (member_id); - -drop index fk_student_code__member on student_code; - -create unique index fk_student_code__member - on student_code (member_id); - -drop index temp_member_index on student_code; +alter table student_code + modify column member_id bigint unique; +정