Skip to content

Commit

Permalink
[BE] feat: 이메일 코드를 통한 학생 인증 기능을 추가 (#430) (#434)
Browse files Browse the repository at this point in the history
* feat: 학생 인증 기능 추가

* refactor: 인증 코드 입력시 회원 검증 하도록 변경

* feat: flyway 반영

* fix: submodule 버전 수정

---------

Co-authored-by: seokjin8678 <[email protected]>
  • Loading branch information
carsago and seokjin8678 authored Sep 11, 2023
1 parent c679560 commit 8d69c94
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import com.festago.domain.MemberRepository;
import com.festago.domain.School;
import com.festago.domain.SchoolRepository;
import com.festago.domain.Student;
import com.festago.domain.StudentCode;
import com.festago.domain.StudentCodeRepository;
import com.festago.domain.StudentRepository;
import com.festago.domain.VerificationCode;
import com.festago.domain.VerificationCodeProvider;
import com.festago.domain.VerificationMailPayload;
import com.festago.dto.StudentSendMailRequest;
import com.festago.dto.StudentVerificateRequest;
import com.festago.exception.BadRequestException;
import com.festago.exception.ErrorCode;
import com.festago.exception.NotFoundException;
Expand Down Expand Up @@ -47,7 +49,7 @@ public void sendVerificationMail(Long memberId, StudentSendMailRequest request)
School school = findSchool(request.schoolId());
VerificationCode code = codeProvider.provide();
studentCodeRepository.deleteByMember(member);
studentCodeRepository.save(new StudentCode(code, member, school));
studentCodeRepository.save(new StudentCode(code, school, member, request.username()));
mailClient.send(new VerificationMailPayload(code, request.username(), school.getDomain()));
}

