diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 5638c024a..1e433dd32 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -83,6 +83,7 @@ public enum ErrorCode { // school email verification SCHOOL_EMAIL_ALREADY_VERIFIED(HttpStatus.BAD_REQUEST.value(), "이미 학교 이메일 인증이 완료되었습니다."), + SCHOOL_EMAIL_ALREADY_USED(HttpStatus.CONFLICT.value(), "이미 인증에 사용된 학교 이메일입니다."), SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 학교 이메일 도메인입니다."), SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "학교 이메일 인증 요청을 찾을 수 없습니다. 인증 코드 발송을 다시 요청해주세요."), SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT(HttpStatus.BAD_REQUEST.value(), "인증 코드가 일치하지 않습니다."), diff --git a/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java b/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java index fb30ea44d..792177306 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java @@ -22,6 +22,9 @@ public class UpdateViewCountService { public void updateViewCount(String key) { Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key); Long viewCount = postRedisManager.getAndDeleteViewCount(key); + if (viewCount == null) { + return; + } postRepository.increaseViewCount(postId, viewCount); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index f072e9eb8..758d2ef4b 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -30,6 +30,10 @@ @UniqueConstraint( name = "uk_site_user_nickname", columnNames = {"nickname"} + ), + @UniqueConstraint( + name = "uk_site_user_verified_school_email", + columnNames = {"verified_school_email"} ) }) public class SiteUser extends BaseEntity { @@ -54,6 +58,9 @@ public class SiteUser extends BaseEntity { @Column(name = "home_university_id", nullable = true) private Long homeUniversityId; + @Column(name = "verified_school_email", nullable = true, length = 100) + private String verifiedSchoolEmail; + @Setter @Column(name = "profile_image_url", length = 500) private String profileImageUrl; @@ -159,8 +166,9 @@ public void updateUserStatus(UserStatus status) { this.userStatus = status; } - public void verifySchool(Long homeUniversityId) { + public void verifySchool(Long homeUniversityId, String verifiedSchoolEmail) { this.homeUniversityId = homeUniversityId; + this.verifiedSchoolEmail = verifiedSchoolEmail; } public void becomeMentor() { diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java index 46f1e9bec..34ebfd9ce 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequest.java @@ -2,10 +2,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; public record SchoolEmailRequest( @NotBlank(message = "학교 이메일은 필수입니다") @Email(message = "올바른 이메일 형식이 아닙니다") + @Size(max = 100, message = "학교 이메일은 100자 이하여야 합니다") String schoolEmail ) { diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index 3d02c77e5..cf33a49e9 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -20,6 +20,8 @@ public interface SiteUserRepository extends JpaRepository, SiteU boolean existsByNickname(String nickname); + boolean existsByVerifiedSchoolEmail(String verifiedSchoolEmail); + @Query("SELECT u FROM SiteUser u WHERE u.quitedAt <= :cutoffDate") List findUsersToBeRemoved(@Param("cutoffDate") LocalDate cutoffDate); diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java index ec8d9c8fc..026704f71 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SchoolEmailService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.siteuser.service; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_VERIFIED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_USED; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED; @@ -17,9 +18,11 @@ import com.example.solidconnection.university.repository.HomeUniversityRepository; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Locale; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,15 +49,17 @@ public void requestSchoolEmailVerification(long siteUserId, String schoolEmail) throw new CustomException(SCHOOL_EMAIL_ALREADY_VERIFIED); } - String domain = extractEmailDomain(schoolEmail); + String normalizedSchoolEmail = normalizeSchoolEmail(schoolEmail); + String domain = extractEmailDomain(normalizedSchoolEmail); HomeUniversity homeUniversity = homeUniversityRepository.findByEmailDomain(domain) .orElseThrow(() -> new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED)); + validateVerifiedSchoolEmailNotDuplicated(normalizedSchoolEmail); String code = generateVerificationCode(); - saveVerificationInfo(siteUserId, new SchoolVerificationInfo(schoolEmail, homeUniversity.getId(), code)); + saveVerificationInfo(siteUserId, new SchoolVerificationInfo(normalizedSchoolEmail, homeUniversity.getId(), code)); try { - mailService.sendVerificationEmail(schoolEmail, code); + mailService.sendVerificationEmail(normalizedSchoolEmail, code); } catch (Exception e) { redisTemplate.delete(KEY_PREFIX + siteUserId); throw e; @@ -72,10 +77,21 @@ public void confirmSchoolEmail(long siteUserId, String code) { throw new CustomException(SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT); } - siteUser.verifySchool(info.getHomeUniversityId()); + String verifiedSchoolEmail = normalizeSchoolEmail(info.getSchoolEmail()); + validateVerifiedSchoolEmailNotDuplicated(verifiedSchoolEmail); + siteUser.verifySchool(info.getHomeUniversityId(), verifiedSchoolEmail); + flushVerifiedSchoolEmail(); redisTemplate.delete(KEY_PREFIX + siteUserId); } + private void flushVerifiedSchoolEmail() { + try { + siteUserRepository.flush(); + } catch (DataIntegrityViolationException e) { + throw new CustomException(SCHOOL_EMAIL_ALREADY_USED); + } + } + private void saveVerificationInfo(long siteUserId, SchoolVerificationInfo info) { try { redisTemplate.opsForValue().set( @@ -95,15 +111,57 @@ private SchoolVerificationInfo getVerificationInfo(long siteUserId) { throw new CustomException(SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND); } try { - return objectMapper.readValue(jsonInfo, SchoolVerificationInfo.class); + SchoolVerificationInfo info = objectMapper.readValue(jsonInfo, SchoolVerificationInfo.class); + validateVerificationInfo(siteUserId, info); + return info; } catch (JsonProcessingException e) { - redisTemplate.delete(KEY_PREFIX + siteUserId); - throw new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED); + throw corruptedVerificationInfoException(siteUserId); + } + } + + private void validateVerificationInfo(long siteUserId, SchoolVerificationInfo info) { + if (info == null + || isBlank(info.getSchoolEmail()) + || !hasEmailDomain(info.getSchoolEmail()) + || info.getHomeUniversityId() == null + || isBlank(info.getCode()) + || !homeUniversityRepository.existsById(info.getHomeUniversityId())) { + throw corruptedVerificationInfoException(siteUserId); + } + } + + private boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + private CustomException corruptedVerificationInfoException(long siteUserId) { + redisTemplate.delete(KEY_PREFIX + siteUserId); + return new CustomException(SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED); + } + + private void validateVerifiedSchoolEmailNotDuplicated(String verifiedSchoolEmail) { + if (siteUserRepository.existsByVerifiedSchoolEmail(verifiedSchoolEmail)) { + throw new CustomException(SCHOOL_EMAIL_ALREADY_USED); } } + private String normalizeSchoolEmail(String schoolEmail) { + return schoolEmail.trim().toLowerCase(Locale.ROOT); + } + private String extractEmailDomain(String email) { - return email.substring(email.indexOf('@') + 1).toLowerCase(); + if (!hasEmailDomain(email)) { + throw new CustomException(SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED); + } + return email.substring(email.indexOf('@') + 1); + } + + private boolean hasEmailDomain(String email) { + if (email == null) { + return false; + } + int atIndex = email.indexOf('@'); + return atIndex > 0 && atIndex < email.length() - 1; } private String generateVerificationCode() { diff --git a/src/main/resources/db/migration/V57__add_verified_school_email_to_site_user.sql b/src/main/resources/db/migration/V57__add_verified_school_email_to_site_user.sql new file mode 100644 index 000000000..48c4a357a --- /dev/null +++ b/src/main/resources/db/migration/V57__add_verified_school_email_to_site_user.sql @@ -0,0 +1,3 @@ +ALTER TABLE site_user + ADD COLUMN verified_school_email VARCHAR(100) NULL, + ADD CONSTRAINT uk_site_user_verified_school_email UNIQUE (verified_school_email); diff --git a/src/test/java/com/example/solidconnection/community/post/service/UpdateViewCountServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/UpdateViewCountServiceTest.java new file mode 100644 index 000000000..651b15abf --- /dev/null +++ b/src/test/java/com/example/solidconnection/community/post/service/UpdateViewCountServiceTest.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.community.post.service; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.example.solidconnection.community.post.repository.PostRepository; +import org.junit.jupiter.api.DisplayName; +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; + +@DisplayName("조회수 업데이트 서비스 테스트") +@ExtendWith(MockitoExtension.class) +class UpdateViewCountServiceTest { + + @InjectMocks + private UpdateViewCountService updateViewCountService; + + @Mock + private PostRepository postRepository; + + @Mock + private PostRedisManager postRedisManager; + + @Test + void Redis_조회수_키가_이미_소비되었으면_DB_업데이트를_하지_않는다() { + // given + String key = "post:view:1"; + given(postRedisManager.getPostIdFromPostViewCountRedisKey(key)).willReturn(1L); + given(postRedisManager.getAndDeleteViewCount(key)).willReturn(null); + + // when + updateViewCountService.updateViewCount(key); + + // then + then(postRepository).shouldHaveNoInteractions(); + } + + @Test + void Redis_조회수가_있으면_DB_조회수를_증가시킨다() { + // given + String key = "post:view:1"; + given(postRedisManager.getPostIdFromPostViewCountRedisKey(key)).willReturn(1L); + given(postRedisManager.getAndDeleteViewCount(key)).willReturn(2L); + + // when + updateViewCountService.updateViewCount(key); + + // then + then(postRepository).should().increaseViewCount(1L, 2L); + } +} diff --git a/src/test/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequestTest.java b/src/test/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequestTest.java new file mode 100644 index 000000000..674fa3deb --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/dto/SchoolEmailRequestTest.java @@ -0,0 +1,66 @@ +package com.example.solidconnection.siteuser.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("학교 이메일 인증 요청 유효성 검사 테스트") +class SchoolEmailRequestTest { + + private static final String MESSAGE = "message"; + private static final String SCHOOL_EMAIL_MAX_LENGTH_MESSAGE = "학교 이메일은 100자 이하여야 합니다"; + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Nested + class 학교_이메일_길이_검증 { + + @Test + void 학교_이메일이_100자이면_검증을_통과한다() { + // given + SchoolEmailRequest request = new SchoolEmailRequest(emailWithLength(100)); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 학교_이메일이_100자를_초과하면_검증에_실패한다() { + // given + SchoolEmailRequest request = new SchoolEmailRequest(emailWithLength(101)); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .extracting(MESSAGE) + .contains(SCHOOL_EMAIL_MAX_LENGTH_MESSAGE); + } + } + + private String emailWithLength(int length) { + String prefix = "a@"; + String domainPrefix = "b".repeat(63) + "."; + String suffix = ".edu"; + String domain = domainPrefix + "c".repeat(length - prefix.length() - domainPrefix.length() - suffix.length()); + return prefix + domain + suffix; + } +} diff --git a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java index 41ec37251..7ea98622d 100644 --- a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java @@ -88,4 +88,22 @@ class 닉네임은_중복될_수_없다 { .isInstanceOf(DataIntegrityViolationException.class); } } + + @Nested + class 인증된_학교_이메일은_중복될_수_없다 { + + @Test + void 인증된_학교_이메일이_동일한_사용자를_저장하면_예외가_발생한다() { + // given + SiteUser user1 = createSiteUser("email1", "nickname1", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email2", "nickname2", AuthType.KAKAO); + user1.verifySchool(1L, "test@inha.edu"); + user2.verifySchool(1L, "test@inha.edu"); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.saveAndFlush(user2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + } } diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java index 2cf4de360..3fb436be1 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SchoolEmailServiceTest.java @@ -1,14 +1,17 @@ package com.example.solidconnection.siteuser.service; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_VERIFIED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_ALREADY_USED; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_CONFIRM_REQUEST_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_DOMAIN_NOT_SUPPORTED; +import static com.example.solidconnection.common.exception.ErrorCode.SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.mail.MailService; @@ -18,11 +21,13 @@ import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.university.domain.HomeUniversity; import com.example.solidconnection.university.fixture.HomeUniversityFixture; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.test.context.bean.override.mockito.MockitoBean; @TestContainerSpringBootTest @@ -44,6 +49,9 @@ class SchoolEmailServiceTest { @Autowired private SiteUserRepository siteUserRepository; + @Autowired + private RedisTemplate redisTemplate; + @Nested @DisplayName("학교 이메일 인증 요청") class 학교_이메일_인증_요청 { @@ -55,7 +63,7 @@ class 학교_이메일_인증_요청 { SiteUser siteUser = siteUserFixture.사용자(); // when & then - schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), " Test@INHA.EDU "); then(mailService).should().sendVerificationEmail(eq("test@inha.edu"), any()); } @@ -72,6 +80,25 @@ class 학교_이메일_인증_요청 { .hasMessage(SCHOOL_EMAIL_ALREADY_VERIFIED.getMessage()); } + @Test + void 이미_인증에_사용된_학교_이메일이면_예외가_발생한다() { + // given + homeUniversityFixture.인하대학교(); + SiteUser verifiedUser = siteUserFixture.사용자(1, "인증사용자"); + SiteUser anotherUser = siteUserFixture.사용자(2, "다른사용자"); + schoolEmailService.requestSchoolEmailVerification(verifiedUser.getId(), "test@inha.edu"); + + ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); + then(mailService).should().sendVerificationEmail(eq("test@inha.edu"), codeCaptor.capture()); + schoolEmailService.confirmSchoolEmail(verifiedUser.getId(), codeCaptor.getValue()); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.requestSchoolEmailVerification(anotherUser.getId(), "test@inha.edu")) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_ALREADY_USED.getMessage()); + } + @Test void 지원하지_않는_이메일_도메인은_예외가_발생한다() { // given @@ -94,7 +121,7 @@ class 학교_이메일_인증_확인 { // given HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); SiteUser siteUser = siteUserFixture.사용자(); - schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), "test@inha.edu"); + schoolEmailService.requestSchoolEmailVerification(siteUser.getId(), " Test@INHA.EDU "); ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); then(mailService).should().sendVerificationEmail(any(), codeCaptor.capture()); @@ -106,6 +133,7 @@ class 학교_이메일_인증_확인 { // Then SiteUser updated = siteUserRepository.findById(siteUser.getId()).orElseThrow(); assertThat(updated.getHomeUniversityId()).isEqualTo(homeUniversity.getId()); + assertThat(updated.getVerifiedSchoolEmail()).isEqualTo("test@inha.edu"); } @Test @@ -133,5 +161,45 @@ class 학교_이메일_인증_확인 { .isInstanceOf(CustomException.class) .hasMessage(SCHOOL_EMAIL_CONFIRM_CODE_DIFFERENT.getMessage()); } + + @Test + void 인증_정보가_손상되면_예외가_발생하고_인증_정보가_삭제된다() { + // given + HomeUniversity homeUniversity = homeUniversityFixture.인하대학교(); + SiteUser siteUser = siteUserFixture.사용자(); + String key = "school-email:" + siteUser.getId(); + redisTemplate.opsForValue().set( + key, + "{\"homeUniversityId\":" + homeUniversity.getId() + ",\"code\":\"123456\"}" + ); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.confirmSchoolEmail(siteUser.getId(), "123456")) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_VERIFICATION_INFO_CORRUPTED.getMessage()); + assertThat(redisTemplate.opsForValue().get(key)).isNull(); + } + + @Test + void 같은_학교_이메일로_인증_코드를_받았더라도_먼저_인증한_사용자가_있으면_나중_인증은_예외가_발생한다() { + // given + homeUniversityFixture.인하대학교(); + SiteUser firstUser = siteUserFixture.사용자(1, "첫번째사용자"); + SiteUser secondUser = siteUserFixture.사용자(2, "두번째사용자"); + schoolEmailService.requestSchoolEmailVerification(firstUser.getId(), "test@inha.edu"); + schoolEmailService.requestSchoolEmailVerification(secondUser.getId(), "test@inha.edu"); + + ArgumentCaptor codeCaptor = ArgumentCaptor.forClass(String.class); + then(mailService).should(times(2)).sendVerificationEmail(eq("test@inha.edu"), codeCaptor.capture()); + List codes = codeCaptor.getAllValues(); + schoolEmailService.confirmSchoolEmail(firstUser.getId(), codes.get(0)); + + // when & then + assertThatThrownBy(() -> + schoolEmailService.confirmSchoolEmail(secondUser.getId(), codes.get(1))) + .isInstanceOf(CustomException.class) + .hasMessage(SCHOOL_EMAIL_ALREADY_USED.getMessage()); + } } }