Expand All @@ -72,4 +74,13 @@ 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)
.orElseThrow(() -> new BadRequestException(ErrorCode.INVALID_STUDENT_VERIFICATION_CODE));
studentRepository.save(new Student(member, studentCode.getSchool(), studentCode.getUsername()));
studentCodeRepository.deleteByMember(member);
}
}
4 changes: 4 additions & 0 deletions backend/src/main/java/com/festago/domain/School.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public class School extends BaseTimeEntity {
protected School() {
}

public School(String domain, String name) {
this(null, domain, name);
}

public School(Long id, String domain, String name) {
validate(domain, name);
this.id = id;
Expand Down
4 changes: 4 additions & 0 deletions backend/src/main/java/com/festago/domain/Student.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public class Student extends BaseTimeEntity {
protected Student() {
}

public Student(Member member, School school, String username) {
this(null, member, school, username);
}

public Student(Long id, Member member, School school, String username) {
validate(member, school, username);
this.id = id;
Expand Down
29 changes: 25 additions & 4 deletions backend/src/main/java/com/festago/domain/StudentCode.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.festago.domain;

import com.festago.exception.ErrorCode;
import com.festago.exception.InternalServerException;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -8,6 +10,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import org.springframework.util.StringUtils;

@Entity
public class StudentCode extends BaseTimeEntity {
Expand All @@ -25,18 +28,32 @@ public class StudentCode extends BaseTimeEntity {
@OneToOne(fetch = FetchType.LAZY)
private Member member;

private String username;

protected StudentCode() {
}

public StudentCode(VerificationCode code, Member member, School school) {
this(null, code, member, school);
public StudentCode(VerificationCode code, School school, Member member, String username) {
this(null, code, school, member, username);
}

public StudentCode(Long id, VerificationCode code, Member member, School school) {
public StudentCode(Long id, VerificationCode code, School school, Member member, String username) {
validate(username);
this.id = id;
this.code = code;
this.member = member;
this.school = school;
this.member = member;
this.username = username;
}

private void validate(String username) {
if (!StringUtils.hasText(username)) {
throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR);
}

if (username.length() > 255) {
throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

public Long getId() {
Expand All @@ -54,4 +71,8 @@ public School getSchool() {
public Member getMember() {
return member;
}

public String getUsername() {
return username;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.festago.domain;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentCodeRepository extends JpaRepository<StudentCode, Long> {

void deleteByMember(Member member);

Optional<StudentCode> findByCodeAndMember(VerificationCode code, Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.festago.dto;

public record StudentVerificateRequest(String code) {

}
1 change: 1 addition & 0 deletions backend/src/main/java/com/festago/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public enum ErrorCode {
ALREADY_STUDENT_VERIFIED("이미 학교 인증이 완료된 사용자입니다."),
DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."),
TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."),
INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."),

// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.festago.application.StudentService;
import com.festago.auth.annotation.Member;
import com.festago.dto.StudentSendMailRequest;
import com.festago.dto.StudentVerificateRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -30,4 +31,13 @@ public ResponseEntity<Void> sendEmail(@Member Long memberId,
return ResponseEntity.ok()
.build();
}

@PostMapping("/verification")
@Operation(description = "학교 인증을 수행한다.", summary = "학생 인증 수행")
public ResponseEntity<Void> verificate(@Member Long memberId,
@RequestBody StudentVerificateRequest request) {
studentService.verificate(memberId, request);
return ResponseEntity.ok()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table student_code
add column username varchar(255);
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

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.domain.MailClient;
import com.festago.domain.Member;
import com.festago.domain.MemberRepository;
import com.festago.domain.School;
import com.festago.domain.SchoolRepository;
import com.festago.domain.StudentCode;
import com.festago.domain.StudentCodeRepository;
import com.festago.domain.StudentRepository;
import com.festago.domain.VerificationCode;
import com.festago.domain.VerificationCodeProvider;
import com.festago.dto.StudentSendMailRequest;
import com.festago.dto.StudentVerificateRequest;
import com.festago.exception.BadRequestException;
import com.festago.exception.NotFoundException;
import com.festago.support.MemberFixture;
Expand Down Expand Up @@ -168,4 +172,59 @@ class 인증_메일_전송 {
.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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.festago.application.StudentService;
import com.festago.dto.StudentSendMailRequest;
import com.festago.dto.StudentVerificateRequest;
import com.festago.support.CustomWebMvcTest;
import com.festago.support.WithMockAuth;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
Expand All @@ -31,30 +33,64 @@ class StudentControllerTest {
@MockBean
StudentService studentService;

@Test
void 인증이_되지_않으면_401() throws Exception {
// given
StudentSendMailRequest request = new StudentSendMailRequest("user", 1L);
@Nested
class 학생_인증_메일_전송 {

// when & then
mockMvc.perform(post("/students/send-verification")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isUnauthorized());
@Test
void 인증이_되지_않으면_401() throws Exception {
// given
StudentSendMailRequest request = new StudentSendMailRequest("user", 1L);

// when & then
mockMvc.perform(post("/students/send-verification")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockAuth
void 학교_인증_요청() throws Exception {
// given
StudentSendMailRequest request = new StudentSendMailRequest("user", 1L);

// when & then
mockMvc.perform(post("/students/send-verification")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer token")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());

}
}

@Test
@WithMockAuth
void 학교_인증_요청() throws Exception {
// given
StudentSendMailRequest request = new StudentSendMailRequest("user", 1L);
@Nested
class 학생_인증 {

@Test
void 인증이_되지_않으면_401() throws Exception {
// given
StudentVerificateRequest request = new StudentVerificateRequest("123456");

// when & then
mockMvc.perform(post("/students/verification")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isUnauthorized());
}

// when & then
mockMvc.perform(post("/students/send-verification")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer token")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
@Test
@WithMockAuth
void 학교_인증_요청() throws Exception {
// given
StudentVerificateRequest request = new StudentVerificateRequest("123456");

// when & then
mockMvc.perform(post("/students/verification")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer token")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
}
}
}

0 comments on commit 8d69c94

Please sign in to comment.