From 54b92b598bffd6e6b40a01b6e6061276a73b11b7 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 21 Oct 2023 12:01:16 +0900 Subject: [PATCH 01/30] =?UTF-8?q?test:=20auth=20-=20domain,=20infrastructu?= =?UTF-8?q?re=20layer=20=ED=85=8C=EC=8A=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/domain/TokenRepository.java | 6 +- .../infrastructure/jwt/JwtTokenProvider.java | 8 +- .../auth/domain/AuthCodeRepositoryTest.java | 50 ++++++++ .../auth/domain/AuthTokenManagerTest.java | 61 ++++++++++ .../auth/domain/TokenRepositoryTest.java | 93 ++++++++++++++ .../auth/domain/entity/AuthCodeTest.java | 24 ++++ .../auth/domain/entity/TokenTest.java | 42 +++++++ .../jwt/JwtTokenProviderTest.java | 115 ++++++++++++++++++ .../utils/AuthRandomGeneratorTest.java | 22 ++++ .../auth/stub/JwtTokenProviderStub.java | 32 +++++ .../common/support/RepositoryTestSupport.java | 11 ++ 11 files changed, 457 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/AuthCodeTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/TokenTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProviderTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java index 6fe8ea4..3f7ada5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java @@ -2,11 +2,11 @@ import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; -public interface TokenRepository extends JpaRepository { +public interface TokenRepository { + + Token save(Token token); Optional findByUserId(Long userId); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProvider.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProvider.java index c801243..d986682 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProvider.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProvider.java @@ -38,12 +38,12 @@ public JwtTokenProvider( this.refreshTokenValidity = refreshTokenValidity; } - public String createAccessToken(final Long memberId) { - return createToken(memberId, accessTokenValidity); + public String createAccessToken(final Long userId) { + return createToken(userId, accessTokenValidity); } - public String createRefreshToken(final Long memberId) { - return createToken(memberId, refreshTokenValidity); + public String createRefreshToken(final Long userId) { + return createToken(userId, refreshTokenValidity); } public Long getPayload(final String token) { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java new file mode 100644 index 0000000..27df834 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java @@ -0,0 +1,50 @@ +package com.inq.wishhair.wesharewishhair.auth.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.inq.wishhair.wesharewishhair.auth.domain.entity.AuthCode; +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; + +@DisplayName("[AuthCodeRepository Test] - Domain Layer") +class AuthCodeRepositoryTest extends RepositoryTestSupport { + + @Autowired + private AuthCodeRepository authCodeRepository; + + @Test + @DisplayName("[해당 이메일에 해당되는 인증코드를 삭제한다]") + void deleteByEmail() { + //given + final String email = "email"; + AuthCode authCode = new AuthCode(email, "code"); + authCodeRepository.save(authCode); + + //when + authCodeRepository.deleteByEmail(email); + + //then + Optional actual = authCodeRepository.findByEmail(email); + assertThat(actual).isNotPresent(); + } + + @Test + @DisplayName("[해당 이메일을 가진 인증코드를 조회한다]") + void findByEmail() { + //given + final String email = "email"; + AuthCode authCode = new AuthCode(email, "code"); + authCodeRepository.save(authCode); + + //when + Optional actual = authCodeRepository.findByEmail(email); + + //then + assertThat(actual).contains(authCode); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java new file mode 100644 index 0000000..63a9450 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java @@ -0,0 +1,61 @@ +package com.inq.wishhair.wesharewishhair.auth.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.AuthAuthTokenManagerAdaptor; +import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.JwtTokenProvider; +import com.inq.wishhair.wesharewishhair.auth.stub.JwtTokenProviderStub; + +@DisplayName("[AuthTokenManager Test] - Infrastructure Layer") +class AuthTokenManagerTest { + + private final JwtTokenProvider jwtTokenProvider = new JwtTokenProviderStub(); + private final AuthTokenManager authTokenManager = new AuthAuthTokenManagerAdaptor(jwtTokenProvider); + + @Test + @DisplayName("[인증 토큰을 발행한다]") + void generate() { + //when + AuthToken actual = authTokenManager.generate(1L); + + //then + AuthToken expected = new AuthToken( + jwtTokenProvider.createAccessToken(1L), + jwtTokenProvider.createRefreshToken(1L) + ); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("[토큰에서 id 를 추출한다]") + void getId() { + //given + String token = jwtTokenProvider.createRefreshToken(1L); + + //when + Long actual = authTokenManager.getId(token); + + //then + Long expected = jwtTokenProvider.getPayload(token); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("[토큰을 검증한다]") + void validateToken() { + //given + String token = jwtTokenProvider.createRefreshToken(1L); + + //when + Executable when = () -> authTokenManager.validateToken(token); + + //then + assertDoesNotThrow(when); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java new file mode 100644 index 0000000..aecdd41 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java @@ -0,0 +1,93 @@ +package com.inq.wishhair.wesharewishhair.auth.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@DisplayName("[TokenRepository Test] - Domain Layer") +class TokenRepositoryTest extends RepositoryTestSupport { + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private TokenRepository tokenRepository; + + @Test + @DisplayName("[해당 유저아이디를 가진 토큰을 조회한다]") + void findByUserId() { + //given + final long userId = 1L; + Token token = Token.issue(userId, "token"); + tokenRepository.save(token); + + //when + Optional actual = tokenRepository.findByUserId(userId); + + //then + assertThat(actual).contains(token); + } + + @Test + @DisplayName("[해당 유저 아이디와 리프래쉬 토큰을 가진 토큰을 조회한다]") + void findByUserIdAndRefreshToken() { + //given + final String refreshToken = "token"; + final long userId = 1L; + Token token = Token.issue(userId, refreshToken); + tokenRepository.save(token); + + //when + Optional actual = tokenRepository.findByUserIdAndRefreshToken(userId, refreshToken); + + //then + assertThat(actual).contains(token); + } + + @Test + @DisplayName("[해당 유저 아이디를 가진 토큰을 삭제한다]") + void deleteByUserId() { + //given + final long userId = 1L; + Token token = Token.issue(userId, "token"); + tokenRepository.save(token); + + //when + tokenRepository.deleteByUserId(userId); + + //then + Optional actual = tokenRepository.findByUserId(userId); + assertThat(actual).isNotPresent(); + } + + @Test + @DisplayName("[해당 유저 아이디를 가진 토큰의 리프래쉬 토큰을 업데이트한다]") + void updateRefreshTokenByUserId() { + //given + final long userId = 1L; + Token token = Token.issue(userId, "token"); + tokenRepository.save(token); + + final String newRefreshToken = "refreshToken"; + + //when + tokenRepository.updateRefreshTokenByUserId(userId, newRefreshToken); + entityManager.flush(); + entityManager.clear(); + + //then + Optional actual = tokenRepository.findByUserId(userId); + assertThat(actual).isPresent(); + assertThat(actual.get().getRefreshToken()).isEqualTo(newRefreshToken); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/AuthCodeTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/AuthCodeTest.java new file mode 100644 index 0000000..16107b1 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/AuthCodeTest.java @@ -0,0 +1,24 @@ +package com.inq.wishhair.wesharewishhair.auth.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("[AuthCode Test] - Domain Layer") +class AuthCodeTest { + + @Test + @DisplayName("[인증 코드를 변경한다]") + void updateCode() { + //given + AuthCode authCode = new AuthCode("email", "code"); + final String newCode = "newCode"; + + //when + authCode.updateCode(newCode); + + //then + assertThat(authCode.getCode()).isEqualTo(newCode); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/TokenTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/TokenTest.java new file mode 100644 index 0000000..93193b7 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/entity/TokenTest.java @@ -0,0 +1,42 @@ +package com.inq.wishhair.wesharewishhair.auth.domain.entity; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("[Token Test] - Domain Layer") +class TokenTest { + + @Test + @DisplayName("[토큰을 생성한다]") + void issueTest() { + //given + final Long userId = 1L; + final String token = "token"; + + //when + Token actual = Token.issue(userId, token); + + //then + assertAll( + () -> assertThat(actual.getUserId()).isEqualTo(userId), + () -> assertThat(actual.getRefreshToken()).isEqualTo(token) + ); + } + + @Test + @DisplayName("[토큰을 변경한다]") + void updateRefreshTokenTest() { + //given + Token token = Token.issue(1L, "token"); + final String newToken = "newToken"; + + //when + token.updateRefreshToken(newToken); + + //then + assertThat(token.getRefreshToken()).isEqualTo(newToken); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProviderTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProviderTest.java new file mode 100644 index 0000000..95a15fa --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProviderTest.java @@ -0,0 +1,115 @@ +package com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt; + +import static com.inq.wishhair.wesharewishhair.global.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; + +@DisplayName("[JwtTokenProvider Test] - Infrastructure Layer") +class JwtTokenProviderTest { + + private final String secretKey; + private final JwtTokenProvider jwtTokenProvider; + + public JwtTokenProviderTest() { + secretKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + this.jwtTokenProvider = new JwtTokenProvider(secretKey, 10000, 10000); + } + + @Test + void createAccessToken() { + //given + final Long userId =1L; + + //when + String actual = jwtTokenProvider.createAccessToken(userId); + + //then + Long expected = jwtTokenProvider.getPayload(actual); + assertThat(userId).isEqualTo(expected); + } + + @Test + void createRefreshToken() { + //given + final Long userId =1L; + + //when + String actual = jwtTokenProvider.createRefreshToken(userId); + + //then + Long expected = jwtTokenProvider.getPayload(actual); + assertThat(userId).isEqualTo(expected); + } + + @Test + void getPayload() { + //given + final Long userId =1L; + String token = jwtTokenProvider.createAccessToken(userId); + + //when + Long actual = jwtTokenProvider.getPayload(token); + + //then + assertThat(actual).isEqualTo(userId); + } + + @Nested + @DisplayName("[토큰을 검증한다]") + class validateToken { + + @Test + @DisplayName("[검증을 통과한다]") + void pass() { + //given + String token = jwtTokenProvider.createAccessToken(1L); + + //when + Executable when = () -> jwtTokenProvider.validateToken(token); + + //then + assertDoesNotThrow(when); + } + + @Test + @DisplayName("[유효기간이 만료되어 검증에 실패한다]") + void failByExpire() { + //given + JwtTokenProvider provider = new JwtTokenProvider(secretKey, 0, 0); + String token = provider.createRefreshToken(1L); + + //when + ThrowingCallable when = () -> provider.validateToken(token); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(AUTH_EXPIRED_TOKEN.getMessage()); + } + + @Test + @DisplayName("[잘못된 시크릿키로 인코딩된 토큰여서 실패한다]") + void failBySecretKey() { + //given + final String newSecretKey = secretKey + "b"; + JwtTokenProvider provider = new JwtTokenProvider(newSecretKey, 10000, 10000); + String token = provider.createRefreshToken(1L); + + //when + ThrowingCallable when = () -> jwtTokenProvider.validateToken(token); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(AUTH_INVALID_TOKEN.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java new file mode 100644 index 0000000..4eaf521 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java @@ -0,0 +1,22 @@ +package com.inq.wishhair.wesharewishhair.auth.infrastructure.utils; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("[AuthRandomGenerator Test] - Infrastructure Layer") +class AuthRandomGeneratorTest { + + private final AuthRandomGenerator authRandomGenerator = new AuthRandomGenerator(); + + @Test + @DisplayName("[1000~9999 사이의 랜덤한 숫자 문자열을 생성한다]") + void generateString() { + //when + String actual = authRandomGenerator.generateString(); + + //then + assertThat(Integer.parseInt(actual)).isBetween(1000, 9999); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java new file mode 100644 index 0000000..a28fe69 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java @@ -0,0 +1,32 @@ +package com.inq.wishhair.wesharewishhair.auth.stub; + +import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.JwtTokenProvider; + +public class JwtTokenProviderStub extends JwtTokenProvider { + + private static final String ACCESS_TOKEN = "access_token"; + private static final String REFRESH_TOKEN = "refresh_token"; + + public JwtTokenProviderStub() { + super("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 0, 0); + } + + @Override + public String createAccessToken(final Long userId) { + return ACCESS_TOKEN; + } + + @Override + public String createRefreshToken(final Long userId) { + return REFRESH_TOKEN; + } + + @Override + public Long getPayload(final String token) { + return 1L; + } + + @Override + public void validateToken(final String token) { + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java new file mode 100644 index 0000000..61f60cc --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java @@ -0,0 +1,11 @@ +package com.inq.wishhair.wesharewishhair.common.support; + +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.inq.wishhair.wesharewishhair.global.config.QueryDslConfig; + +@Import(QueryDslConfig.class) +@DataJpaTest +public abstract class RepositoryTestSupport { +} From 088e0585bd0955a416078b5f6af09cf4cc0e49bb Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 21 Oct 2023 17:24:44 +0900 Subject: [PATCH 02/30] =?UTF-8?q?test:=20auth=20-=20application=20layer=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthService.java | 27 +++- .../auth/application/MailAuthService.java | 6 +- .../auth/application/TokenManager.java | 42 ----- .../auth/application/TokenReissueService.java | 7 +- .../auth/domain/AuthCodeRepository.java | 2 + .../auth/domain/TokenRepository.java | 2 +- .../infrastructure/AuthCodeJpaRepository.java | 2 + .../infrastructure/TokenJpaRepository.java | 6 +- .../auth/application/AuthServiceTest.java | 151 ++++++++++++++++++ .../auth/application/MailAuthServiceTest.java | 115 +++++++++++++ .../application/TokenReissueServiceTest.java | 79 +++++++++ .../auth/domain/AuthCodeRepositoryTest.java | 50 ------ .../auth/domain/TokenRepositoryTest.java | 7 +- .../auth/fixture/TokenFixture.java | 17 ++ .../common/stub/PasswordEncoderStub.java | 18 +++ .../user/fixture/UserFixture.java | 41 +++++ 16 files changed, 458 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenManager.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/fixture/TokenFixture.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/stub/PasswordEncoderStub.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/AuthService.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/AuthService.java index fce0a64..ae90b73 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/AuthService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/AuthService.java @@ -7,6 +7,8 @@ import com.inq.wishhair.wesharewishhair.auth.application.dto.response.LoginResponse; import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; +import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; +import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; import com.inq.wishhair.wesharewishhair.user.domain.entity.Email; @@ -20,26 +22,39 @@ @Transactional(readOnly = true) public class AuthService { - private final TokenManager tokenManager; private final UserRepository userRepository; + private final TokenRepository tokenRepository; private final PasswordEncoder passwordEncoder; private final AuthTokenManager authTokenManager; @Transactional - public LoginResponse login(String email, String pw) { + public LoginResponse login( + final String email, + final String pw + ) { User user = userRepository.findByEmail(new Email(email)) .filter(findUser -> passwordEncoder.matches(pw, findUser.getPasswordValue())) .orElseThrow(() -> new WishHairException(ErrorCode.LOGIN_FAIL)); AuthToken authToken = authTokenManager.generate(user.getId()); - - tokenManager.synchronizeRefreshToken(user.getId(), authToken.refreshToken()); + synchronizeRefreshToken(user, authToken); return new LoginResponse(authToken.accessToken(), authToken.refreshToken()); } @Transactional - public void logout(Long userId) { - tokenManager.deleteToken(userId); + public void logout(final Long userId) { + tokenRepository.deleteByUserId(userId); + } + + private void synchronizeRefreshToken( + final User user, + final AuthToken authToken + ) { + tokenRepository.findByUserId(user.getId()) + .ifPresentOrElse( + token -> token.updateRefreshToken(authToken.refreshToken()), + () -> tokenRepository.save(Token.issue(user.getId(), authToken.refreshToken())) + ); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthService.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthService.java index 93a613a..1af88ad 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthService.java @@ -38,12 +38,10 @@ public void requestMailAuthorization(final String email) { public void checkAuthCode( final String email, - final String authCode + final String code ) { authCodeRepository.findByEmail(email) - .filter(actualAuthCode -> actualAuthCode.getCode().equals(authCode)) + .filter(authCode -> authCode.getCode().equals(code)) .orElseThrow(() -> new WishHairException(AUTH_INVALID_AUTH_CODE)); - - authCodeRepository.deleteByEmail(email); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenManager.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenManager.java deleted file mode 100644 index 093c844..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenManager.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.inq.wishhair.wesharewishhair.auth.application; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; -import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class TokenManager { - - private final TokenRepository tokenRepository; - - @Transactional - public void synchronizeRefreshToken(Long userId, String refreshToken) { - tokenRepository.findByUserId(userId) - .ifPresentOrElse( - token -> token.updateRefreshToken(refreshToken), - () -> tokenRepository.save(Token.issue(userId, refreshToken)) - ); - } - - public boolean existByUserIdAndRefreshToken(Long userId, String refreshToken) { - return tokenRepository - .findByUserIdAndRefreshToken(userId, refreshToken) - .isPresent(); - } - - @Transactional - public void deleteToken(Long userId) { - tokenRepository.deleteByUserId(userId); - } - - @Transactional - public void updateRefreshToken(Long userId, String refreshToken) { - tokenRepository.updateRefreshTokenByUserId(userId, refreshToken); - } -} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueService.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueService.java index b5d2c33..d50ad65 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueService.java @@ -6,6 +6,7 @@ import com.inq.wishhair.wesharewishhair.auth.application.dto.response.TokenResponse; import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; +import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; @@ -15,20 +16,20 @@ @RequiredArgsConstructor public class TokenReissueService { - private final TokenManager tokenManager; + private final TokenRepository tokenRepository; private final AuthTokenManager authTokenManager; @Transactional public TokenResponse reissueToken(Long userId, String refreshToken) { //사용하지 않은 RTR 토큰인지, 존재하는지 확인 - if (!tokenManager.existByUserIdAndRefreshToken(userId, refreshToken)) { + if (!tokenRepository.existsByUserIdAndRefreshToken(userId, refreshToken)) { throw new WishHairException(ErrorCode.AUTH_INVALID_TOKEN); } AuthToken authToken = authTokenManager.generate(userId); - tokenManager.updateRefreshToken(userId, authToken.refreshToken()); + tokenRepository.updateRefreshTokenByUserId(userId, authToken.refreshToken()); return new TokenResponse(authToken.accessToken(), authToken.refreshToken()); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java index 1e15f84..d25759b 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java @@ -8,6 +8,8 @@ public interface AuthCodeRepository { AuthCode save(AuthCode authCode); + void deleteById(Long id); + void deleteByEmail(String email); Optional findByEmail(String email); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java index 3f7ada5..5e6355e 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepository.java @@ -10,7 +10,7 @@ public interface TokenRepository { Optional findByUserId(Long userId); - Optional findByUserIdAndRefreshToken(Long userId, String refreshToken); + boolean existsByUserIdAndRefreshToken(Long userId, String refreshToken); void deleteByUserId(Long userId); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java index 82437cd..941ad9f 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java @@ -9,6 +9,8 @@ public interface AuthCodeJpaRepository extends AuthCodeRepository, JpaRepository { + void deleteById(Long id); + @Override void deleteByEmail(String email); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/TokenJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/TokenJpaRepository.java index ea40ffa..6727684 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/TokenJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/TokenJpaRepository.java @@ -14,11 +14,7 @@ public interface TokenJpaRepository extends TokenRepository, JpaRepository findByUserId(Long userId); - @Query("select t from Token t where t.userId = :userId " + - "and t.refreshToken = :refreshToken") - Optional findByUserIdAndRefreshToken( - @Param("userId") Long userId, - @Param("refreshToken") String refreshToken); + boolean existsByUserIdAndRefreshToken(Long userId, String refreshToken); @Modifying @Query("delete from Token t where t.userId = :userId") diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java new file mode 100644 index 0000000..fc2cfc2 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java @@ -0,0 +1,151 @@ +package com.inq.wishhair.wesharewishhair.auth.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import com.inq.wishhair.wesharewishhair.auth.application.dto.response.LoginResponse; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; +import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; +import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; +import com.inq.wishhair.wesharewishhair.auth.fixture.TokenFixture; +import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.AuthAuthTokenManagerAdaptor; +import com.inq.wishhair.wesharewishhair.auth.stub.JwtTokenProviderStub; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.Email; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[AuthService Test] - Application Layer") +class AuthServiceTest { + + private static final String EMAIL = "hello@naver.com"; + private static final String PW = "hello1234@"; + private static final User USER = UserFixture.getFixedManUser(); + + private final AuthService authService; + private final UserRepository userRepository; + private final TokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + private final AuthTokenManager authTokenManager; + + public AuthServiceTest() { + this.userRepository = Mockito.mock(UserRepository.class); + this.tokenRepository = Mockito.mock(TokenRepository.class); + this.passwordEncoder = Mockito.mock(PasswordEncoder.class); + this.authTokenManager = new AuthAuthTokenManagerAdaptor(new JwtTokenProviderStub()); + this.authService = new AuthService( + userRepository, + tokenRepository, + passwordEncoder, + authTokenManager + ); + } + + @Nested + @DisplayName("로그인을 한다") + class login { + + @Nested + @DisplayName("[로그인에 성공한다]") + class loginSuccess { + + public loginSuccess() { + given(userRepository.findByEmail(new Email(EMAIL))) + .willReturn(Optional.of(USER)); + + given(passwordEncoder.matches(PW, USER.getPasswordValue())) + .willReturn(true); + } + + @Test + @DisplayName("[토큰을 가지고있는 유저로 기존 토큰의 RefreshToken 을 업데이트한다]") + void updateRefreshToken() { + //given + Token token = TokenFixture.getFixedToken(); + given(tokenRepository.findByUserId(null)) + .willReturn(Optional.of(token)); + + //when + LoginResponse actual = authService.login(EMAIL, PW); + + //then + assertLoginResponse(actual); + assertThat(token.getRefreshToken()).isEqualTo(actual.refreshToken()); + } + + @Test + @DisplayName("[토큰이 없는 유저로 새로운 토큰을 생성한다]") + void createNewToken() { + //given + given(tokenRepository.findByUserId(null)) + .willReturn(Optional.empty()); + + //when + LoginResponse actual = authService.login(EMAIL, PW); + + //then + assertLoginResponse(actual); + verify(tokenRepository, times(1)).save(any(Token.class)); + } + + private void assertLoginResponse(LoginResponse actual) { + AuthToken expected = authTokenManager.generate(1L); + assertAll( + () -> assertThat(actual.accessToken()).isEqualTo(expected.accessToken()), + () -> assertThat(actual.refreshToken()).isEqualTo(expected.refreshToken()) + ); + } + } + + @Test + @DisplayName("[잘못된 이메일로 로그인에 실패한다]") + void failByInvalidEmail() { + //given + given(userRepository.findByEmail(new Email(EMAIL))) + .willReturn(Optional.empty()); + + //when + ThrowingCallable when = () -> authService.login(EMAIL, PW); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.LOGIN_FAIL.getMessage()); + } + + @Test + @DisplayName("[잘못된 비밀번호로 로그인에 실패한다]") + void failByInvalidPw() { + //given + given(userRepository.findByEmail(new Email(EMAIL))) + .willReturn(Optional.of(USER)); + + given(passwordEncoder.matches(PW, USER.getPasswordValue())) + .willReturn(false); + + //when + ThrowingCallable when = () -> authService.login(EMAIL, PW); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.LOGIN_FAIL.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthServiceTest.java new file mode 100644 index 0000000..b1b1d45 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthServiceTest.java @@ -0,0 +1,115 @@ +package com.inq.wishhair.wesharewishhair.auth.application; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import com.inq.wishhair.wesharewishhair.auth.application.utils.RandomGenerator; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthCodeRepository; +import com.inq.wishhair.wesharewishhair.auth.domain.entity.AuthCode; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[AuthService Test] - Application Layer") +class MailAuthServiceTest { + + private static final String CODE = "1234"; + private static final String EMAIL = "hello@naver.com"; + + private final MailAuthService mailAuthService; + private final AuthCodeRepository authCodeRepository; + + public MailAuthServiceTest() { + this.authCodeRepository = Mockito.mock(AuthCodeRepository.class); + ApplicationEventPublisher eventPublisher = Mockito.mock(ApplicationEventPublisher.class); + RandomGenerator randomGenerator = Mockito.mock(RandomGenerator.class); + + this.mailAuthService = new MailAuthService( + eventPublisher, authCodeRepository, randomGenerator + ); + + given(randomGenerator.generateString()).willReturn(CODE); + } + + @Nested + @DisplayName("[인증 메일을 요청한다]") + class requestMailAuthorization { + + @Test + @DisplayName("[인증 메일을 요청하고 인증코드를 새로 생성한다]") + void createNewAuthCode() { + //given + given(authCodeRepository.findByEmail(EMAIL)) + .willReturn(Optional.empty()); + + //when + mailAuthService.requestMailAuthorization(EMAIL); + + //then + verify(authCodeRepository, times(1)).save(any(AuthCode.class)); + } + + @Test + @DisplayName("[인증 메일을 요청하고 기존의 인증코드가 존재해서 기존 인증코드를 업데이트한다]") + void updateAuthCode() { + //given + AuthCode authCode = new AuthCode(EMAIL, "code"); + given(authCodeRepository.findByEmail(EMAIL)) + .willReturn(Optional.of(authCode)); + + //when + mailAuthService.requestMailAuthorization(EMAIL); + + //then + assertThat(authCode.getCode()).isEqualTo(CODE); + } + } + + @Nested + @DisplayName("[인증코드가 올바른지 확인한다]") + class checkAuthCode { + + private static final String CODE = "code"; + + public checkAuthCode() { + //given + given(authCodeRepository.findByEmail(EMAIL)) + .willReturn(Optional.of(new AuthCode(EMAIL, CODE))); + } + + @Test + @DisplayName("[검사에 성공한다]") + void success() { + //when + Executable when = () -> mailAuthService.checkAuthCode(EMAIL, CODE); + + //then + assertDoesNotThrow(when); + } + + @Test + @DisplayName("잘못된 code 로 검사에 실패한다") + void failByInvalidCode() { + //when + ThrowingCallable when = () -> mailAuthService.checkAuthCode(EMAIL, "hello"); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.AUTH_INVALID_AUTH_CODE.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java new file mode 100644 index 0000000..1ac7ba8 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java @@ -0,0 +1,79 @@ +package com.inq.wishhair.wesharewishhair.auth.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.inq.wishhair.wesharewishhair.auth.application.dto.response.TokenResponse; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; +import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; +import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.AuthAuthTokenManagerAdaptor; +import com.inq.wishhair.wesharewishhair.auth.stub.JwtTokenProviderStub; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[TokenReissueService 테스트] - Application Layer") +class TokenReissueServiceTest { + + private final TokenReissueService tokenReissueService; + private final TokenRepository tokenRepository; + private final AuthTokenManager authTokenManager; + + public TokenReissueServiceTest() { + this.tokenRepository = Mockito.mock(TokenRepository.class); + this.authTokenManager = new AuthAuthTokenManagerAdaptor(new JwtTokenProviderStub()); + this.tokenReissueService = new TokenReissueService(tokenRepository, authTokenManager); + } + + @Nested + @DisplayName("토큰을 재발급한다") + class reissueToken { + + private static final Long USER_ID = 1L; + private static final String REFRESH_TOKEN = "TOKEN"; + + @Test + @DisplayName("[재발급에 성공한다]") + void success() { + //given + given(tokenRepository.existsByUserIdAndRefreshToken(USER_ID, REFRESH_TOKEN)) + .willReturn(true); + + //when + TokenResponse actual = tokenReissueService.reissueToken(USER_ID, REFRESH_TOKEN); + + //then + AuthToken expected = authTokenManager.generate(1L); + assertAll( + () -> assertThat(actual.accessToken()).isEqualTo(expected.accessToken()), + () -> assertThat(actual.refreshToken()).isEqualTo(expected.refreshToken()) + ); + } + + @Test + @DisplayName("[이미 재발급에 사용된 토큰으로 실패한다]") + void failByInvalidRefreshToken() { + //given + given(tokenRepository.existsByUserIdAndRefreshToken(USER_ID, REFRESH_TOKEN)) + .willReturn(false); + + //when + ThrowingCallable when = () -> tokenReissueService.reissueToken(USER_ID, REFRESH_TOKEN); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.AUTH_INVALID_TOKEN.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java deleted file mode 100644 index 27df834..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepositoryTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.inq.wishhair.wesharewishhair.auth.domain; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Optional; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import com.inq.wishhair.wesharewishhair.auth.domain.entity.AuthCode; -import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; - -@DisplayName("[AuthCodeRepository Test] - Domain Layer") -class AuthCodeRepositoryTest extends RepositoryTestSupport { - - @Autowired - private AuthCodeRepository authCodeRepository; - - @Test - @DisplayName("[해당 이메일에 해당되는 인증코드를 삭제한다]") - void deleteByEmail() { - //given - final String email = "email"; - AuthCode authCode = new AuthCode(email, "code"); - authCodeRepository.save(authCode); - - //when - authCodeRepository.deleteByEmail(email); - - //then - Optional actual = authCodeRepository.findByEmail(email); - assertThat(actual).isNotPresent(); - } - - @Test - @DisplayName("[해당 이메일을 가진 인증코드를 조회한다]") - void findByEmail() { - //given - final String email = "email"; - AuthCode authCode = new AuthCode(email, "code"); - authCodeRepository.save(authCode); - - //when - Optional actual = authCodeRepository.findByEmail(email); - - //then - assertThat(actual).contains(authCode); - } -} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java index aecdd41..fd45266 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/TokenRepositoryTest.java @@ -39,19 +39,20 @@ void findByUserId() { } @Test - @DisplayName("[해당 유저 아이디와 리프래쉬 토큰을 가진 토큰을 조회한다]") + @DisplayName("[해당 유저 아이디와 리프래쉬 토큰을 가진 토큰의 존재여부를 조회한다]") void findByUserIdAndRefreshToken() { //given final String refreshToken = "token"; final long userId = 1L; + Token token = Token.issue(userId, refreshToken); tokenRepository.save(token); //when - Optional actual = tokenRepository.findByUserIdAndRefreshToken(userId, refreshToken); + boolean actual = tokenRepository.existsByUserIdAndRefreshToken(userId, refreshToken); //then - assertThat(actual).contains(token); + assertThat(actual).isTrue(); } @Test diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/fixture/TokenFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/fixture/TokenFixture.java new file mode 100644 index 0000000..bc20be3 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/fixture/TokenFixture.java @@ -0,0 +1,17 @@ +package com.inq.wishhair.wesharewishhair.auth.fixture; + +import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TokenFixture { + + private static final String TOKEN = "token"; + private static final Long USER_ID = 1L; + + public static Token getFixedToken() { + return Token.issue(USER_ID, TOKEN); + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/stub/PasswordEncoderStub.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/stub/PasswordEncoderStub.java new file mode 100644 index 0000000..ece942e --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/stub/PasswordEncoderStub.java @@ -0,0 +1,18 @@ +package com.inq.wishhair.wesharewishhair.common.stub; + +import org.springframework.security.crypto.password.PasswordEncoder; + +public class PasswordEncoderStub implements PasswordEncoder { + + private static final String ENCODED_PW = "encoded_password"; + + @Override + public String encode(CharSequence rawPassword) { + return ENCODED_PW; + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return true; + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java new file mode 100644 index 0000000..27d2114 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java @@ -0,0 +1,41 @@ +package com.inq.wishhair.wesharewishhair.user.fixture; + +import org.springframework.security.crypto.password.PasswordEncoder; + +import com.inq.wishhair.wesharewishhair.common.stub.PasswordEncoderStub; +import com.inq.wishhair.wesharewishhair.user.domain.entity.Password; +import com.inq.wishhair.wesharewishhair.user.domain.entity.Sex; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class UserFixture { + + private static final PasswordEncoder PASSWORD_ENCODER = new PasswordEncoderStub(); + private static final String EMAIL = "hello@naver.com"; + private static final String PW = "hello1234@"; + private static final String NAME = "hello"; + private static final String NICKNAME = "hello"; + + public static User getFixedManUser() { + return User.createUser( + EMAIL, + Password.encrypt(PW, PASSWORD_ENCODER), + NAME, + NICKNAME, + Sex.MAN + ); + } + + public static User getFixedWomanUser() { + return User.createUser( + EMAIL, + Password.encrypt(PW, PASSWORD_ENCODER), + NAME, + NICKNAME, + Sex.WOMAN + ); + } +} From d64059a3e5e2339eee1c6e25197fbfb8a34bf3b8 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Tue, 24 Oct 2023 11:42:43 +0900 Subject: [PATCH 03/30] =?UTF-8?q?test:=20global=20-=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/domain/AuthCodeRepository.java | 4 - .../infrastructure/AuthCodeJpaRepository.java | 3 - .../interceptor/PathMatcherContainer.java | 17 +- .../interceptor/PathMatcherInterceptor.java | 13 +- .../auth/application/AuthServiceTest.java | 8 +- .../application/TokenReissueServiceTest.java | 5 +- .../utils/AuthRandomGeneratorTest.java | 10 +- .../auth/domain/AuthTokenManagerTest.java | 151 +++++++++++++++--- .../auth/stub/AuthTokenMangerStub.java | 21 +++ .../auth/stub/JwtTokenProviderStub.java | 32 ---- .../AuthenticationInterceptorTest.java | 90 +++++++++++ .../interceptor/PathMatcherContainerTest.java | 81 ++++++++++ 12 files changed, 344 insertions(+), 91 deletions(-) rename src/test/java/com/inq/wishhair/wesharewishhair/auth/{infrastructure => application}/utils/AuthRandomGeneratorTest.java (51%) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/AuthTokenMangerStub.java delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/AuthenticationInterceptorTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainerTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java index d25759b..6a09b29 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthCodeRepository.java @@ -8,9 +8,5 @@ public interface AuthCodeRepository { AuthCode save(AuthCode authCode); - void deleteById(Long id); - - void deleteByEmail(String email); - Optional findByEmail(String email); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java index 941ad9f..6b05b9c 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/AuthCodeJpaRepository.java @@ -11,9 +11,6 @@ public interface AuthCodeJpaRepository extends AuthCodeRepository, JpaRepository void deleteById(Long id); - @Override - void deleteByEmail(String email); - @Override Optional findByEmail(String email); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainer.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainer.java index 52711fb..be5d251 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainer.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainer.java @@ -3,6 +3,7 @@ import java.util.HashSet; import java.util.Set; +import org.springframework.http.HttpMethod; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; @@ -18,7 +19,7 @@ public PathMatcherContainer() { excludePath = new HashSet<>(); } - public boolean isInterceptorRequired(final String path, final String method) { + public boolean isInterceptorRequired(final String path, final HttpMethod method) { boolean isInclude = includePath.stream() .anyMatch(include -> matches(include, path, method)); @@ -28,27 +29,27 @@ public boolean isInterceptorRequired(final String path, final String method) { return isNotExclude && isInclude; } - public void includePathPattern(final String pathPattern, final String method) { + public void includePathPattern(final String pathPattern, final HttpMethod method) { includePath.add(new PathInfo(pathPattern, method)); } - public void excludePathPattern(final String pathPattern, final String method) { + public void excludePathPattern(final String pathPattern, final HttpMethod method) { excludePath.add(new PathInfo(pathPattern, method)); } private boolean matches( final PathInfo pathInfo, final String targetPath, - final String targetMethod + final HttpMethod targetMethod ) { - boolean match = pathMatcher.match(pathInfo.pathPattern, targetPath); - boolean equals = pathInfo.method.equals(targetMethod); - return match && equals; + boolean pathMatch = pathMatcher.match(pathInfo.pathPattern, targetPath); + boolean methodMath = pathInfo.method.equals(targetMethod); + return pathMatch && methodMath; } private record PathInfo( String pathPattern, - String method + HttpMethod method ) { } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherInterceptor.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherInterceptor.java index 553f5c8..bbdec28 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherInterceptor.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherInterceptor.java @@ -24,35 +24,38 @@ public boolean preHandle( final HttpServletResponse response, final Object handler ) throws Exception { + boolean interceptorRequired = pathMatcherContainer.isInterceptorRequired( + request.getRequestURI(), HttpMethod.valueOf(request.getMethod()) + ); - if (pathMatcherContainer.isInterceptorRequired(request.getRequestURI(), request.getMethod())) { + if (interceptorRequired) { return target.preHandle(request, response, handler); } return true; } public PathMatcherInterceptor addIncludePathPattern(final String pathPattern, final HttpMethod method) { - pathMatcherContainer.includePathPattern(pathPattern, method.toString()); + pathMatcherContainer.includePathPattern(pathPattern, method); return this; } public PathMatcherInterceptor addIncludePathPattern(final String pathPattern) { Arrays.stream(HttpMethod.values()) .forEach(method -> - pathMatcherContainer.includePathPattern(pathPattern, method.toString())); + pathMatcherContainer.includePathPattern(pathPattern, method)); return this; } public PathMatcherInterceptor addExcludePathPattern(final String pathPattern, final HttpMethod method) { - pathMatcherContainer.excludePathPattern(pathPattern, method.toString()); + pathMatcherContainer.excludePathPattern(pathPattern, method); return this; } public PathMatcherInterceptor addExcludePathPattern(final String pathPattern) { Arrays.stream(HttpMethod.values()) .forEach(method -> - pathMatcherContainer.excludePathPattern(pathPattern, method.toString())); + pathMatcherContainer.excludePathPattern(pathPattern, method)); return this; } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java index fc2cfc2..aec13af 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java @@ -10,9 +10,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; import com.inq.wishhair.wesharewishhair.auth.application.dto.response.LoginResponse; @@ -21,8 +19,7 @@ import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; import com.inq.wishhair.wesharewishhair.auth.fixture.TokenFixture; -import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.AuthAuthTokenManagerAdaptor; -import com.inq.wishhair.wesharewishhair.auth.stub.JwtTokenProviderStub; +import com.inq.wishhair.wesharewishhair.auth.stub.AuthTokenMangerStub; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; @@ -30,7 +27,6 @@ import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; -@ExtendWith(MockitoExtension.class) @DisplayName("[AuthService Test] - Application Layer") class AuthServiceTest { @@ -48,7 +44,7 @@ public AuthServiceTest() { this.userRepository = Mockito.mock(UserRepository.class); this.tokenRepository = Mockito.mock(TokenRepository.class); this.passwordEncoder = Mockito.mock(PasswordEncoder.class); - this.authTokenManager = new AuthAuthTokenManagerAdaptor(new JwtTokenProviderStub()); + this.authTokenManager = new AuthTokenMangerStub(); this.authService = new AuthService( userRepository, tokenRepository, diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java index 1ac7ba8..d329b26 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/TokenReissueServiceTest.java @@ -16,8 +16,7 @@ import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; -import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.AuthAuthTokenManagerAdaptor; -import com.inq.wishhair.wesharewishhair.auth.stub.JwtTokenProviderStub; +import com.inq.wishhair.wesharewishhair.auth.stub.AuthTokenMangerStub; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; @@ -31,7 +30,7 @@ class TokenReissueServiceTest { public TokenReissueServiceTest() { this.tokenRepository = Mockito.mock(TokenRepository.class); - this.authTokenManager = new AuthAuthTokenManagerAdaptor(new JwtTokenProviderStub()); + this.authTokenManager = new AuthTokenMangerStub(); this.tokenReissueService = new TokenReissueService(tokenRepository, authTokenManager); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/utils/AuthRandomGeneratorTest.java similarity index 51% rename from src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java rename to src/test/java/com/inq/wishhair/wesharewishhair/auth/application/utils/AuthRandomGeneratorTest.java index 4eaf521..c834eeb 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/utils/AuthRandomGeneratorTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/utils/AuthRandomGeneratorTest.java @@ -1,20 +1,22 @@ -package com.inq.wishhair.wesharewishhair.auth.infrastructure.utils; +package com.inq.wishhair.wesharewishhair.auth.application.utils; import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -@DisplayName("[AuthRandomGenerator Test] - Infrastructure Layer") +import com.inq.wishhair.wesharewishhair.auth.infrastructure.utils.AuthRandomGenerator; + +@DisplayName("[RandomGenerator Test] - Infrastructure Layer") class AuthRandomGeneratorTest { - private final AuthRandomGenerator authRandomGenerator = new AuthRandomGenerator(); + private final RandomGenerator randomGenerator = new AuthRandomGenerator(); @Test @DisplayName("[1000~9999 사이의 랜덤한 숫자 문자열을 생성한다]") void generateString() { //when - String actual = authRandomGenerator.generateString(); + String actual = randomGenerator.generateString(); //then assertThat(Integer.parseInt(actual)).isBetween(1000, 9999); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java index 63a9450..575be65 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/domain/AuthTokenManagerTest.java @@ -1,21 +1,32 @@ package com.inq.wishhair.wesharewishhair.auth.domain; +import static com.inq.wishhair.wesharewishhair.global.exception.ErrorCode.*; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.AuthAuthTokenManagerAdaptor; import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.JwtTokenProvider; -import com.inq.wishhair.wesharewishhair.auth.stub.JwtTokenProviderStub; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; @DisplayName("[AuthTokenManager Test] - Infrastructure Layer") class AuthTokenManagerTest { - private final JwtTokenProvider jwtTokenProvider = new JwtTokenProviderStub(); - private final AuthTokenManager authTokenManager = new AuthAuthTokenManagerAdaptor(jwtTokenProvider); + private final AuthTokenManager authTokenManager; + private final String secretKey; + private final String differentSecretKey; + + public AuthTokenManagerTest() { + secretKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + differentSecretKey = secretKey.replaceAll("a", "b"); + JwtTokenProvider jwtTokenProvider = new JwtTokenProvider(secretKey, 10000, 10000); + authTokenManager = new AuthAuthTokenManagerAdaptor(jwtTokenProvider); + } @Test @DisplayName("[인증 토큰을 발행한다]") @@ -24,38 +35,126 @@ void generate() { AuthToken actual = authTokenManager.generate(1L); //then - AuthToken expected = new AuthToken( - jwtTokenProvider.createAccessToken(1L), - jwtTokenProvider.createRefreshToken(1L) - ); - - assertThat(actual).isEqualTo(expected); + assertDoesNotThrow(() -> authTokenManager.validateToken(actual.accessToken())); + assertDoesNotThrow(() -> authTokenManager.validateToken(actual.refreshToken())); } - @Test + @Nested @DisplayName("[토큰에서 id 를 추출한다]") - void getId() { - //given - String token = jwtTokenProvider.createRefreshToken(1L); + class getId { - //when - Long actual = authTokenManager.getId(token); + @Test + @DisplayName("[성공적으로 id 를 추출한다]") + void success() { + long userId = 1L; + AuthToken authToken = authTokenManager.generate(userId); + String token = authToken.accessToken(); - //then - Long expected = jwtTokenProvider.getPayload(token); - assertThat(actual).isEqualTo(expected); + //when + Long actual = authTokenManager.getId(token); + + //then + assertThat(actual).isEqualTo(userId); + } + + @Test + @DisplayName("[유효하지 않은 토큰으로 추출에 실패한다]") + void failByInvalidToken() { + //given + AuthTokenManager differentKeyTokenManager = new AuthAuthTokenManagerAdaptor( + new JwtTokenProvider(differentSecretKey, 100, 100)); + + String token1 = authTokenManager.generate(1L).accessToken() + "e"; + String token2 = differentKeyTokenManager.generate(1L).accessToken(); + + //when + ThrowingCallable when1 = () -> authTokenManager.getId(token1); + ThrowingCallable when2 = () -> authTokenManager.getId(token2); + + //then + assertThrownByInvalidToken(when1, when2); + } + + @Test + @DisplayName("[유효기간이 만료된 토큰으로 추출에 실패한다]") + void failByExpired() { + //given + AuthTokenManager timeOutTokenManger = new AuthAuthTokenManagerAdaptor( + new JwtTokenProvider(secretKey, 0, 0)); + + String token = timeOutTokenManger.generate(1L).accessToken(); + + //when + ThrowingCallable when = () -> authTokenManager.getId(token); + + //then + assertThrownByExpiredToken(when); + } } - @Test + @Nested @DisplayName("[토큰을 검증한다]") - void validateToken() { - //given - String token = jwtTokenProvider.createRefreshToken(1L); + class validateToken { - //when - Executable when = () -> authTokenManager.validateToken(token); + @Test + @DisplayName("[검증에 성공한다]") + void success() { + //given + String token = authTokenManager.generate(1L).accessToken(); - //then - assertDoesNotThrow(when); + //when + Executable when = () -> authTokenManager.validateToken(token); + + //then + assertDoesNotThrow(when); + } + + @Test + @DisplayName("[유효하지 않은 토큰으로 검증에 실패한다]") + void failByInvalidToken() { + //given + AuthTokenManager differentKeyTokenManager = new AuthAuthTokenManagerAdaptor( + new JwtTokenProvider(differentSecretKey, 100, 100)); + + String token1 = authTokenManager.generate(1L).accessToken() + "e"; + String token2 = differentKeyTokenManager.generate(1L).accessToken(); + + //when + ThrowingCallable when1 = () -> authTokenManager.validateToken(token1); + ThrowingCallable when2 = () -> authTokenManager.validateToken(token2); + + //then + assertThrownByInvalidToken(when1, when2); + } + + @Test + @DisplayName("[유효기간이 만료된 토큰으로 검증에 실패한다]") + void failByExpired() { + //given + AuthTokenManager timeOutTokenManger = new AuthAuthTokenManagerAdaptor( + new JwtTokenProvider(secretKey, 0, 0)); + + String token = timeOutTokenManger.generate(1L).accessToken(); + + //when + ThrowingCallable when = () -> authTokenManager.getId(token); + + //then + assertThrownByExpiredToken(when); + } + } + + private void assertThrownByExpiredToken(ThrowingCallable when) { + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(AUTH_EXPIRED_TOKEN.getMessage()); + } + + private void assertThrownByInvalidToken(ThrowingCallable... whens) { + for (ThrowingCallable when : whens) { + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(AUTH_INVALID_TOKEN.getMessage()); + } } } \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/AuthTokenMangerStub.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/AuthTokenMangerStub.java new file mode 100644 index 0000000..f46c2b8 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/AuthTokenMangerStub.java @@ -0,0 +1,21 @@ +package com.inq.wishhair.wesharewishhair.auth.stub; + +import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; + +public class AuthTokenMangerStub implements AuthTokenManager { + + @Override + public AuthToken generate(Long userId) { + return new AuthToken("accessToken", "refreshToken"); + } + + @Override + public Long getId(String token) { + return 1L; + } + + @Override + public void validateToken(String token) { + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java deleted file mode 100644 index a28fe69..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/stub/JwtTokenProviderStub.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.inq.wishhair.wesharewishhair.auth.stub; - -import com.inq.wishhair.wesharewishhair.auth.infrastructure.jwt.JwtTokenProvider; - -public class JwtTokenProviderStub extends JwtTokenProvider { - - private static final String ACCESS_TOKEN = "access_token"; - private static final String REFRESH_TOKEN = "refresh_token"; - - public JwtTokenProviderStub() { - super("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 0, 0); - } - - @Override - public String createAccessToken(final Long userId) { - return ACCESS_TOKEN; - } - - @Override - public String createRefreshToken(final Long userId) { - return REFRESH_TOKEN; - } - - @Override - public Long getPayload(final String token) { - return 1L; - } - - @Override - public void validateToken(final String token) { - } -} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/AuthenticationInterceptorTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/AuthenticationInterceptorTest.java new file mode 100644 index 0000000..de8c6aa --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/AuthenticationInterceptorTest.java @@ -0,0 +1,90 @@ +package com.inq.wishhair.wesharewishhair.global.interceptor.interceptor; + +import static com.inq.wishhair.wesharewishhair.global.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.inq.wishhair.wesharewishhair.auth.stub.AuthTokenMangerStub; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@DisplayName("[AuthenticationInterceptor 테스트] - Global") +class AuthenticationInterceptorTest { + + private static final String AUTHORIZATION = "Authorization"; + + private final AuthenticationInterceptor interceptor; + private final HttpServletRequest mockRequest; + private final HttpServletResponse mockResponse; + + public AuthenticationInterceptorTest() { + mockRequest = Mockito.mock(HttpServletRequest.class); + mockResponse = Mockito.mock(HttpServletResponse.class); + this.interceptor = new AuthenticationInterceptor(new AuthTokenMangerStub()); + } + + @Nested + @DisplayName("[요청에 대해서 인증을 수행한다]") + class preHandle { + + @Test + @DisplayName("[인증에 성공한다]") + void success() { + //given + given(mockRequest.getHeader(AUTHORIZATION)).willReturn("Bearer token"); + + //when + boolean actual = interceptor.preHandle(mockRequest, mockResponse, new Object()); + + //then + assertThat(actual).isTrue(); + } + + @Nested + @DisplayName("[인증에 실패한다]") + class fail { + + @Test + @DisplayName("[인증헤더 값이 존재하지 않아 실패한다]") + void failByNoHeader() { + //given + given(mockRequest.getHeader(AUTHORIZATION)).willReturn(""); + + //when + ThrowingCallable when = () -> interceptor.preHandle( + mockRequest, mockResponse, new Object() + ); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(AUTH_REQUIRED_LOGIN.getMessage()); + } + + @Test + @DisplayName("[인증헤더 값의 형식이 올바르지 않아 실패한다]") + void failByHeaderFormat() { + //given + given(mockRequest.getHeader(AUTHORIZATION)).willReturn("token"); + + //when + ThrowingCallable when = () -> interceptor.preHandle( + mockRequest, mockResponse, new Object() + ); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(AUTH_INVALID_AUTHORIZATION_HEADER.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainerTest.java new file mode 100644 index 0000000..8b20468 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/global/interceptor/interceptor/PathMatcherContainerTest.java @@ -0,0 +1,81 @@ +package com.inq.wishhair.wesharewishhair.global.interceptor.interceptor; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.http.HttpMethod.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("[PathMatcherContainer 테스트] - Global") +class PathMatcherContainerTest { + + private final PathMatcherContainer pathMatcherContainer; + + public PathMatcherContainerTest() { + //given + this.pathMatcherContainer = new PathMatcherContainer(); + pathMatcherContainer.includePathPattern("/include/**", GET); + pathMatcherContainer.includePathPattern("/path/include", GET); + + pathMatcherContainer.excludePathPattern("/exclude/**", GET); + pathMatcherContainer.excludePathPattern("/path/exclude", GET); + + pathMatcherContainer.includePathPattern("/exclude/path", GET); + } + + @Nested + @DisplayName("[인터셉터가 적용되어아햐는 요청인지 확인한다]") + class isInterceptorRequired { + + @Test + @DisplayName("[적용되어아야하는 요청으로 true 를 반환한다]") + void returnTrue() { + //when + boolean actual1 = pathMatcherContainer.isInterceptorRequired("/include/path", GET); + boolean actual2 = pathMatcherContainer.isInterceptorRequired("/path/include", GET); + + //then + assertAll( + () -> assertThat(actual1).isTrue(), + () -> assertThat(actual2).isTrue() + ); + } + + @Nested + @DisplayName("[적용되지않아야하는 요청으로 false 를 반환한다]") + class returnFalse { + + @Test + @DisplayName("[include 패턴에 포함되지만 exclude 패턴에도 포함되어서 false 를 반환한다]") + void returnFalse1() { + //when + boolean actual = pathMatcherContainer.isInterceptorRequired("/exclude/path", GET); + + //then + assertThat(actual).isFalse(); + } + + @Test + @DisplayName("[path 가 include 패턴에 포함되지 않아 false 를 반환한다]") + void returnFalse2() { + //when + boolean actual = pathMatcherContainer.isInterceptorRequired("/hello/path", GET); + + //then + assertThat(actual).isFalse(); + } + + @Test + @DisplayName("[메소드가 맞지 않아 false 를 반환한다]") + void returnFalse3() { + //when + boolean actual = pathMatcherContainer.isInterceptorRequired("/include/path", POST); + + //then + assertThat(actual).isFalse(); + } + } + } +} \ No newline at end of file From aa808a5a4b29f352c5269f10f4ec0a310f00ac9a Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Fri, 27 Oct 2023 19:51:54 +0900 Subject: [PATCH 04/30] =?UTF-8?q?test:=20Auth=20Api=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SecurityConfig.java | 8 ++ .../auth/application/AuthServiceTest.java | 11 +++ .../auth/presentation/AuthControllerTest.java | 76 ++++++++++++++++ .../presentation/MailAuthControllerTest.java | 91 +++++++++++++++++++ .../TokenReissueControllerTest.java | 54 +++++++++++ .../common/fixture/AuthFixture.java | 13 +++ .../common/support/ApiTestSupport.java | 30 ++++++ 7 files changed, 283 insertions(+) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/fixture/AuthFixture.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SecurityConfig.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SecurityConfig.java index 395a94a..7248635 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SecurityConfig.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SecurityConfig.java @@ -2,8 +2,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @@ -12,4 +15,9 @@ public class SecurityConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http.csrf(AbstractHttpConfigurer::disable).build(); + } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java index aec13af..a334fa0 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/AuthServiceTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; import org.mockito.Mockito; import org.springframework.security.crypto.password.PasswordEncoder; @@ -144,4 +145,14 @@ void failByInvalidPw() { .hasMessageContaining(ErrorCode.LOGIN_FAIL.getMessage()); } } + + @Test + @DisplayName("[로그아웃을 한다]") + void logout() { + //when + Executable when = () -> authService.logout(1L); + + //then + assertDoesNotThrow(when); + } } \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java new file mode 100644 index 0000000..12fb1fb --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java @@ -0,0 +1,76 @@ +package com.inq.wishhair.wesharewishhair.auth.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.auth.application.AuthService; +import com.inq.wishhair.wesharewishhair.auth.application.dto.response.LoginResponse; +import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.LoginRequest; +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; + +@WebMvcTest(value = {AuthController.class, SecurityConfig.class}) +@DisplayName("[AuthController 테스트] - API") +class AuthControllerTest extends ApiTestSupport { + + private static final String LOGIN_URL = "/api/auth/login"; + private static final String LOGOUT_URL = "/api/auth/logout"; + + @Autowired + private MockMvc mockMvc; + @MockBean + private AuthService authService; + + @Test + @DisplayName("[로그인 API 를 호출한다]") + void login() throws Exception { + //given + String email = "email"; + String pw = "pw"; + LoginRequest loginRequest = new LoginRequest(email, pw); + + LoginResponse loginResponse = new LoginResponse("token", "token"); + given(authService.login(email, pw)) + .willReturn(loginResponse); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(LOGIN_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(loginRequest)) + + ); + + //then + result.andExpectAll( + jsonPath("$.accessToken").value(loginResponse.accessToken()), + jsonPath("$.refreshToken").value(loginResponse.refreshToken()) + ); + } + + @Test + @DisplayName("[로그아웃 API 를 호출한다]") + void logout() throws Exception { + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(LOGOUT_URL) + .header(AUTHORIZATION, ACCESS_TOKEN) + ); + + //then + result.andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java new file mode 100644 index 0000000..048055d --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java @@ -0,0 +1,91 @@ +package com.inq.wishhair.wesharewishhair.auth.presentation; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.auth.application.MailAuthService; +import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.AuthKeyRequest; +import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.MailRequest; +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; +import com.inq.wishhair.wesharewishhair.user.application.utils.UserValidator; + +@WebMvcTest(value = {MailAuthController.class, SecurityConfig.class}) +@DisplayName("[MailAuthController 테스트] - API") +class MailAuthControllerTest extends ApiTestSupport { + + private static final String CHECK_DUPLICATED_EMAIL_URL = "/api/email/check"; + private static final String SEND_AUTHORIZATION_MAIL_URL = "/api/email/send"; + private static final String AUTHORIZE_KEY_URL = "/api/email/validate"; + private static final String EMAIL = "hello@naver.com"; + + @Autowired + private MockMvc mockMvc; + @MockBean + private UserValidator userValidator; + @MockBean + private MailAuthService mailAuthService; + + @Test + @DisplayName("[이메일 중복검사 API 를 호출한다]") + void checkDuplicateEmail() throws Exception { + //given + MailRequest mailRequest = new MailRequest(EMAIL); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(CHECK_DUPLICATED_EMAIL_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(mailRequest)) + ); + + //then + result.andExpect(status().isOk()); + } + + @Test + @DisplayName("[인증 메일 발송 API 를 호출한다]") + void sendAuthorizationMail() throws Exception { + //given + MailRequest mailRequest = new MailRequest(EMAIL); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(SEND_AUTHORIZATION_MAIL_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(mailRequest)) + ); + + //then + result.andExpect(status().isOk()); + } + + @Test + @DisplayName("[인증코드 확인 API 를 호출한다]") + void authorizeKey() throws Exception { + //given + AuthKeyRequest authKeyRequest = new AuthKeyRequest(EMAIL, "authcode"); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(AUTHORIZE_KEY_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(authKeyRequest)) + ); + + //then + result.andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java new file mode 100644 index 0000000..e79aead --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java @@ -0,0 +1,54 @@ +package com.inq.wishhair.wesharewishhair.auth.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.auth.application.TokenReissueService; +import com.inq.wishhair.wesharewishhair.auth.application.dto.response.TokenResponse; +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; + +@WebMvcTest(value = {TokenReissueController.class, SecurityConfig.class}) +@DisplayName("[TokenReissueController 테스트] - API") +class TokenReissueControllerTest extends ApiTestSupport { + + private static final String REISSUE_TOKEN_URL = "/api/token/reissue"; + + @Autowired + private MockMvc mockMvc; + @MockBean + private TokenReissueService tokenReissueService; + + @Test + @DisplayName("[토큰 재발급 API 를 호출한다]") + void reissueToken() throws Exception { + //given + TokenResponse tokenResponse = new TokenResponse("accessToken", "refreshToken"); + given(tokenReissueService.reissueToken(1L, TOKEN)) + .willReturn(tokenResponse); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(REISSUE_TOKEN_URL) + .header(AUTHORIZATION, ACCESS_TOKEN) + ); + + //then + result.andExpectAll( + status().isOk(), + jsonPath("$.accessToken").value(tokenResponse.accessToken()), + jsonPath("$.refreshToken").value(tokenResponse.refreshToken()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/fixture/AuthFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/fixture/AuthFixture.java new file mode 100644 index 0000000..dbf431c --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/fixture/AuthFixture.java @@ -0,0 +1,13 @@ +package com.inq.wishhair.wesharewishhair.common.fixture; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class AuthFixture { + + public static final String TOKEN = "token"; + public static final String BEARER = "Bearer "; + public static final String AUTHORIZATION = "Authorization"; + public static final String ACCESS_TOKEN = BEARER + TOKEN; +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java new file mode 100644 index 0000000..5e350e7 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java @@ -0,0 +1,30 @@ +package com.inq.wishhair.wesharewishhair.common.support; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.mock.mockito.MockBean; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; +import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; + +public abstract class ApiTestSupport { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @MockBean + protected AuthTokenManager authTokenManager; + + @BeforeEach + public void setAuthorization() { + given(authTokenManager.generate(any(Long.class))).willReturn(new AuthToken(TOKEN, TOKEN)); + given(authTokenManager.getId(anyString())).willReturn(1L); + } + + public String toJson(Object object) throws JsonProcessingException { + return objectMapper.writeValueAsString(object); + } +} From 67cfec5f482c5576a7b49e159e0af11f2f82aecc Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Tue, 31 Oct 2023 15:38:28 +0900 Subject: [PATCH 05/30] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A6=AC=EB=B7=B0=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20join=20=EC=A0=9C=EA=B1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20Review=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20lik?= =?UTF-8?q?eCount=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=8A=94=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SchedulerConfig.java | 9 ++ .../query/HairStyleQueryDslRepository.java | 2 + .../review/application/LikeReviewService.java | 40 ++++----- .../review/application/ReviewFindService.java | 5 ++ .../application/ReviewSearchService.java | 19 ++--- .../dto/response/ReviewResponse.java | 2 + .../dto/response/ReviewResponseAssembler.java | 20 +++-- .../query/ReviewQueryRepository.java | 31 ------- .../query/dto/ReviewQueryResponse.java | 12 --- .../scheduler/LikeCountUpdater.java | 46 ++++++++++ .../review/config/ReviewConfig.java | 11 +++ .../review/domain/ReviewQueryRepository.java | 30 +++++++ .../review/domain/ReviewRepository.java | 10 +++ .../review/domain/entity/Review.java | 16 +++- .../review/domain/likereview/LikeReview.java | 3 - .../infrastructure/ReviewJpaRepository.java | 18 ++++ .../ReviewQueryDslRepository.java | 64 +++++--------- src/main/resources/application.yml | 3 +- .../user/fixture/UserFixture.java | 2 +- src/test/resources/application.yml | 85 ++++++++++++++++++- 20 files changed, 292 insertions(+), 136 deletions(-) create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/global/config/SchedulerConfig.java delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/ReviewQueryRepository.java delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/dto/ReviewQueryResponse.java create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepository.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SchedulerConfig.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SchedulerConfig.java new file mode 100644 index 0000000..6d674b5 --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/config/SchedulerConfig.java @@ -0,0 +1,9 @@ +package com.inq.wishhair.wesharewishhair.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulerConfig { +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java index 31c14b5..d8503ab 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import com.inq.wishhair.wesharewishhair.hairstyle.application.query.HairStyleQueryRepository; @@ -23,6 +24,7 @@ import lombok.RequiredArgsConstructor; +@Repository @Transactional(readOnly = true) @RequiredArgsConstructor public class HairStyleQueryDslRepository implements HairStyleQueryRepository { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java index ad91df7..e436253 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java @@ -1,54 +1,54 @@ package com.inq.wishhair.wesharewishhair.review.application; +import java.util.Set; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; -import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReview; import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; import com.inq.wishhair.wesharewishhair.review.application.dto.response.LikeReviewResponse; import lombok.RequiredArgsConstructor; +//todo : 캐시를 이용해서 DB 에 변경을 줄이는 방식 적용해보기 @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class LikeReviewService { private final LikeReviewRepository likeReviewRepository; + private final ReviewFindService reviewFindService; + private final Set updateReviewSet; @Transactional public void executeLike(Long reviewId, Long userId) { - validateIsNotLiking(userId, reviewId); + if (notExistLikeReview(userId, reviewId)) { + likeReviewRepository.save(LikeReview.addLike(userId, reviewId)); + + Review review = reviewFindService.getById(reviewId); + review.addLike(); - likeReviewRepository.save(LikeReview.addLike(userId, reviewId)); + updateReviewSet.add(reviewId); + } } @Transactional public void cancelLike(Long reviewId, Long userId) { - validateIsLiking(userId, reviewId); - likeReviewRepository.deleteByUserIdAndReviewId(userId, reviewId); - } - public LikeReviewResponse checkIsLiking(Long userId, Long reviewId) { - return new LikeReviewResponse(existLikeReview(userId, reviewId)); - } + Review review = reviewFindService.getById(reviewId); + review.cancelLike(); - private boolean existLikeReview(Long userId, Long reviewId) { - return likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); + updateReviewSet.add(reviewId); } - private void validateIsNotLiking(Long userId, Long reviewId) { - if (existLikeReview(userId, reviewId)) { - throw new WishHairException(ErrorCode.REVIEW_ALREADY_LIKING); - } + public LikeReviewResponse checkIsLiking(Long userId, Long reviewId) { + return new LikeReviewResponse(notExistLikeReview(userId, reviewId)); } - private void validateIsLiking(Long userId, Long reviewId) { - if (!existLikeReview(userId, reviewId)) { - throw new WishHairException(ErrorCode.REVIEW_NOT_LIKING); - } + private boolean notExistLikeReview(Long userId, Long reviewId) { + return !likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java index 793639f..da83c53 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java @@ -17,6 +17,11 @@ public class ReviewFindService { private final ReviewRepository reviewRepository; + public Review getById(Long id) { + return reviewRepository.findById(id) + .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); + } + public Review findWithPhotosById(Long id) { return reviewRepository.findWithPhotosById(id) .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java index 50c24b4..f181e2b 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java @@ -17,8 +17,7 @@ import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewDetailResponse; import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewResponse; import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewSimpleResponse; -import com.inq.wishhair.wesharewishhair.review.application.query.ReviewQueryRepository; -import com.inq.wishhair.wesharewishhair.review.application.query.dto.ReviewQueryResponse; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewQueryRepository; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; @@ -29,52 +28,52 @@ @Transactional(readOnly = true) public class ReviewSearchService { - private final ReviewQueryRepository reviewRepository; + private final ReviewQueryRepository reviewQueryRepository; private final LikeReviewRepository likeReviewRepository; /*리뷰 단건 조회*/ @AddisWriter public ReviewDetailResponse findReviewById(Long userId, Long reviewId) { - ReviewQueryResponse queryResponse = reviewRepository.findReviewById(reviewId) + Review review = reviewQueryRepository.findReviewById(reviewId) .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); boolean isLiking = likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); - return toReviewDetailResponse(queryResponse, isLiking); + return toReviewDetailResponse(review, isLiking); } /*전체 리뷰 조회*/ @AddisWriter public PagedResponse findPagedReviews(Long userId, Pageable pageable) { - Slice sliceResult = reviewRepository.findReviewByPaging(pageable); + Slice sliceResult = reviewQueryRepository.findReviewByPaging(pageable); return toPagedReviewResponse(sliceResult); } /*좋아요한 리뷰 조회*/ @AddisWriter public PagedResponse findLikingReviews(Long userId, Pageable pageable) { - Slice sliceResult = reviewRepository.findReviewByLike(userId, pageable); + Slice sliceResult = reviewQueryRepository.findReviewByLike(userId, pageable); return toPagedReviewResponse(sliceResult); } /*나의 리뷰 조회*/ @AddisWriter public PagedResponse findMyReviews(Long userId, Pageable pageable) { - Slice sliceResult = reviewRepository.findReviewByUser(userId, pageable); + Slice sliceResult = reviewQueryRepository.findReviewByUser(userId, pageable); return toPagedReviewResponse(sliceResult); } /*이달의 추천 리뷰 조회*/ public ResponseWrapper findReviewOfMonth() { - List result = reviewRepository.findReviewByCreatedDate(); + List result = reviewQueryRepository.findReviewByCreatedDate(); return toWrappedSimpleResponse(result); } /*헤어스타일의 리뷰 조회*/ @AddisWriter public ResponseWrapper findReviewByHairStyle(Long userId, Long hairStyleId) { - List result = reviewRepository.findReviewByHairStyle(hairStyleId); + List result = reviewQueryRepository.findReviewByHairStyle(hairStyleId); return toWrappedReviewResponse(result); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponse.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponse.java index 111d98e..bef963e 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponse.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponse.java @@ -24,8 +24,10 @@ public class ReviewResponse { private long likes; private List hashTags; private boolean isWriter; + @JsonIgnore private Long writerId; + public void addIsWriter(Long userId) { this.isWriter = writerId.equals(userId); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java index 4325698..1fca5bc 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java @@ -9,7 +9,6 @@ import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; import com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper; -import com.inq.wishhair.wesharewishhair.review.application.query.dto.ReviewQueryResponse; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import lombok.AccessLevel; @@ -18,16 +17,15 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ReviewResponseAssembler { - public static PagedResponse toPagedReviewResponse(Slice slice) { + public static PagedResponse toPagedReviewResponse(Slice slice) { return new PagedResponse<>(transferContentToResponse(slice)); } - private static Slice transferContentToResponse(Slice slice) { + private static Slice transferContentToResponse(Slice slice) { return slice.map(ReviewResponseAssembler::toReviewResponse); } - public static ReviewResponse toReviewResponse(ReviewQueryResponse queryResponse) { - Review review = queryResponse.review(); + public static ReviewResponse toReviewResponse(Review review) { return ReviewResponse.builder() .reviewId(review.getId()) @@ -37,14 +35,17 @@ public static ReviewResponse toReviewResponse(ReviewQueryResponse queryResponse) .contents(review.getContentsValue()) .createdDate(review.getCreatedDate()) .photos(toPhotoResponses(review.getPhotos())) - .likes(queryResponse.likes()) + .likes(review.getLikeCount()) .hashTags(toHashTagResponses(review.getHairStyle().getHashTags())) .writerId(review.getWriter().getId()) .build(); } - public static ReviewDetailResponse toReviewDetailResponse(ReviewQueryResponse queryResponse, boolean isLiking) { - return new ReviewDetailResponse(toReviewResponse(queryResponse), isLiking); + public static ReviewDetailResponse toReviewDetailResponse( + Review review, + boolean isLiking + ) { + return new ReviewDetailResponse(toReviewResponse(review), isLiking); } public static ResponseWrapper toWrappedSimpleResponse(List reviews) { @@ -52,10 +53,11 @@ public static ResponseWrapper toWrappedSimpleResponse(List return new ResponseWrapper<>(responses); } - public static ResponseWrapper toWrappedReviewResponse(List responses) { + public static ResponseWrapper toWrappedReviewResponse(List responses) { List reviewResponses = responses.stream() .map(ReviewResponseAssembler::toReviewResponse) .toList(); + return new ResponseWrapper<>(reviewResponses); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/ReviewQueryRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/ReviewQueryRepository.java deleted file mode 100644 index efe818d..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/ReviewQueryRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.inq.wishhair.wesharewishhair.review.application.query; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; - -import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; -import com.inq.wishhair.wesharewishhair.review.application.query.dto.ReviewQueryResponse; - -public interface ReviewQueryRepository { - - //리뷰 단건 조회 - Optional findReviewById(Long id); - - //전체 리뷰 조회 - Slice findReviewByPaging(Pageable pageable); - - //좋아요 한 리뷰 조회 - Slice findReviewByLike(Long userId, Pageable pageable); - - //작성한 리뷰 조회 - Slice findReviewByUser(Long userId, Pageable pageable); - - //지난달에 작성한 리뷰 조회 - List findReviewByCreatedDate(); - - //헤어스타일의 리뷰 조회 - List findReviewByHairStyle(Long hairStyleId); -} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/dto/ReviewQueryResponse.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/dto/ReviewQueryResponse.java deleted file mode 100644 index a7cc097..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/query/dto/ReviewQueryResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.inq.wishhair.wesharewishhair.review.application.query.dto; - -import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; -import com.querydsl.core.annotations.QueryProjection; - -public record ReviewQueryResponse(Review review, long likes) { - - @QueryProjection - public ReviewQueryResponse { - //QueryProjection 적용 - } -} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java new file mode 100644 index 0000000..90b12e2 --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java @@ -0,0 +1,46 @@ +package com.inq.wishhair.wesharewishhair.review.application.scheduler; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class LikeCountUpdater { + + private static final Comparator ASC = Long::compare; + + private final ReviewRepository reviewRepository; + private final Set updateReviewSet; + + @Scheduled(fixedRate = 2 * 60 * 60 * 1000) // 2시간마다 실행 + public void syncLikeCount() { + List orderedLikeCounts = reviewRepository.countLikeReviewByIdsOrderById(updateReviewSet); + List orderedReviewIds = getOrderedReviewIds(); + + int bound = orderedReviewIds.size(); + for (int i = 0; i < bound; i++) { + int likeCount = orderedLikeCounts.get(i); + Long reviewId = orderedReviewIds.get(i); + + reviewRepository.updateLikeCountById(reviewId, likeCount); + } + + updateReviewSet.clear(); + } + + private List getOrderedReviewIds() { + List reviewIds = new ArrayList<>(updateReviewSet); + reviewIds.sort(ASC); + + return reviewIds; + } +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java index dfc14f5..8434c42 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java @@ -1,5 +1,10 @@ package com.inq.wishhair.wesharewishhair.review.config; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -13,4 +18,10 @@ public class ReviewConfig implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { registry.addConverter(new ScoreConverter()); } + + @Bean + public Set updateReviewMap() { + Map concurrentHashMap = new ConcurrentHashMap<>(); + return concurrentHashMap.keySet(); + } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepository.java new file mode 100644 index 0000000..74c702e --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepository.java @@ -0,0 +1,30 @@ +package com.inq.wishhair.wesharewishhair.review.domain; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +public interface ReviewQueryRepository { + + //리뷰 단건 조회 + Optional findReviewById(Long id); + + //전체 리뷰 조회 + Slice findReviewByPaging(Pageable pageable); + + //좋아요 한 리뷰 조회 + Slice findReviewByLike(Long userId, Pageable pageable); + + //작성한 리뷰 조회 + Slice findReviewByUser(Long userId, Pageable pageable); + + //지난달에 작성한 리뷰 조회 + List findReviewByCreatedDate(); + + //헤어스타일의 리뷰 조회 + List findReviewByHairStyle(Long hairStyleId); +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java index d5e720f..8930127 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java @@ -2,6 +2,9 @@ import java.util.List; import java.util.Optional; +import java.util.Set; + +import org.springframework.data.repository.query.Param; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; @@ -9,6 +12,8 @@ public interface ReviewRepository { Review save(Review review); + Optional findById(Long id); + //review find service - 리뷰 단순 조회 Optional findWithPhotosById(Long id); @@ -18,4 +23,9 @@ public interface ReviewRepository { void deleteAllByWriter(List reviewIds); void delete(Review review); + + //reviewIds 에 해당되는 리뷰의 좋아요 수 조회 + List countLikeReviewByIdsOrderById(@Param("ids") Set reviewIds); + + void updateLikeCountById(Long id, int likeCount); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index 849a59c..5436e26 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -44,14 +44,15 @@ public class Review extends BaseEntity { @Enumerated(EnumType.STRING) private Score score; - @OneToMany(mappedBy = "review", - cascade = CascadeType.PERSIST) // 사진을 값타입 컬렉션 처럼 사용 + @OneToMany(mappedBy = "review", cascade = CascadeType.PERSIST) // 사진을 값타입 컬렉션 처럼 사용 private final List photos = new ArrayList<>(); @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "hair_style_id") private HairStyle hairStyle; + private Long likeCount; + private Review(User writer, String contents, Score score, List photos, HairStyle hairStyle) { this.writer = writer; this.contents = new Contents(contents); @@ -59,6 +60,7 @@ private Review(User writer, String contents, Score score, List photos, H applyPhotos(photos); this.hairStyle = hairStyle; this.createdDate = LocalDateTime.now(); + this.likeCount = 0L; } //==Factory method==// @@ -86,6 +88,16 @@ public void updateReview(Contents contents, Score score, List storeUrls) updatePhotos(storeUrls); } + public void addLike() { + this.likeCount++; + } + + public void cancelLike() { + if (likeCount > 0) { + this.likeCount--; + } + } + private void updateContents(Contents contents) { this.contents = contents; } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java index f42a331..53e8672 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java @@ -4,7 +4,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,10 +17,8 @@ public class LikeReview { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @JoinColumn private Long userId; - @JoinColumn private Long reviewId; //==생성 메서드==// diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java index 216b10a..0c18359 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -28,4 +29,21 @@ public interface ReviewJpaRepository extends ReviewRepository, JpaRepository reviewIds); + + @Query("select case " + + "when l.reviewId is null then 0 " + + "else count(r.id) end " + + "from Review r " + + "left join LikeReview l on r.id = l.reviewId " + + "where r.id in :reviewIds " + + "group by r.id " + + "order by r.id") + List countLikeReviewByIdsOrderById(@Param("reviewIds") Set reviewIds); + + @Modifying + @Query("update Review r SET r.likeCount = :likeCount where r.id = :id") + void updateLikeCountById( + @Param("id") Long id, + @Param("likeCount") int likeCount + ); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java index b7df7d6..72f9b54 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java @@ -13,16 +13,11 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import com.inq.wishhair.wesharewishhair.review.application.query.ReviewQueryRepository; -import com.inq.wishhair.wesharewishhair.review.application.query.dto.QReviewQueryResponse; -import com.inq.wishhair.wesharewishhair.review.application.query.dto.ReviewQueryResponse; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewQueryRepository; import com.inq.wishhair.wesharewishhair.review.domain.entity.QReview; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import com.inq.wishhair.wesharewishhair.review.domain.likereview.QLikeReview; -import com.querydsl.core.types.ConstructorExpression; import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.dsl.CaseBuilder; -import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -38,39 +33,30 @@ public class ReviewQueryDslRepository implements ReviewQueryRepository { private final QReview review = new QReview("r"); private final QLikeReview like = new QLikeReview("l"); - private final NumberExpression likes = new CaseBuilder() - .when(like.id.sum().isNull()) - .then(0L) - .otherwise(review.id.count()); - @Override - public Optional findReviewById(Long id) { + public Optional findReviewById(Long id) { return Optional.ofNullable( factory - .select(assembleReviewProjection()) + .select(review) .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() .where(review.id.eq(id)) - .groupBy(review.id) .fetchOne() ); } @Override - public Slice findReviewByPaging(Pageable pageable) { - List result = factory - .select(assembleReviewProjection()) + public Slice findReviewByPaging(Pageable pageable) { + List result = factory + .select(review) .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() - .groupBy(review.id) .orderBy(applyOrderBy(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1L) @@ -80,7 +66,7 @@ public Slice findReviewByPaging(Pageable pageable) { } @Override - public Slice findReviewByLike(Long userId, Pageable pageable) { + public Slice findReviewByLike(Long userId, Pageable pageable) { List filteredReviewId = factory .select(review.id) .from(review) @@ -89,16 +75,14 @@ public Slice findReviewByLike(Long userId, Pageable pageabl .groupBy(review.id) .fetch(); - List result = factory - .select(assembleReviewProjection()) + List result = factory + .select(review) .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.writer) .fetchJoin() .leftJoin(review.hairStyle) .fetchJoin() .where(review.id.in(filteredReviewId)) - .groupBy(review.id) .orderBy(review.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1L) @@ -108,22 +92,20 @@ public Slice findReviewByLike(Long userId, Pageable pageabl } @Override - public Slice findReviewByUser(Long userId, Pageable pageable) { - JPAQuery query = factory - .select(assembleReviewProjection()) + public Slice findReviewByUser(Long userId, Pageable pageable) { + JPAQuery query = factory + .select(review) .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() - .groupBy(review.id) .orderBy(applyOrderBy(pageable)) .where(review.writer.id.eq(userId)) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1L); - List result = query.fetch(); + List result = query.fetch(); return new SliceImpl<>(result, pageable, validateHasNext(pageable, result)); } @@ -135,48 +117,40 @@ public List findReviewByCreatedDate() { return factory .select(review) .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() .where(review.createdDate.between(startDate, endDate)) - .groupBy(review.id) - .orderBy(likes.desc()) + .orderBy(review.likeCount.desc()) .offset(0) .limit(4) .fetch(); } @Override - public List findReviewByHairStyle(Long hairStyleId) { + public List findReviewByHairStyle(Long hairStyleId) { return factory - .select(assembleReviewProjection()) + .select(review) .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() .where(review.hairStyle.id.eq(hairStyleId)) - .groupBy(review.id) - .orderBy(likes.desc()) + .orderBy(review.likeCount.desc()) .offset(0) .limit(4) .fetch(); } - private ConstructorExpression assembleReviewProjection() { - return new QReviewQueryResponse(review, likes.as("likes")); - } - private OrderSpecifier[] applyOrderBy(Pageable pageable) { List> orderBy = new LinkedList<>(); String sort = pageable.getSort().toString().replace(": ", "."); switch (sort) { case LIKES_DESC -> { - orderBy.add(likes.desc()); + orderBy.add(review.likeCount.desc()); orderBy.add(review.id.desc()); } case DATE_DESC -> orderBy.add(review.id.desc()); @@ -185,7 +159,7 @@ private OrderSpecifier[] applyOrderBy(Pageable pageable) { return orderBy.toArray(OrderSpecifier[]::new); } - private boolean validateHasNext(Pageable pageable, List result) { + private boolean validateHasNext(Pageable pageable, List result) { if (result.size() > pageable.getPageSize()) { result.remove(pageable.getPageSize()); return true; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6c35476..811c50e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,13 +9,14 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: default_batch_fetch_size: 100 show_sql: true format_sql: true + dialect: org.hibernate.dialect.MySQL57Dialect open-in-view: false diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java index 27d2114..07e83cf 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java @@ -6,7 +6,7 @@ import com.inq.wishhair.wesharewishhair.user.domain.entity.Password; import com.inq.wishhair.wesharewishhair.user.domain.entity.Sex; import com.inq.wishhair.wesharewishhair.user.domain.entity.User; - + import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index d10fe77..7718ae2 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,3 +1,84 @@ +#spring: +# profiles: +# default: test + +server: + port: 8080 + spring: - profiles: - default: test \ No newline at end of file + datasource: + url: jdbc:mysql://localhost:3306/wish_hair_db + username: root + password: 1234! + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + + properties: + hibernate: + default_batch_fetch_size: 100 + show_sql: true + format_sql: true + dialect: org.hibernate.dialect.MySQL57Dialect + + open-in-view: false + + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL} # namhm23@kyonggi.ac.kr + password: ${MAIL_PW} # qkvpxhpgyuywcbgh + protocol: smtp + properties: + mail: + smtp: + starttls: + enable: true + auth: true + + servlet: + multipart: + max-file-size: 10MB + +# 포인트 메일 수신자 # +mail: + point-mail-receiver: ${POINT_MAIL} #namhm23@naver.com + +# Flask domain # +flask: + domain: ${FLASK_DOMAIN} + +# 서버 런시 발생하는 에러 로그 방지 +logging: + level: + com: + amazonaws: + util: + EC2MetadataUtils: error + +#p6spy 설정 +decorator: + datasource: + p6spy: + enable-logging: true + +#JWT key +jwt: + secret-key: ${JWT_SECRET_KEY} # wishhairOiJIUzI1NiIvLoAR5cCI6IkpXSCJ9.eyJzdWIiOiIiLCLoCP1lIjoiSm9obiBEV9UiLCJpYXBCusE1MTYyMzkwMjJ9.163aevla8s7d6f987qweahqwculaoxce80k1i2o387tg + access-token-validity: ${ACCESS_TOKEN_VALIDITY} # 1800000 + refresh-token-validity: ${REFRESH_TOKEN_VALIDITY} # 259200000 + +# 네이버 클라우드 오브젝트 스토리지 +cloud: + aws: + credentials: + access-key: ${S3_ACCESS_KEY} # N3JCSJUtgse4alKEaOqX + secret-key: ${S3_SECRET_KEY} #rRamLrfUV8v6ZziJFEN4CVLjE9rl4kCBnEOLje5f + stack: + auto: false + region: + static: ap-northeast-2 + s3: + endpoint: https://kr.object.ncloudstorage.com + bucket: ${BUCKET_NAME} # wswh-storage From b303b90615dd7286e735dfa0601a41ed035c5943 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Fri, 3 Nov 2023 13:11:17 +0900 Subject: [PATCH 06/30] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=B4=EC=84=9C=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=EC=84=B1=20+=20=EC=84=B1=EB=8A=A5=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../global/config/RedisConfig.java | 33 +++++++ .../global/exception/ErrorCode.java | 4 +- .../global/utils/RedisUtils.java | 46 ++++++++++ .../review/application/LikeReviewService.java | 68 ++++++++++----- .../application/ReviewSearchService.java | 32 +++++-- .../dto/response/ReviewResponseAssembler.java | 41 ++++++--- .../scheduler/LikeCountUpdater.java | 46 ---------- .../review/config/ReviewConfig.java | 11 --- .../review/domain/likereview/LikeReview.java | 3 + .../likereview/LikeReviewRepository.java | 4 +- .../LikeReviewJpaRepository.java | 8 +- .../ReviewQueryDslRepository.java | 2 + src/main/resources/application.yml | 19 +++-- .../wishhair/wesharewishhair/LikeTest.java | 45 ++++++++++ src/test/resources/application-test.yml | 19 ++--- src/test/resources/application.yml | 85 +------------------ 17 files changed, 266 insertions(+), 202 deletions(-) create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/global/config/RedisConfig.java create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java diff --git a/build.gradle b/build.gradle index cb0e616..1c11ca6 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,8 @@ dependencies { //DB runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson-spring-boot-starter:3.24.3' //네이버 클라우드 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/config/RedisConfig.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/config/RedisConfig.java new file mode 100644 index 0000000..01391c0 --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/config/RedisConfig.java @@ -0,0 +1,33 @@ +package com.inq.wishhair.wesharewishhair.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + public String host; + @Value("${spring.data.redis.port}") + public int port; + + @Bean + public LettuceConnectionFactory lettuceConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate cacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); + return redisTemplate; + } +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ErrorCode.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ErrorCode.java index 8ddd7f2..6b1c6d1 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ErrorCode.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ErrorCode.java @@ -61,7 +61,9 @@ public enum ErrorCode { FLASK_SERVER_EXCEPTION("FLASK_001", "Flask 서버 요청 간 에러가 발생하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR), FLASK_RESPONSE_ERROR("FLASK_002", "Flask 서버의 응답값의 형식이 올바르지 않습니다.", HttpStatus.INTERNAL_SERVER_ERROR), - AOP_GENERIC_EXCEPTION("AOP_001", "AOP 에서 발생한 Generic 에러 입니다.", HttpStatus.INTERNAL_SERVER_ERROR); + AOP_GENERIC_EXCEPTION("AOP_001", "AOP 에서 발생한 Generic 에러 입니다.", HttpStatus.INTERNAL_SERVER_ERROR), + + REDIS_FAIL_ACQUIRE_LOCK("REDIS_001", "서버 일시적 오류입니다. 재시도 해주세요", HttpStatus.INTERNAL_SERVER_ERROR); private final String code; private final String message; diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java new file mode 100644 index 0000000..eebc0c9 --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java @@ -0,0 +1,46 @@ +package com.inq.wishhair.wesharewishhair.global.utils; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +public class RedisUtils { + + private final RedisTemplate redisTemplate; + private final long expireTime; + + public RedisUtils( + RedisTemplate redisTemplate, + @Value("${spring.data.redis.expire-time}") long expireTime + ) { + this.redisTemplate = redisTemplate; + this.expireTime = expireTime; + } + + public void setData(Long key, Long value) { + redisTemplate + .opsForValue() + .set(String.valueOf(key), value, expireTime, TimeUnit.MILLISECONDS); + } + + public void increaseData(Long key) { + redisTemplate.opsForValue().increment(String.valueOf(key)); + } + + public void decreaseData(Long key) { + redisTemplate.opsForValue().decrement(String.valueOf(key)); + } + + public Optional getData(Long key) { + return Optional.ofNullable( + redisTemplate.opsForValue().get(String.valueOf(key)) + ); + } +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java index e436253..e844b56 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java @@ -1,54 +1,82 @@ package com.inq.wishhair.wesharewishhair.review.application; -import java.util.Set; +import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.global.utils.RedisUtils; +import com.inq.wishhair.wesharewishhair.review.application.dto.response.LikeReviewResponse; import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReview; import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; -import com.inq.wishhair.wesharewishhair.review.application.dto.response.LikeReviewResponse; +import jakarta.persistence.EntityExistsException; import lombok.RequiredArgsConstructor; -//todo : 캐시를 이용해서 DB 에 변경을 줄이는 방식 적용해보기 @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class LikeReviewService { private final LikeReviewRepository likeReviewRepository; - private final ReviewFindService reviewFindService; - private final Set updateReviewSet; + private final RedisUtils redisUtils; @Transactional - public void executeLike(Long reviewId, Long userId) { - if (notExistLikeReview(userId, reviewId)) { + public boolean executeLike(Long reviewId, Long userId) { + try { likeReviewRepository.save(LikeReview.addLike(userId, reviewId)); + } catch (EntityExistsException e) { + return false; + } - Review review = reviewFindService.getById(reviewId); - review.addLike(); + //락을 걸지않고 값이없으면 좋아요 개수를 로드해서 반영 기능 추가 + redisUtils.getData(reviewId) + .ifPresentOrElse( + likeCount -> redisUtils.increaseData(reviewId), + () -> updateLikeCountFromRedis(reviewId) + ); - updateReviewSet.add(reviewId); - } + return true; } @Transactional - public void cancelLike(Long reviewId, Long userId) { - likeReviewRepository.deleteByUserIdAndReviewId(userId, reviewId); + public boolean cancelLike(Long reviewId, Long userId) { + int deletedCount = likeReviewRepository.deleteByUserIdAndReviewId(userId, reviewId); + if (deletedCount == 0) { + return false; + } - Review review = reviewFindService.getById(reviewId); - review.cancelLike(); + redisUtils.getData(reviewId) + .ifPresentOrElse( + likeCount -> redisUtils.decreaseData(reviewId), + () -> updateLikeCountFromRedis(reviewId) + ); - updateReviewSet.add(reviewId); + return true; } public LikeReviewResponse checkIsLiking(Long userId, Long reviewId) { - return new LikeReviewResponse(notExistLikeReview(userId, reviewId)); + return new LikeReviewResponse(existLikeReview(userId, reviewId)); + } + + public Long getLikeCount(Long reviewId) { + return redisUtils.getData(reviewId) + .orElse(updateLikeCountFromRedis(reviewId)); + } + + public List getLikeCounts(List reviewIds) { + return reviewIds.stream() + .map(this::getLikeCount) + .toList(); + } + + private Long updateLikeCountFromRedis(Long reviewId) { + Long likeCount = likeReviewRepository.countByReviewId(reviewId); + redisUtils.setData(reviewId, likeCount); + return likeCount; } - private boolean notExistLikeReview(Long userId, Long reviewId) { - return !likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); + private boolean existLikeReview(Long userId, Long reviewId) { + return likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java index f181e2b..65f1b60 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java @@ -19,7 +19,6 @@ import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewSimpleResponse; import com.inq.wishhair.wesharewishhair.review.domain.ReviewQueryRepository; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; -import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; import lombok.RequiredArgsConstructor; @@ -29,7 +28,7 @@ public class ReviewSearchService { private final ReviewQueryRepository reviewQueryRepository; - private final LikeReviewRepository likeReviewRepository; + private final LikeReviewService likeReviewService; /*리뷰 단건 조회*/ @AddisWriter @@ -37,23 +36,30 @@ public ReviewDetailResponse findReviewById(Long userId, Long reviewId) { Review review = reviewQueryRepository.findReviewById(reviewId) .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); - boolean isLiking = likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); + Long likeCount = likeReviewService.getLikeCount(review.getId()); + boolean isLiking = likeReviewService.checkIsLiking(userId, reviewId).isLiking(); - return toReviewDetailResponse(review, isLiking); + return toReviewDetailResponse(review, likeCount, isLiking); } /*전체 리뷰 조회*/ @AddisWriter public PagedResponse findPagedReviews(Long userId, Pageable pageable) { Slice sliceResult = reviewQueryRepository.findReviewByPaging(pageable); - return toPagedReviewResponse(sliceResult); + + List likeCounts = fetchLikeCounts(sliceResult.getContent()); + + return toPagedReviewResponse(sliceResult, likeCounts); } /*좋아요한 리뷰 조회*/ @AddisWriter public PagedResponse findLikingReviews(Long userId, Pageable pageable) { Slice sliceResult = reviewQueryRepository.findReviewByLike(userId, pageable); - return toPagedReviewResponse(sliceResult); + + List likeCounts = fetchLikeCounts(sliceResult.getContent()); + + return toPagedReviewResponse(sliceResult, likeCounts); } /*나의 리뷰 조회*/ @@ -61,7 +67,9 @@ public PagedResponse findLikingReviews(Long userId, Pageable pag public PagedResponse findMyReviews(Long userId, Pageable pageable) { Slice sliceResult = reviewQueryRepository.findReviewByUser(userId, pageable); - return toPagedReviewResponse(sliceResult); + List likeCounts = fetchLikeCounts(sliceResult.getContent()); + + return toPagedReviewResponse(sliceResult, likeCounts); } /*이달의 추천 리뷰 조회*/ @@ -74,6 +82,14 @@ public ResponseWrapper findReviewOfMonth() { @AddisWriter public ResponseWrapper findReviewByHairStyle(Long userId, Long hairStyleId) { List result = reviewQueryRepository.findReviewByHairStyle(hairStyleId); - return toWrappedReviewResponse(result); + + List likeCounts = fetchLikeCounts(result); + + return toWrappedReviewResponse(result, likeCounts); + } + + private List fetchLikeCounts(List result) { + List reviewIds = result.stream().map(Review::getId).toList(); + return likeReviewService.getLikeCounts(reviewIds); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java index 1fca5bc..f7028c2 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewResponseAssembler.java @@ -3,9 +3,11 @@ import static com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleResponseAssembler.*; import static com.inq.wishhair.wesharewishhair.photo.application.dto.response.PhotoResponseAssembler.*; +import java.util.ArrayList; import java.util.List; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; import com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper; @@ -17,15 +19,25 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ReviewResponseAssembler { - public static PagedResponse toPagedReviewResponse(Slice slice) { - return new PagedResponse<>(transferContentToResponse(slice)); + public static PagedResponse toPagedReviewResponse(Slice slice, List likeCounts) { + return new PagedResponse<>(transferContentToResponse(slice, likeCounts)); } - private static Slice transferContentToResponse(Slice slice) { - return slice.map(ReviewResponseAssembler::toReviewResponse); + private static Slice transferContentToResponse(Slice slice, List likeCounts) { + List reviews = slice.getContent(); + + List reviewResponses = new ArrayList<>(); + for (int i = 0; i < reviews.size(); i++) { + Review review = reviews.get(i); + Long likeCount = likeCounts.get(i); + + reviewResponses.add(toReviewResponse(review, likeCount)); + } + + return new SliceImpl<>(reviewResponses, slice.getPageable(), slice.hasNext()); } - public static ReviewResponse toReviewResponse(Review review) { + public static ReviewResponse toReviewResponse(Review review, Long likeCount) { return ReviewResponse.builder() .reviewId(review.getId()) @@ -35,7 +47,7 @@ public static ReviewResponse toReviewResponse(Review review) { .contents(review.getContentsValue()) .createdDate(review.getCreatedDate()) .photos(toPhotoResponses(review.getPhotos())) - .likes(review.getLikeCount()) + .likes(likeCount) .hashTags(toHashTagResponses(review.getHairStyle().getHashTags())) .writerId(review.getWriter().getId()) .build(); @@ -43,9 +55,10 @@ public static ReviewResponse toReviewResponse(Review review) { public static ReviewDetailResponse toReviewDetailResponse( Review review, + Long likeCount, boolean isLiking ) { - return new ReviewDetailResponse(toReviewResponse(review), isLiking); + return new ReviewDetailResponse(toReviewResponse(review, likeCount), isLiking); } public static ResponseWrapper toWrappedSimpleResponse(List reviews) { @@ -53,10 +66,16 @@ public static ResponseWrapper toWrappedSimpleResponse(List return new ResponseWrapper<>(responses); } - public static ResponseWrapper toWrappedReviewResponse(List responses) { - List reviewResponses = responses.stream() - .map(ReviewResponseAssembler::toReviewResponse) - .toList(); + public static ResponseWrapper toWrappedReviewResponse( + List responses, + List likeCounts + ) { + List reviewResponses = new ArrayList<>(); + + for (int i = 0; i < responses.size(); i++) { + ReviewResponse reviewResponse = toReviewResponse(responses.get(i), likeCounts.get(i)); + reviewResponses.add(reviewResponse); + } return new ResponseWrapper<>(reviewResponses); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java deleted file mode 100644 index 90b12e2..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/scheduler/LikeCountUpdater.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.inq.wishhair.wesharewishhair.review.application.scheduler; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Set; - -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class LikeCountUpdater { - - private static final Comparator ASC = Long::compare; - - private final ReviewRepository reviewRepository; - private final Set updateReviewSet; - - @Scheduled(fixedRate = 2 * 60 * 60 * 1000) // 2시간마다 실행 - public void syncLikeCount() { - List orderedLikeCounts = reviewRepository.countLikeReviewByIdsOrderById(updateReviewSet); - List orderedReviewIds = getOrderedReviewIds(); - - int bound = orderedReviewIds.size(); - for (int i = 0; i < bound; i++) { - int likeCount = orderedLikeCounts.get(i); - Long reviewId = orderedReviewIds.get(i); - - reviewRepository.updateLikeCountById(reviewId, likeCount); - } - - updateReviewSet.clear(); - } - - private List getOrderedReviewIds() { - List reviewIds = new ArrayList<>(updateReviewSet); - reviewIds.sort(ASC); - - return reviewIds; - } -} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java index 8434c42..dfc14f5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/config/ReviewConfig.java @@ -1,10 +1,5 @@ package com.inq.wishhair.wesharewishhair.review.config; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -18,10 +13,4 @@ public class ReviewConfig implements WebMvcConfigurer { public void addFormatters(FormatterRegistry registry) { registry.addConverter(new ScoreConverter()); } - - @Bean - public Set updateReviewMap() { - Map concurrentHashMap = new ConcurrentHashMap<>(); - return concurrentHashMap.keySet(); - } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java index 53e8672..d65f701 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReview.java @@ -4,6 +4,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,6 +13,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "reviewId"})) public class LikeReview { @Id diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java index 73eb145..3410537 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java @@ -6,9 +6,11 @@ public interface LikeReviewRepository { LikeReview save(LikeReview likeReview); + Long countByReviewId(Long reviewId); + void deleteAllByReview(Long reviewId); - void deleteByUserIdAndReviewId(Long userId, Long reviewId); + int deleteByUserIdAndReviewId(Long userId, Long reviewId); boolean existsByUserIdAndReviewId(Long userId, Long reviewId); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java index 27d04e3..dd53ba8 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java @@ -12,14 +12,16 @@ public interface LikeReviewJpaRepository extends LikeReviewRepository, JpaRepository { + @Query("select count(l.id) from LikeReview l where l.reviewId = :reviewId") + Long countByReviewId(@Param("reviewId") Long reviewId); + @Modifying @Query("delete from LikeReview l where l.reviewId = :reviewId") void deleteAllByReview(@Param("reviewId") Long reviewId); @Modifying - @Query("delete from LikeReview l " + - "where l.userId = :userId and l.reviewId = :reviewId") - void deleteByUserIdAndReviewId(@Param("userId") Long userId, + @Query("delete from LikeReview l where l.userId = :userId and l.reviewId = :reviewId") + int deleteByUserIdAndReviewId(@Param("userId") Long userId, @Param("reviewId") Long reviewId); boolean existsByUserIdAndReviewId(Long userId, Long reviewId); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java index 72f9b54..62a38dd 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java @@ -53,10 +53,12 @@ public Slice findReviewByPaging(Pageable pageable) { List result = factory .select(review) .from(review) + .leftJoin(like).on(like.reviewId.eq(review.id)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() + .groupBy(review.id) .orderBy(applyOrderBy(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1L) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 811c50e..be19839 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,12 +14,18 @@ spring: properties: hibernate: default_batch_fetch_size: 100 - show_sql: true - format_sql: true - dialect: org.hibernate.dialect.MySQL57Dialect +# show_sql: true +# format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect open-in-view: false + data: + redis: + host: localhost + port: 6379 + expire-time: 21600000 + mail: host: smtp.gmail.com port: 587 @@ -47,17 +53,18 @@ flask: # 서버 런시 발생하는 에러 로그 방지 logging: - level: - com: + com: amazonaws: util: EC2MetadataUtils: error + level: + root: warn #p6spy 설정 decorator: datasource: p6spy: - enable-logging: true + enable-logging: false #JWT key jwt: diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java new file mode 100644 index 0000000..d5bf4b2 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java @@ -0,0 +1,45 @@ +package com.inq.wishhair.wesharewishhair; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.inq.wishhair.wesharewishhair.review.application.LikeReviewService; + +@SpringBootTest +@DisplayName("[좋아요 동시성 테스트]") +class LikeTest { + + @Autowired + private LikeReviewService likeReviewService; + + @Test + @DisplayName("[100개의 동시요청에서 100개의 좋아요 개수를 기록한다]") + void test() throws InterruptedException { + //given + int threadCount = 1000; + ExecutorService service = Executors.newFixedThreadPool(50); + CountDownLatch latch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + service.execute(() -> { + Random random = new Random(); + int id = random.nextInt(Integer.MAX_VALUE); + + likeReviewService.executeLike(201L, (long)id); + latch.countDown(); + }); + } + + latch.await(); + Long likeCount = likeReviewService.getLikeCount(1L); + assertThat(likeCount).isEqualTo(102); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index cb8df18..e6ea0b4 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,7 +1,7 @@ spring: datasource: driver-class-name: org.h2.Driver - url: jdbc:h2:tcp://localhost/~/wishhair-db-test + url: jdbc:h2:mem:test_db;MODE=MySQL; username: sa password: @@ -21,6 +21,12 @@ spring: open-in-view: false + data: + redis: + host: localhost + port: 6379 + expire-time: 50000 + #메일 설정 mail: host: smtp.gmail.com @@ -60,17 +66,6 @@ jwt: access-token-validity: 1111 refresh-token-validity: 1111 -# OAuth -oauth2: - google: - grant-type: authorization_code - client-id: client-id - redirect-url: redirect-url - scope: profile, email - auth-url: https://accounts.google.com/o/oauth2/v2/auth - token-url: https://www.googleapis.com/oauth2/v4/token - user-info-url: https://www.googleapis.com/oauth2/v3/userinfo - # 네이버 클라우드 오브젝트 스토리지 cloud: aws: diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 7718ae2..d10fe77 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,84 +1,3 @@ -#spring: -# profiles: -# default: test - -server: - port: 8080 - spring: - datasource: - url: jdbc:mysql://localhost:3306/wish_hair_db - username: root - password: 1234! - driver-class-name: com.mysql.cj.jdbc.Driver - jpa: - hibernate: - ddl-auto: update - - properties: - hibernate: - default_batch_fetch_size: 100 - show_sql: true - format_sql: true - dialect: org.hibernate.dialect.MySQL57Dialect - - open-in-view: false - - mail: - host: smtp.gmail.com - port: 587 - username: ${MAIL} # namhm23@kyonggi.ac.kr - password: ${MAIL_PW} # qkvpxhpgyuywcbgh - protocol: smtp - properties: - mail: - smtp: - starttls: - enable: true - auth: true - - servlet: - multipart: - max-file-size: 10MB - -# 포인트 메일 수신자 # -mail: - point-mail-receiver: ${POINT_MAIL} #namhm23@naver.com - -# Flask domain # -flask: - domain: ${FLASK_DOMAIN} - -# 서버 런시 발생하는 에러 로그 방지 -logging: - level: - com: - amazonaws: - util: - EC2MetadataUtils: error - -#p6spy 설정 -decorator: - datasource: - p6spy: - enable-logging: true - -#JWT key -jwt: - secret-key: ${JWT_SECRET_KEY} # wishhairOiJIUzI1NiIvLoAR5cCI6IkpXSCJ9.eyJzdWIiOiIiLCLoCP1lIjoiSm9obiBEV9UiLCJpYXBCusE1MTYyMzkwMjJ9.163aevla8s7d6f987qweahqwculaoxce80k1i2o387tg - access-token-validity: ${ACCESS_TOKEN_VALIDITY} # 1800000 - refresh-token-validity: ${REFRESH_TOKEN_VALIDITY} # 259200000 - -# 네이버 클라우드 오브젝트 스토리지 -cloud: - aws: - credentials: - access-key: ${S3_ACCESS_KEY} # N3JCSJUtgse4alKEaOqX - secret-key: ${S3_SECRET_KEY} #rRamLrfUV8v6ZziJFEN4CVLjE9rl4kCBnEOLje5f - stack: - auto: false - region: - static: ap-northeast-2 - s3: - endpoint: https://kr.object.ncloudstorage.com - bucket: ${BUCKET_NAME} # wswh-storage + profiles: + default: test \ No newline at end of file From eb40aa002e3f4a7d6c2fcdcb156a19c55e1de6f7 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Fri, 3 Nov 2023 23:58:21 +0900 Subject: [PATCH 07/30] =?UTF-8?q?enhancement:=20=EC=99=B8=EB=9E=98?= =?UTF-8?q?=ED=82=A4=20=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wishhair/wesharewishhair/photo/domain/Photo.java | 6 ++++-- .../wesharewishhair/point/domain/PointLog.java | 10 +++++++++- .../wesharewishhair/point/domain/PointType.java | 7 +++++-- .../wesharewishhair/review/domain/entity/Review.java | 6 ++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java index 81165ba..9a862c9 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java @@ -4,8 +4,10 @@ import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -25,11 +27,11 @@ public class Photo { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "hair_style_id") + @JoinColumn(name = "hair_style_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private HairStyle hairStyle; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "review_id") + @JoinColumn(name = "review_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private Review review; @Column(nullable = false, updatable = false, unique = true) diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointLog.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointLog.java index 3016fc5..075ed05 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointLog.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointLog.java @@ -4,15 +4,18 @@ import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,6 +23,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "point_log") public class PointLog extends BaseEntity { @Id @@ -37,7 +41,11 @@ public class PointLog extends BaseEntity { private int point; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false, updatable = false) + @JoinColumn( + name = "user_id", + nullable = false, updatable = false, + foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT) + ) private User user; private PointLog( diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointType.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointType.java index 206c21d..708afa4 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointType.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/point/domain/PointType.java @@ -13,7 +13,7 @@ public enum PointType { CHARGE( "충전", (chargeAmount, prePoint) -> { - if (chargeAmount <= 0) { + if (chargeAmount < 0) { throw new WishHairException(POINT_INVALID_POINT_RANGE); } return prePoint + chargeAmount; @@ -22,7 +22,10 @@ public enum PointType { "사용", (useAmount, prePoint) -> { int point = prePoint - useAmount; - if (useAmount <= 0 || point < 0) { + if (useAmount < 0) { + throw new WishHairException(POINT_INVALID_POINT_RANGE); + } + if (point < 0) { throw new WishHairException(POINT_NOT_ENOUGH); } return point; diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index 5436e26..a63ae23 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -11,10 +11,12 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -35,7 +37,7 @@ public class Review extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @JoinColumn(name = "user_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private User writer; private Contents contents; @@ -48,7 +50,7 @@ public class Review extends BaseEntity { private final List photos = new ArrayList<>(); @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "hair_style_id") + @JoinColumn(name = "hair_style_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private HairStyle hairStyle; private Long likeCount; From a80fa6c2db17d07a7ed08d5ffecd1717acf39444 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Fri, 3 Nov 2023 23:59:17 +0900 Subject: [PATCH 08/30] =?UTF-8?q?test:=20pointlog=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B3=84=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point/domain/PointLogRepositoryTest.java | 36 ++++++ .../point/domain/PointLogTest.java | 122 ++++++++++++++++++ .../point/fixture/PointLogFixture.java | 32 +++++ 3 files changed, 190 insertions(+) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogRepositoryTest.java new file mode 100644 index 0000000..d0baad8 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogRepositoryTest.java @@ -0,0 +1,36 @@ +package com.inq.wishhair.wesharewishhair.point.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; +import com.inq.wishhair.wesharewishhair.point.fixture.PointLogFixture; + +@DisplayName("[PointLogRepository 테스트] - Domain") +class PointLogRepositoryTest extends RepositoryTestSupport { + + @Autowired + private PointLogRepository pointLogRepository; + + @Test + @DisplayName("[사용자 아이디로 가장 최근 PointLog 를 조회한다]") + void findByUserIdOrderByNew() { + //given + PointLog pointLog1 = pointLogRepository.save(PointLogFixture.getChargePointLog()); + PointLog pointLog2 = pointLogRepository.save(PointLogFixture.getChargePointLog()); + + //when + Slice actual = pointLogRepository.findByUserIdOrderByNew( + pointLog1.getUser().getId(), + PageRequest.of(0, 2) + ); + + //then + assertThat(actual.getContent()).contains(pointLog1, pointLog2); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogTest.java new file mode 100644 index 0000000..325033c --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/domain/PointLogTest.java @@ -0,0 +1,122 @@ +package com.inq.wishhair.wesharewishhair.point.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[PointLog 테스트 - Domain]") +class PointLogTest { + + @Nested + @DisplayName("[포인트 로그를 추가한다]") + class addPointLog { + + @Nested + @DisplayName("[충전 포인트 로그를 생성한다]") + class charge { + + @Test + @DisplayName("[성공적으로 생성한다]") + void success() { + //given + User user = UserFixture.getFixedManUser(); + int dealAmount = 1000; + int prePoint = 100; + + //when + PointLog actual = PointLog.addPointLog(user, PointType.CHARGE, dealAmount, prePoint); + + //then + assertAll( + () -> assertThat(actual.getPoint()).isEqualTo(prePoint + dealAmount), + () -> assertThat(actual.getDealAmount()).isEqualTo(dealAmount), + () -> assertThat(actual.getPointType()).isEqualTo(PointType.CHARGE) + ); + } + + @Test + @DisplayName("[충전 금액이 0이하여서 실패한다]") + void fail() { + //given + User user = UserFixture.getFixedManUser(); + int dealAmount = -100; + int prePoint = 100; + + //when + ThrowingCallable when = () -> PointLog.addPointLog(user, PointType.CHARGE, dealAmount, prePoint); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.POINT_INVALID_POINT_RANGE.getMessage()); + } + } + + @Nested + @DisplayName("[사용 포인트 로그를 생성한다]") + class use { + + @Test + @DisplayName("[성공적으로 생성한다]") + void success() { + //given + User user = UserFixture.getFixedManUser(); + int dealAmount = 1000; + int prePoint = 2000; + + //when + PointLog actual = PointLog.addPointLog(user, PointType.USE, dealAmount, prePoint); + + //then + assertAll( + () -> assertThat(actual.getPoint()).isEqualTo(prePoint - dealAmount), + () -> assertThat(actual.getDealAmount()).isEqualTo(dealAmount), + () -> assertThat(actual.getPointType()).isEqualTo(PointType.USE) + ); + } + + @Test + @DisplayName("[충전 금액이 0이하여서 실패한다]") + void failByInvalidAmountRange() { + //given + User user = UserFixture.getFixedManUser(); + int dealAmount = -100; + int prePoint = 100; + + //when + ThrowingCallable when = () -> PointLog.addPointLog(user, PointType.USE, dealAmount, prePoint); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.POINT_INVALID_POINT_RANGE.getMessage()); + } + + @Test + @DisplayName("[포인트 잔액이 부족해서 실패한다]") + void failByNotEnoughPoint() { + //given + User user = UserFixture.getFixedManUser(); + int dealAmount = 2000; + int prePoint = 1000; + + //when + ThrowingCallable when = () -> PointLog.addPointLog(user, PointType.USE, dealAmount, prePoint); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.POINT_NOT_ENOUGH.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java new file mode 100644 index 0000000..5ad30da --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java @@ -0,0 +1,32 @@ +package com.inq.wishhair.wesharewishhair.point.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.point.domain.PointLog; +import com.inq.wishhair.wesharewishhair.point.domain.PointType; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class PointLogFixture { + + private static final int DEAL_AMOUNT = 1000; + private static final int PRE_POINT = 2000; + + private static User getUser() { + User user = UserFixture.getFixedManUser(); + ReflectionTestUtils.setField(user, "id", 1L); + return user; + } + + public static PointLog getChargePointLog() { + return PointLog.addPointLog(getUser(), PointType.CHARGE, DEAL_AMOUNT, PRE_POINT); + } + + public static PointLog getUsePointLog() { + return PointLog.addPointLog(getUser(), PointType.USE, DEAL_AMOUNT, PRE_POINT); + } +} From ddf0d7fb23204772a5ff9989b571755625e41d35 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 01:48:41 +0900 Subject: [PATCH 09/30] =?UTF-8?q?test:=20pointLog=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EA=B3=84=EC=B8=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point/application/PointSearchService.java | 1 - .../point/application/PointService.java | 69 +++++++--- .../application/PointSearchServiceTest.java | 75 ++++++++++ .../point/application/PointServiceTest.java | 128 ++++++++++++++++++ .../point/fixture/PointLogFixture.java | 9 ++ .../user/fixture/UserFixture.java | 7 + 6 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchService.java index 7c50912..2b8fe29 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchService.java @@ -25,7 +25,6 @@ public PagedResponse getPointHistories( final Long userId, final Pageable pageable ) { - Slice result = pointLogRepository.findByUserIdOrderByNew(userId, pageable); return toPagedResponse(result); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointService.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointService.java index 144a338..fc7156d 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/point/application/PointService.java @@ -4,6 +4,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; import com.inq.wishhair.wesharewishhair.point.application.dto.PointUseRequest; import com.inq.wishhair.wesharewishhair.point.domain.PointLog; import com.inq.wishhair.wesharewishhair.point.domain.PointLogRepository; @@ -22,36 +24,61 @@ public class PointService { private final ApplicationEventPublisher eventPublisher; private final PointLogRepository pointLogRepository; + private void saveNewPointLog( + User user, + PointType pointType, + int dealAmount, + int prePoint + ) { + PointLog newPointLog = PointLog.addPointLog( + user, + pointType, + dealAmount, + prePoint + ); + pointLogRepository.save(newPointLog); + } + @Transactional - public void usePoint(final PointUseRequest request, final Long userId) { + public boolean usePoint(final PointUseRequest request, final Long userId) { User user = userFindService.findByUserId(userId); - insertPointHistory(PointType.USE, request.dealAmount(), user); + + pointLogRepository.findByUserOrderByCreatedDateDesc(user) + .ifPresentOrElse( + lastPointLog -> saveNewPointLog( + user, + PointType.USE, + request.dealAmount(), + lastPointLog.getPoint() + ), + () -> { + throw new WishHairException(ErrorCode.POINT_NOT_ENOUGH); + }); eventPublisher.publishEvent(request.toRefundMailEvent(user.getName())); + return true; } @Transactional - public void chargePoint(int dealAmount, Long userId) { + public boolean chargePoint(int dealAmount, Long userId) { User user = userFindService.findByUserId(userId); - insertPointHistory(PointType.CHARGE, dealAmount, user); - } - - private void insertPointHistory( - final PointType pointType, - final int dealAmount, - final User user - ) { pointLogRepository.findByUserOrderByCreatedDateDesc(user) - .ifPresent(lastPointLog -> { - PointLog newPointLog = PointLog.addPointLog( - user, - pointType, - dealAmount, - lastPointLog.getPoint() - ); - pointLogRepository.save(newPointLog); - }); + .ifPresentOrElse( + lastPointLog -> saveNewPointLog( + user, + PointType.CHARGE, + dealAmount, + lastPointLog.getPoint() + ), + () -> saveNewPointLog( + user, + PointType.CHARGE, + dealAmount, + 0 + ) + ); + + return true; } } - diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchServiceTest.java new file mode 100644 index 0000000..4da884e --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointSearchServiceTest.java @@ -0,0 +1,75 @@ +package com.inq.wishhair.wesharewishhair.point.application; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; + +import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; +import com.inq.wishhair.wesharewishhair.global.dto.response.Paging; +import com.inq.wishhair.wesharewishhair.point.application.dto.PointResponse; +import com.inq.wishhair.wesharewishhair.point.domain.PointLog; +import com.inq.wishhair.wesharewishhair.point.domain.PointLogRepository; +import com.inq.wishhair.wesharewishhair.point.fixture.PointLogFixture; + +@DisplayName("[PointSearchService 테스트] - Application") +class PointSearchServiceTest { + + private final PointSearchService pointSearchService; + private final PointLogRepository pointLogRepository; + + public PointSearchServiceTest() { + this.pointLogRepository = Mockito.mock(PointLogRepository.class); + this.pointSearchService = new PointSearchService(pointLogRepository); + } + + private void assertPointResponse(PointResponse actual, PointLog expected) { + assertAll( + () -> assertThat(actual.point()).isEqualTo(expected.getPoint()), + () -> assertThat(actual.pointType()).isEqualTo(expected.getPointType().getDescription()), + () -> assertThat(actual.dealAmount()).isEqualTo(expected.getDealAmount()) + ); + } + + @Test + @DisplayName("[사용자의 PointLog 를 조회한다]") + void getPointHistories() { + //given + Pageable pageable = PageRequest.of(0, 2); + List pointLogs = List.of(PointLogFixture.getChargePointLog(), PointLogFixture.getUsePointLog()); + SliceImpl slicePointLogs = new SliceImpl<>( + pointLogs, + pageable, + false + ); + + given(pointLogRepository.findByUserIdOrderByNew(1L, pageable)) + .willReturn(slicePointLogs); + + //when + PagedResponse actual = pointSearchService.getPointHistories(1L, pageable); + + //then + Paging paging = actual.getPaging(); + assertAll( + () -> assertThat(paging.hasNext()).isFalse(), + () -> assertThat(paging.getPage()).isZero(), + () -> assertThat(paging.getContentSize()).isEqualTo(slicePointLogs.getContent().size()) + ); + + List responses = actual.getResult(); + assertAll( + () -> assertThat(responses).hasSameSizeAs(slicePointLogs.getContent()), + () -> assertPointResponse(responses.get(0), pointLogs.get(0)), + () -> assertPointResponse(responses.get(1), pointLogs.get(1)) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java new file mode 100644 index 0000000..80c106b --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java @@ -0,0 +1,128 @@ +package com.inq.wishhair.wesharewishhair.point.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.context.ApplicationEventPublisher; + +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.point.domain.PointLog; +import com.inq.wishhair.wesharewishhair.point.domain.PointLogRepository; +import com.inq.wishhair.wesharewishhair.point.fixture.PointLogFixture; +import com.inq.wishhair.wesharewishhair.user.application.UserFindService; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.event.RefundMailSendEvent; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[PointLogService 테스트] - Application") +class PointServiceTest { + + private final PointService pointService; + private final UserFindService userFindService; + private final ApplicationEventPublisher eventPublisher; + private final PointLogRepository pointLogRepository; + + public PointServiceTest() { + this.userFindService = Mockito.mock(UserFindService.class); + this.eventPublisher = Mockito.mock(ApplicationEventPublisher.class); + this.pointLogRepository = Mockito.mock(PointLogRepository.class); + this.pointService = new PointService( + userFindService, eventPublisher, pointLogRepository + ); + } + + @Nested + @DisplayName("[포인트를 사용한다]") + class usePoint { + + @Test + @DisplayName("[성공적으로 사용한다]") + void success() { + //given + User user = UserFixture.getManUserWithId(1L); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + PointLog pointLog = PointLogFixture.getUsePointLog(user); + given(pointLogRepository.findByUserOrderByCreatedDateDesc(user)) + .willReturn(Optional.of(pointLog)); + + //when + boolean actual = pointService.usePoint(PointLogFixture.getPointUseRequest(), 1L); + + //then + assertThat(actual).isTrue(); + verify(eventPublisher, times(1)).publishEvent(any(RefundMailSendEvent.class)); + } + + @Test + @DisplayName("[이전 PointLog 가 없어서 실패한다]") + void fail() { + //given + User user = UserFixture.getManUserWithId(1L); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + given(pointLogRepository.findByUserOrderByCreatedDateDesc(user)) + .willReturn(Optional.empty()); + + //when + ThrowingCallable when = () -> pointService.usePoint(PointLogFixture.getPointUseRequest(), 1L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.POINT_NOT_ENOUGH.getMessage()); + } + } + + @Nested + @DisplayName("[포인트를 충전한다]") + class chargePoint { + + @Test + @DisplayName("[성공적으로 충전한다]") + void success1() { + //given + User user = UserFixture.getManUserWithId(1L); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + PointLog pointLog = PointLogFixture.getUsePointLog(user); + given(pointLogRepository.findByUserOrderByCreatedDateDesc(user)) + .willReturn(Optional.of(pointLog)); + + //when + boolean actual = pointService.chargePoint(1000, 1L); + + //then + assertThat(actual).isTrue(); + } + + @Test + @DisplayName("[이전 PointLog 가 없어서 0원에서 시작한 PointLog 를 만든다]") + void success2() { + //given + User user = UserFixture.getManUserWithId(1L); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + given(pointLogRepository.findByUserOrderByCreatedDateDesc(user)) + .willReturn(Optional.empty()); + + //when + boolean actual = pointService.chargePoint(1000, 1L); + + //then + assertThat(actual).isTrue(); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java index 5ad30da..0795056 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java @@ -2,6 +2,7 @@ import org.springframework.test.util.ReflectionTestUtils; +import com.inq.wishhair.wesharewishhair.point.application.dto.PointUseRequest; import com.inq.wishhair.wesharewishhair.point.domain.PointLog; import com.inq.wishhair.wesharewishhair.point.domain.PointType; import com.inq.wishhair.wesharewishhair.user.domain.entity.User; @@ -29,4 +30,12 @@ public static PointLog getChargePointLog() { public static PointLog getUsePointLog() { return PointLog.addPointLog(getUser(), PointType.USE, DEAL_AMOUNT, PRE_POINT); } + + public static PointLog getUsePointLog(User user) { + return PointLog.addPointLog(user, PointType.USE, DEAL_AMOUNT, PRE_POINT); + } + + public static PointUseRequest getPointUseRequest() { + return new PointUseRequest("bank", "1234-1234", DEAL_AMOUNT); + } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java index 07e83cf..932d7c6 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java @@ -1,6 +1,7 @@ package com.inq.wishhair.wesharewishhair.user.fixture; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; import com.inq.wishhair.wesharewishhair.common.stub.PasswordEncoderStub; import com.inq.wishhair.wesharewishhair.user.domain.entity.Password; @@ -29,6 +30,12 @@ public static User getFixedManUser() { ); } + public static User getManUserWithId(Long id) { + User user = getFixedManUser(); + ReflectionTestUtils.setField(user, "id", id); + return user; + } + public static User getFixedWomanUser() { return User.createUser( EMAIL, From f524abc732c3a32c426703ad58712329ef7db958 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 10:48:05 +0900 Subject: [PATCH 10/30] =?UTF-8?q?test:=20pointLog=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point/presentation/PointController.java | 3 +- .../wishhair/wesharewishhair/LikeTest.java | 6 +- .../auth/presentation/AuthControllerTest.java | 3 - .../presentation/MailAuthControllerTest.java | 3 - .../TokenReissueControllerTest.java | 3 - .../common/support/ApiTestSupport.java | 4 ++ .../point/fixture/PointLogFixture.java | 4 ++ .../presentation/PointControllerTest.java | 57 +++++++++++++++++++ 8 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java index 91fa489..82b86db 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java @@ -15,7 +15,7 @@ import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/api/users/point") +@RequestMapping("/api/points") @RequiredArgsConstructor public class PointController { @@ -26,7 +26,6 @@ public ResponseEntity usePoint( final @RequestBody PointUseRequest request, final @FetchAuthInfo AuthInfo authInfo ) { - pointService.usePoint(request, authInfo.userId()); return ResponseEntity.ok(new Success()); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java index d5bf4b2..0666ac7 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java @@ -14,14 +14,14 @@ import com.inq.wishhair.wesharewishhair.review.application.LikeReviewService; -@SpringBootTest +// @SpringBootTest @DisplayName("[좋아요 동시성 테스트]") class LikeTest { - @Autowired + // @Autowired private LikeReviewService likeReviewService; - @Test + // @Test @DisplayName("[100개의 동시요청에서 100개의 좋아요 개수를 기록한다]") void test() throws InterruptedException { //given diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java index 12fb1fb..0aa46ec 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -18,9 +17,7 @@ import com.inq.wishhair.wesharewishhair.auth.application.dto.response.LoginResponse; import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.LoginRequest; import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; -import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; -@WebMvcTest(value = {AuthController.class, SecurityConfig.class}) @DisplayName("[AuthController 테스트] - API") class AuthControllerTest extends ApiTestSupport { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java index 048055d..8045de2 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -16,10 +15,8 @@ import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.AuthKeyRequest; import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.MailRequest; import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; -import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; import com.inq.wishhair.wesharewishhair.user.application.utils.UserValidator; -@WebMvcTest(value = {MailAuthController.class, SecurityConfig.class}) @DisplayName("[MailAuthController 테스트] - API") class MailAuthControllerTest extends ApiTestSupport { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java index e79aead..d4d97c2 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -16,9 +15,7 @@ import com.inq.wishhair.wesharewishhair.auth.application.TokenReissueService; import com.inq.wishhair.wesharewishhair.auth.application.dto.response.TokenResponse; import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; -import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; -@WebMvcTest(value = {TokenReissueController.class, SecurityConfig.class}) @DisplayName("[TokenReissueController 테스트] - API") class TokenReissueControllerTest extends ApiTestSupport { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java index 5e350e7..6b310bd 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java @@ -4,6 +4,8 @@ import static org.mockito.BDDMockito.*; import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import com.fasterxml.jackson.core.JsonProcessingException; @@ -11,6 +13,8 @@ import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; +@SpringBootTest +@AutoConfigureMockMvc public abstract class ApiTestSupport { private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java index 0795056..e4db238 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java @@ -35,6 +35,10 @@ public static PointLog getUsePointLog(User user) { return PointLog.addPointLog(user, PointType.USE, DEAL_AMOUNT, PRE_POINT); } + public static PointLog getChargePointLog(User user) { + return PointLog.addPointLog(user, PointType.CHARGE, DEAL_AMOUNT, PRE_POINT); + } + public static PointUseRequest getPointUseRequest() { return new PointUseRequest("bank", "1234-1234", DEAL_AMOUNT); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java new file mode 100644 index 0000000..e50e10f --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java @@ -0,0 +1,57 @@ +package com.inq.wishhair.wesharewishhair.point.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.point.domain.PointLogRepository; +import com.inq.wishhair.wesharewishhair.point.fixture.PointLogFixture; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@SpringBootTest +@AutoConfigureMockMvc +@DisplayName("[PointController 테스트 - API]") +class PointControllerTest extends ApiTestSupport { + + private static final String POINT_USE_URL = "/api/points/use"; + + @Autowired + private MockMvc mockMvc; + @Autowired + private PointLogRepository pointLogRepository; + @Autowired + private UserRepository userRepository; + + @Test + @DisplayName("[포인트 사용 API 를 호출한다]") + void usePoint() throws Exception { + //given + User user = UserFixture.getFixedManUser(); + userRepository.save(user); + pointLogRepository.save(PointLogFixture.getUsePointLog(user)); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(POINT_USE_URL) + .header(AUTHORIZATION, ACCESS_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(PointLogFixture.getPointUseRequest())) + ); + + //then + result.andExpect(status().isOk()); + } +} \ No newline at end of file From 8012fe31395aff09c174c21b9bf90ddf07e1b901 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 14:46:53 +0900 Subject: [PATCH 11/30] =?UTF-8?q?test:=20photo=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=8C=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .../photo/domain/PhotoRepository.java | 7 ++ .../photo/domain/PhotoStore.java | 2 +- .../photo/infrastructure/S3PhotoStore.java | 3 +- .../review/domain/entity/Review.java | 3 +- .../common/utils/FileMockingUtils.java | 45 +++++++++ .../photo/domain/PhotoRepositoryTest.java | 63 ++++++++++++ .../photo/domain/PhotoStoreTest.java | 93 ++++++++++++++++++ src/test/resources/images/hello1.jpg | Bin 0 -> 42605 bytes src/test/resources/images/hello2.jpg | Bin 0 -> 29427 bytes src/test/resources/images/hello3.png | Bin 0 -> 53411 bytes 11 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java create mode 100644 src/test/resources/images/hello1.jpg create mode 100644 src/test/resources/images/hello2.jpg create mode 100644 src/test/resources/images/hello3.png diff --git a/.gitignore b/.gitignore index a8660af..39420aa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ build/ ### images ### **/src/main/resources/static/ -**/src/test/resources/images/ ### STS ### .apt_generated diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java index 1c25028..f2c0844 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java @@ -1,9 +1,16 @@ package com.inq.wishhair.wesharewishhair.photo.domain; import java.util.List; +import java.util.Optional; public interface PhotoRepository { + Photo save(Photo photo); + + Optional findById(Long id); + + List findAll(); + void deleteAllByReview(Long reviewId); void deleteAllByReviews(List reviewIds); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java index cd6c2fb..0987ea2 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java @@ -8,5 +8,5 @@ public interface PhotoStore { List uploadFiles(List files); - void deleteFiles(List storeUrls); + boolean deleteFiles(List storeUrls); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java index 456e15f..90166ae 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java @@ -61,8 +61,9 @@ private String uploadFile(final MultipartFile file) { } } - public void deleteFiles(final List storeUrls) { + public boolean deleteFiles(final List storeUrls) { storeUrls.forEach(this::deleteFile); + return true; } private void deleteFile(final String storeUrl) { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index a63ae23..4e3f8c8 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -23,13 +23,12 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class Review extends BaseEntity { @Id diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java new file mode 100644 index 0000000..daf94d7 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java @@ -0,0 +1,45 @@ +package com.inq.wishhair.wesharewishhair.common.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public abstract class FileMockingUtils { + + private static final String FILE_PATH = "src/test/resources/images/"; + private static final String FILE_META_NAME = "files"; + private static final String CONTENT_TYPE = "image/bmp"; + + public static MultipartFile createMockMultipartFile( + final String fileName + ) throws IOException { + try (final FileInputStream stream = new FileInputStream(FILE_PATH + fileName)) { + return new MockMultipartFile(FILE_META_NAME, fileName, CONTENT_TYPE, stream); + } + } + + public static List createMockMultipartFiles() throws IOException { + List files = new ArrayList<>(); + for (int i = 1; i <= 2; i++) { + files.add(createMockMultipartFile(String.format("hello%s.jpg", i))); + } + return files; + } + + public static MultipartFile createEmptyFile() { + return new MockMultipartFile( + "file", + "hello.png", + "image/png", + new byte[] {} + ); + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java new file mode 100644 index 0000000..8ec4253 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java @@ -0,0 +1,63 @@ +package com.inq.wishhair.wesharewishhair.photo.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@DisplayName("[PhotoRepository 테스트] - Domain") +class PhotoRepositoryTest extends RepositoryTestSupport { + + @PersistenceContext + private EntityManager entityManager; + @Autowired + private PhotoRepository photoRepository; + + @Test + @DisplayName("[리뷰 아이디를 가진 Photo 를 삭제한다]") + void deleteAllByReview() { + //given + Review review = new Review(); + ReflectionTestUtils.setField(review, "id", 1L); + Photo photo = photoRepository.save(Photo.createReviewPhoto("url", review)); + + //when + photoRepository.deleteAllByReview(1L); + entityManager.clear(); + + //then + Optional actual = photoRepository.findById(photo.getId()); + assertThat(actual).isNotPresent(); + } + + @Test + @DisplayName("[리뷰 아이디 리스트에 포함된 Photo 를 삭제한다]") + void deleteAllByReviews() { + //given + Review review1 = new Review(); + ReflectionTestUtils.setField(review1, "id", 1L); + Review review2 = new Review(); + ReflectionTestUtils.setField(review2, "id", 2L); + photoRepository.save(Photo.createReviewPhoto("url1", review1)); + photoRepository.save(Photo.createReviewPhoto("url2", review2)); + + //when + photoRepository.deleteAllByReviews(List.of(1L, 2L)); + entityManager.clear(); + + //then + List actual = photoRepository.findAll(); + assertThat(actual).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java new file mode 100644 index 0000000..82201bb --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java @@ -0,0 +1,93 @@ +package com.inq.wishhair.wesharewishhair.photo.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.photo.infrastructure.S3PhotoStore; + +@DisplayName("[PhotoStore 테스트] - Domain") +class PhotoStoreTest { + + private static final String BUCKET_NAME = "bucket"; + + private final PhotoStore photoStore; + private final AmazonS3Client amazonS3Client; + + public PhotoStoreTest() { + this.amazonS3Client = Mockito.mock(AmazonS3Client.class); + this.photoStore = new S3PhotoStore(amazonS3Client, "bucket"); + } + + @Nested + @DisplayName("[이미지를 업로드한다]") + class uploadFiles { + + @Test + @DisplayName("[성공적으로 업로드한다]") + void success() throws IOException { + //given + MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); + + URL url = URI.create("http://localhost:8080/test/url").toURL(); + given(amazonS3Client.getUrl(eq(BUCKET_NAME), anyString())) + .willReturn(url); + + //when + List actual = photoStore.uploadFiles(List.of(file)); + + //then + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isEqualTo(url.toString()); + } + + @Test + @DisplayName("[이미지 업로드에 실패한다]") + void fail() throws IOException { + //given + MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); + + given(amazonS3Client.putObject(any(PutObjectRequest.class))) + .willThrow(new WishHairException(ErrorCode.FILE_TRANSFER_EX)); + + //when + ThrowingCallable when = () -> photoStore.uploadFiles(List.of(file)); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.FILE_TRANSFER_EX.getMessage()); + } + } + + @Test + @DisplayName("[이미지를 삭제한다]") + void fail() { + //given + String url = "http://localhost:8080/" + UUID.randomUUID(); + + //when + boolean actual = photoStore.deleteFiles(List.of(url)); + + //then + assertThat(actual).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/resources/images/hello1.jpg b/src/test/resources/images/hello1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a5dc1f56f4c0fa0a2a3a416d0d13c07a2569719 GIT binary patch literal 42605 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/images/hello2.jpg b/src/test/resources/images/hello2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a9c1ab83d0d1ea7dcc18c5f669255f4809c7fc9 GIT binary patch literal 29427 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 RvzDBUI9owEm?cOS?8 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 literal 0 HcmV?d00001 From 41c1b6f76850c96cfbbbc964eb87cd11d832a97f Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 15:11:48 +0900 Subject: [PATCH 12/30] =?UTF-8?q?test:=20photo=20Application=20=EA=B2=8C?= =?UTF-8?q?=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo/application/PhotoService.java | 8 +- .../photo/application/PhotoServiceTest.java | 84 +++++++++++++++++++ .../review/fixture/ReviewFixture.java | 18 ++++ 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java index 5a8b419..dbf64bf 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java @@ -25,15 +25,19 @@ public List uploadPhotos(final List files) { } @Transactional - public void deletePhotosByReviewId(final Review review) { + public boolean deletePhotosByReviewId(final Review review) { deletePhotosInCloud(review); photoRepository.deleteAllByReview(review.getId()); + + return true; } @Transactional - public void deletePhotosByWriter(final List reviews) { + public boolean deletePhotosByWriter(final List reviews) { reviews.forEach(this::deletePhotosInCloud); photoRepository.deleteAllByReviews(reviews.stream().map(Review::getId).toList()); + + return true; } private void deletePhotosInCloud(final Review review) { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java new file mode 100644 index 0000000..c4013d0 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java @@ -0,0 +1,84 @@ +package com.inq.wishhair.wesharewishhair.photo.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.photo.domain.Photo; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoRepository; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoStore; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; + +@DisplayName("[PhotoService 테스트] - Application") +class PhotoServiceTest { + + private final PhotoService photoService; + private final PhotoStore photoStore; + + public PhotoServiceTest() { + PhotoRepository photoRepository = Mockito.mock(PhotoRepository.class); + this.photoStore = Mockito.mock(PhotoStore.class); + this.photoService = new PhotoService(photoStore, photoRepository); + } + + @Test + void uploadPhotos() throws IOException { + //given + List files = FileMockingUtils.createMockMultipartFiles(); + + List urls = List.of("test_url1", "test_url2"); + given(photoStore.uploadFiles(anyList())) + .willReturn(urls); + + //when + List actual = photoService.uploadPhotos(files); + + //then + assertThat(actual).isEqualTo(urls); + } + + @Test + void deletePhotosByReviewId() { + //given + Review review = ReviewFixture.getEmptyReview(1L); + Photo photo = Photo.createReviewPhoto("url1", review); + + ReflectionTestUtils.setField(review, "photos", List.of(photo)); + + //when + boolean actual = photoService.deletePhotosByReviewId(review); + + //then + assertThat(actual).isTrue(); + verify(photoStore, times(1)).deleteFiles(anyList()); + } + + @Test + void deletePhotosByWriter() { + //given + Review review1 = ReviewFixture.getEmptyReview(1L); + Review review2 = ReviewFixture.getEmptyReview(2L); + Photo photo1 = Photo.createReviewPhoto("url1", review1); + Photo photo2 = Photo.createReviewPhoto("url1", review2); + + ReflectionTestUtils.setField(review1, "photos", List.of(photo1)); + ReflectionTestUtils.setField(review2, "photos", List.of(photo2)); + + //when + boolean actual = photoService.deletePhotosByWriter(List.of(review1, review2)); + + //then + assertThat(actual).isTrue(); + verify(photoStore, times(2)).deleteFiles(anyList()); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java new file mode 100644 index 0000000..434ee5f --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -0,0 +1,18 @@ +package com.inq.wishhair.wesharewishhair.review.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ReviewFixture { + + public static Review getEmptyReview(Long id) { + Review review = new Review(); + ReflectionTestUtils.setField(review, "id", id); + return review; + } +} From 37f2a9cfc6561b52339f73c9f5dc3997016683b0 Mon Sep 17 00:00:00 2001 From: EunChanNam <75837025+EunChanNam@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:21:34 +0900 Subject: [PATCH 13/30] =?UTF-8?q?Revert=20"test:=20photo=20Application=20?= =?UTF-8?q?=EA=B2=8C=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 41c1b6f76850c96cfbbbc964eb87cd11d832a97f. --- .../photo/application/PhotoService.java | 8 +- .../photo/application/PhotoServiceTest.java | 84 ------------------- .../review/fixture/ReviewFixture.java | 18 ---- 3 files changed, 2 insertions(+), 108 deletions(-) delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java index dbf64bf..5a8b419 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java @@ -25,19 +25,15 @@ public List uploadPhotos(final List files) { } @Transactional - public boolean deletePhotosByReviewId(final Review review) { + public void deletePhotosByReviewId(final Review review) { deletePhotosInCloud(review); photoRepository.deleteAllByReview(review.getId()); - - return true; } @Transactional - public boolean deletePhotosByWriter(final List reviews) { + public void deletePhotosByWriter(final List reviews) { reviews.forEach(this::deletePhotosInCloud); photoRepository.deleteAllByReviews(reviews.stream().map(Review::getId).toList()); - - return true; } private void deletePhotosInCloud(final Review review) { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java deleted file mode 100644 index c4013d0..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.inq.wishhair.wesharewishhair.photo.application; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -import java.io.IOException; -import java.util.List; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.multipart.MultipartFile; - -import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; -import com.inq.wishhair.wesharewishhair.photo.domain.Photo; -import com.inq.wishhair.wesharewishhair.photo.domain.PhotoRepository; -import com.inq.wishhair.wesharewishhair.photo.domain.PhotoStore; -import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; -import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; - -@DisplayName("[PhotoService 테스트] - Application") -class PhotoServiceTest { - - private final PhotoService photoService; - private final PhotoStore photoStore; - - public PhotoServiceTest() { - PhotoRepository photoRepository = Mockito.mock(PhotoRepository.class); - this.photoStore = Mockito.mock(PhotoStore.class); - this.photoService = new PhotoService(photoStore, photoRepository); - } - - @Test - void uploadPhotos() throws IOException { - //given - List files = FileMockingUtils.createMockMultipartFiles(); - - List urls = List.of("test_url1", "test_url2"); - given(photoStore.uploadFiles(anyList())) - .willReturn(urls); - - //when - List actual = photoService.uploadPhotos(files); - - //then - assertThat(actual).isEqualTo(urls); - } - - @Test - void deletePhotosByReviewId() { - //given - Review review = ReviewFixture.getEmptyReview(1L); - Photo photo = Photo.createReviewPhoto("url1", review); - - ReflectionTestUtils.setField(review, "photos", List.of(photo)); - - //when - boolean actual = photoService.deletePhotosByReviewId(review); - - //then - assertThat(actual).isTrue(); - verify(photoStore, times(1)).deleteFiles(anyList()); - } - - @Test - void deletePhotosByWriter() { - //given - Review review1 = ReviewFixture.getEmptyReview(1L); - Review review2 = ReviewFixture.getEmptyReview(2L); - Photo photo1 = Photo.createReviewPhoto("url1", review1); - Photo photo2 = Photo.createReviewPhoto("url1", review2); - - ReflectionTestUtils.setField(review1, "photos", List.of(photo1)); - ReflectionTestUtils.setField(review2, "photos", List.of(photo2)); - - //when - boolean actual = photoService.deletePhotosByWriter(List.of(review1, review2)); - - //then - assertThat(actual).isTrue(); - verify(photoStore, times(2)).deleteFiles(anyList()); - } -} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java deleted file mode 100644 index 434ee5f..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.inq.wishhair.wesharewishhair.review.fixture; - -import org.springframework.test.util.ReflectionTestUtils; - -import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class ReviewFixture { - - public static Review getEmptyReview(Long id) { - Review review = new Review(); - ReflectionTestUtils.setField(review, "id", id); - return review; - } -} From d289894d32113a1c93f087f5a3696d8e7bf6fcd4 Mon Sep 17 00:00:00 2001 From: EunChanNam <75837025+EunChanNam@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:21:34 +0900 Subject: [PATCH 14/30] =?UTF-8?q?Revert=20"test:=20photo=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B2=8C=EC=B8=B5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8012fe31395aff09c174c21b9bf90ddf07e1b901. --- .gitignore | 1 + .../photo/domain/PhotoRepository.java | 7 -- .../photo/domain/PhotoStore.java | 2 +- .../photo/infrastructure/S3PhotoStore.java | 3 +- .../review/domain/entity/Review.java | 3 +- .../common/utils/FileMockingUtils.java | 45 --------- .../photo/domain/PhotoRepositoryTest.java | 63 ------------ .../photo/domain/PhotoStoreTest.java | 93 ------------------ src/test/resources/images/hello1.jpg | Bin 42605 -> 0 bytes src/test/resources/images/hello2.jpg | Bin 29427 -> 0 bytes src/test/resources/images/hello3.png | Bin 53411 -> 0 bytes 11 files changed, 5 insertions(+), 212 deletions(-) delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java delete mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java delete mode 100644 src/test/resources/images/hello1.jpg delete mode 100644 src/test/resources/images/hello2.jpg delete mode 100644 src/test/resources/images/hello3.png diff --git a/.gitignore b/.gitignore index 39420aa..a8660af 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build/ ### images ### **/src/main/resources/static/ +**/src/test/resources/images/ ### STS ### .apt_generated diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java index f2c0844..1c25028 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java @@ -1,16 +1,9 @@ package com.inq.wishhair.wesharewishhair.photo.domain; import java.util.List; -import java.util.Optional; public interface PhotoRepository { - Photo save(Photo photo); - - Optional findById(Long id); - - List findAll(); - void deleteAllByReview(Long reviewId); void deleteAllByReviews(List reviewIds); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java index 0987ea2..cd6c2fb 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java @@ -8,5 +8,5 @@ public interface PhotoStore { List uploadFiles(List files); - boolean deleteFiles(List storeUrls); + void deleteFiles(List storeUrls); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java index 90166ae..456e15f 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java @@ -61,9 +61,8 @@ private String uploadFile(final MultipartFile file) { } } - public boolean deleteFiles(final List storeUrls) { + public void deleteFiles(final List storeUrls) { storeUrls.forEach(this::deleteFile); - return true; } private void deleteFile(final String storeUrl) { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index 4e3f8c8..a63ae23 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -23,12 +23,13 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Review extends BaseEntity { @Id diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java deleted file mode 100644 index daf94d7..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.inq.wishhair.wesharewishhair.common.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public abstract class FileMockingUtils { - - private static final String FILE_PATH = "src/test/resources/images/"; - private static final String FILE_META_NAME = "files"; - private static final String CONTENT_TYPE = "image/bmp"; - - public static MultipartFile createMockMultipartFile( - final String fileName - ) throws IOException { - try (final FileInputStream stream = new FileInputStream(FILE_PATH + fileName)) { - return new MockMultipartFile(FILE_META_NAME, fileName, CONTENT_TYPE, stream); - } - } - - public static List createMockMultipartFiles() throws IOException { - List files = new ArrayList<>(); - for (int i = 1; i <= 2; i++) { - files.add(createMockMultipartFile(String.format("hello%s.jpg", i))); - } - return files; - } - - public static MultipartFile createEmptyFile() { - return new MockMultipartFile( - "file", - "hello.png", - "image/png", - new byte[] {} - ); - } -} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java deleted file mode 100644 index 8ec4253..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.inq.wishhair.wesharewishhair.photo.domain; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.util.ReflectionTestUtils; - -import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; -import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; - -@DisplayName("[PhotoRepository 테스트] - Domain") -class PhotoRepositoryTest extends RepositoryTestSupport { - - @PersistenceContext - private EntityManager entityManager; - @Autowired - private PhotoRepository photoRepository; - - @Test - @DisplayName("[리뷰 아이디를 가진 Photo 를 삭제한다]") - void deleteAllByReview() { - //given - Review review = new Review(); - ReflectionTestUtils.setField(review, "id", 1L); - Photo photo = photoRepository.save(Photo.createReviewPhoto("url", review)); - - //when - photoRepository.deleteAllByReview(1L); - entityManager.clear(); - - //then - Optional actual = photoRepository.findById(photo.getId()); - assertThat(actual).isNotPresent(); - } - - @Test - @DisplayName("[리뷰 아이디 리스트에 포함된 Photo 를 삭제한다]") - void deleteAllByReviews() { - //given - Review review1 = new Review(); - ReflectionTestUtils.setField(review1, "id", 1L); - Review review2 = new Review(); - ReflectionTestUtils.setField(review2, "id", 2L); - photoRepository.save(Photo.createReviewPhoto("url1", review1)); - photoRepository.save(Photo.createReviewPhoto("url2", review2)); - - //when - photoRepository.deleteAllByReviews(List.of(1L, 2L)); - entityManager.clear(); - - //then - List actual = photoRepository.findAll(); - assertThat(actual).isEmpty(); - } -} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java deleted file mode 100644 index 82201bb..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.inq.wishhair.wesharewishhair.photo.domain; - -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.ThrowableAssert.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.BDDMockito.*; - -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.util.List; -import java.util.UUID; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.web.multipart.MultipartFile; - -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; -import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; -import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; -import com.inq.wishhair.wesharewishhair.photo.infrastructure.S3PhotoStore; - -@DisplayName("[PhotoStore 테스트] - Domain") -class PhotoStoreTest { - - private static final String BUCKET_NAME = "bucket"; - - private final PhotoStore photoStore; - private final AmazonS3Client amazonS3Client; - - public PhotoStoreTest() { - this.amazonS3Client = Mockito.mock(AmazonS3Client.class); - this.photoStore = new S3PhotoStore(amazonS3Client, "bucket"); - } - - @Nested - @DisplayName("[이미지를 업로드한다]") - class uploadFiles { - - @Test - @DisplayName("[성공적으로 업로드한다]") - void success() throws IOException { - //given - MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); - - URL url = URI.create("http://localhost:8080/test/url").toURL(); - given(amazonS3Client.getUrl(eq(BUCKET_NAME), anyString())) - .willReturn(url); - - //when - List actual = photoStore.uploadFiles(List.of(file)); - - //then - assertThat(actual).hasSize(1); - assertThat(actual.get(0)).isEqualTo(url.toString()); - } - - @Test - @DisplayName("[이미지 업로드에 실패한다]") - void fail() throws IOException { - //given - MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); - - given(amazonS3Client.putObject(any(PutObjectRequest.class))) - .willThrow(new WishHairException(ErrorCode.FILE_TRANSFER_EX)); - - //when - ThrowingCallable when = () -> photoStore.uploadFiles(List.of(file)); - - //then - assertThatThrownBy(when) - .isInstanceOf(WishHairException.class) - .hasMessageContaining(ErrorCode.FILE_TRANSFER_EX.getMessage()); - } - } - - @Test - @DisplayName("[이미지를 삭제한다]") - void fail() { - //given - String url = "http://localhost:8080/" + UUID.randomUUID(); - - //when - boolean actual = photoStore.deleteFiles(List.of(url)); - - //then - assertThat(actual).isTrue(); - } -} \ No newline at end of file diff --git a/src/test/resources/images/hello1.jpg b/src/test/resources/images/hello1.jpg deleted file mode 100644 index 4a5dc1f56f4c0fa0a2a3a416d0d13c07a2569719..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42605 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 diff --git a/src/test/resources/images/hello2.jpg b/src/test/resources/images/hello2.jpg deleted file mode 100644 index 8a9c1ab83d0d1ea7dcc18c5f669255f4809c7fc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29427 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 RvzDBUI9owEm?cOS?8 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 From 68f971952baba946c72da8150eb8ca3f410b1377 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 14:46:53 +0900 Subject: [PATCH 15/30] =?UTF-8?q?test:=20photo=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=8C=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .../photo/domain/PhotoRepository.java | 7 ++ .../photo/domain/PhotoStore.java | 2 +- .../photo/infrastructure/S3PhotoStore.java | 3 +- .../review/domain/entity/Review.java | 3 +- .../common/utils/FileMockingUtils.java | 45 +++++++++ .../photo/domain/PhotoRepositoryTest.java | 63 ++++++++++++ .../photo/domain/PhotoStoreTest.java | 93 ++++++++++++++++++ src/test/resources/images/hello1.jpg | Bin 0 -> 42605 bytes src/test/resources/images/hello2.jpg | Bin 0 -> 29427 bytes src/test/resources/images/hello3.png | Bin 0 -> 53411 bytes 11 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java create mode 100644 src/test/resources/images/hello1.jpg create mode 100644 src/test/resources/images/hello2.jpg create mode 100644 src/test/resources/images/hello3.png diff --git a/.gitignore b/.gitignore index a8660af..39420aa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ build/ ### images ### **/src/main/resources/static/ -**/src/test/resources/images/ ### STS ### .apt_generated diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java index 1c25028..f2c0844 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepository.java @@ -1,9 +1,16 @@ package com.inq.wishhair.wesharewishhair.photo.domain; import java.util.List; +import java.util.Optional; public interface PhotoRepository { + Photo save(Photo photo); + + Optional findById(Long id); + + List findAll(); + void deleteAllByReview(Long reviewId); void deleteAllByReviews(List reviewIds); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java index cd6c2fb..0987ea2 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStore.java @@ -8,5 +8,5 @@ public interface PhotoStore { List uploadFiles(List files); - void deleteFiles(List storeUrls); + boolean deleteFiles(List storeUrls); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java index 456e15f..90166ae 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/infrastructure/S3PhotoStore.java @@ -61,8 +61,9 @@ private String uploadFile(final MultipartFile file) { } } - public void deleteFiles(final List storeUrls) { + public boolean deleteFiles(final List storeUrls) { storeUrls.forEach(this::deleteFile); + return true; } private void deleteFile(final String storeUrl) { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index a63ae23..4e3f8c8 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -23,13 +23,12 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class Review extends BaseEntity { @Id diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java new file mode 100644 index 0000000..daf94d7 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java @@ -0,0 +1,45 @@ +package com.inq.wishhair.wesharewishhair.common.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public abstract class FileMockingUtils { + + private static final String FILE_PATH = "src/test/resources/images/"; + private static final String FILE_META_NAME = "files"; + private static final String CONTENT_TYPE = "image/bmp"; + + public static MultipartFile createMockMultipartFile( + final String fileName + ) throws IOException { + try (final FileInputStream stream = new FileInputStream(FILE_PATH + fileName)) { + return new MockMultipartFile(FILE_META_NAME, fileName, CONTENT_TYPE, stream); + } + } + + public static List createMockMultipartFiles() throws IOException { + List files = new ArrayList<>(); + for (int i = 1; i <= 2; i++) { + files.add(createMockMultipartFile(String.format("hello%s.jpg", i))); + } + return files; + } + + public static MultipartFile createEmptyFile() { + return new MockMultipartFile( + "file", + "hello.png", + "image/png", + new byte[] {} + ); + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java new file mode 100644 index 0000000..8ec4253 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoRepositoryTest.java @@ -0,0 +1,63 @@ +package com.inq.wishhair.wesharewishhair.photo.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@DisplayName("[PhotoRepository 테스트] - Domain") +class PhotoRepositoryTest extends RepositoryTestSupport { + + @PersistenceContext + private EntityManager entityManager; + @Autowired + private PhotoRepository photoRepository; + + @Test + @DisplayName("[리뷰 아이디를 가진 Photo 를 삭제한다]") + void deleteAllByReview() { + //given + Review review = new Review(); + ReflectionTestUtils.setField(review, "id", 1L); + Photo photo = photoRepository.save(Photo.createReviewPhoto("url", review)); + + //when + photoRepository.deleteAllByReview(1L); + entityManager.clear(); + + //then + Optional actual = photoRepository.findById(photo.getId()); + assertThat(actual).isNotPresent(); + } + + @Test + @DisplayName("[리뷰 아이디 리스트에 포함된 Photo 를 삭제한다]") + void deleteAllByReviews() { + //given + Review review1 = new Review(); + ReflectionTestUtils.setField(review1, "id", 1L); + Review review2 = new Review(); + ReflectionTestUtils.setField(review2, "id", 2L); + photoRepository.save(Photo.createReviewPhoto("url1", review1)); + photoRepository.save(Photo.createReviewPhoto("url2", review2)); + + //when + photoRepository.deleteAllByReviews(List.of(1L, 2L)); + entityManager.clear(); + + //then + List actual = photoRepository.findAll(); + assertThat(actual).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java new file mode 100644 index 0000000..82201bb --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/domain/PhotoStoreTest.java @@ -0,0 +1,93 @@ +package com.inq.wishhair.wesharewishhair.photo.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.photo.infrastructure.S3PhotoStore; + +@DisplayName("[PhotoStore 테스트] - Domain") +class PhotoStoreTest { + + private static final String BUCKET_NAME = "bucket"; + + private final PhotoStore photoStore; + private final AmazonS3Client amazonS3Client; + + public PhotoStoreTest() { + this.amazonS3Client = Mockito.mock(AmazonS3Client.class); + this.photoStore = new S3PhotoStore(amazonS3Client, "bucket"); + } + + @Nested + @DisplayName("[이미지를 업로드한다]") + class uploadFiles { + + @Test + @DisplayName("[성공적으로 업로드한다]") + void success() throws IOException { + //given + MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); + + URL url = URI.create("http://localhost:8080/test/url").toURL(); + given(amazonS3Client.getUrl(eq(BUCKET_NAME), anyString())) + .willReturn(url); + + //when + List actual = photoStore.uploadFiles(List.of(file)); + + //then + assertThat(actual).hasSize(1); + assertThat(actual.get(0)).isEqualTo(url.toString()); + } + + @Test + @DisplayName("[이미지 업로드에 실패한다]") + void fail() throws IOException { + //given + MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); + + given(amazonS3Client.putObject(any(PutObjectRequest.class))) + .willThrow(new WishHairException(ErrorCode.FILE_TRANSFER_EX)); + + //when + ThrowingCallable when = () -> photoStore.uploadFiles(List.of(file)); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.FILE_TRANSFER_EX.getMessage()); + } + } + + @Test + @DisplayName("[이미지를 삭제한다]") + void fail() { + //given + String url = "http://localhost:8080/" + UUID.randomUUID(); + + //when + boolean actual = photoStore.deleteFiles(List.of(url)); + + //then + assertThat(actual).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/resources/images/hello1.jpg b/src/test/resources/images/hello1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a5dc1f56f4c0fa0a2a3a416d0d13c07a2569719 GIT binary patch literal 42605 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/images/hello2.jpg b/src/test/resources/images/hello2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a9c1ab83d0d1ea7dcc18c5f669255f4809c7fc9 GIT binary patch literal 29427 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 RvzDBUI9owEm?cOS?8 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 literal 0 HcmV?d00001 From 7f6175583be946d3d4079b56893df6df8d7c256f Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 15:11:48 +0900 Subject: [PATCH 16/30] =?UTF-8?q?test:=20photo=20Application=20=EA=B2=8C?= =?UTF-8?q?=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo/application/PhotoService.java | 8 +- .../photo/application/PhotoServiceTest.java | 84 +++++++++++++++++++ .../review/fixture/ReviewFixture.java | 18 ++++ 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java index 5a8b419..dbf64bf 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoService.java @@ -25,15 +25,19 @@ public List uploadPhotos(final List files) { } @Transactional - public void deletePhotosByReviewId(final Review review) { + public boolean deletePhotosByReviewId(final Review review) { deletePhotosInCloud(review); photoRepository.deleteAllByReview(review.getId()); + + return true; } @Transactional - public void deletePhotosByWriter(final List reviews) { + public boolean deletePhotosByWriter(final List reviews) { reviews.forEach(this::deletePhotosInCloud); photoRepository.deleteAllByReviews(reviews.stream().map(Review::getId).toList()); + + return true; } private void deletePhotosInCloud(final Review review) { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java new file mode 100644 index 0000000..c4013d0 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java @@ -0,0 +1,84 @@ +package com.inq.wishhair.wesharewishhair.photo.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.photo.domain.Photo; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoRepository; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoStore; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; + +@DisplayName("[PhotoService 테스트] - Application") +class PhotoServiceTest { + + private final PhotoService photoService; + private final PhotoStore photoStore; + + public PhotoServiceTest() { + PhotoRepository photoRepository = Mockito.mock(PhotoRepository.class); + this.photoStore = Mockito.mock(PhotoStore.class); + this.photoService = new PhotoService(photoStore, photoRepository); + } + + @Test + void uploadPhotos() throws IOException { + //given + List files = FileMockingUtils.createMockMultipartFiles(); + + List urls = List.of("test_url1", "test_url2"); + given(photoStore.uploadFiles(anyList())) + .willReturn(urls); + + //when + List actual = photoService.uploadPhotos(files); + + //then + assertThat(actual).isEqualTo(urls); + } + + @Test + void deletePhotosByReviewId() { + //given + Review review = ReviewFixture.getEmptyReview(1L); + Photo photo = Photo.createReviewPhoto("url1", review); + + ReflectionTestUtils.setField(review, "photos", List.of(photo)); + + //when + boolean actual = photoService.deletePhotosByReviewId(review); + + //then + assertThat(actual).isTrue(); + verify(photoStore, times(1)).deleteFiles(anyList()); + } + + @Test + void deletePhotosByWriter() { + //given + Review review1 = ReviewFixture.getEmptyReview(1L); + Review review2 = ReviewFixture.getEmptyReview(2L); + Photo photo1 = Photo.createReviewPhoto("url1", review1); + Photo photo2 = Photo.createReviewPhoto("url1", review2); + + ReflectionTestUtils.setField(review1, "photos", List.of(photo1)); + ReflectionTestUtils.setField(review2, "photos", List.of(photo2)); + + //when + boolean actual = photoService.deletePhotosByWriter(List.of(review1, review2)); + + //then + assertThat(actual).isTrue(); + verify(photoStore, times(2)).deleteFiles(anyList()); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java new file mode 100644 index 0000000..434ee5f --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -0,0 +1,18 @@ +package com.inq.wishhair.wesharewishhair.review.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ReviewFixture { + + public static Review getEmptyReview(Long id) { + Review review = new Review(); + ReflectionTestUtils.setField(review, "id", id); + return review; + } +} From ac499429852b1080915779ba3c1ab678ee4d2362 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 15:27:02 +0900 Subject: [PATCH 17/30] =?UTF-8?q?chore:=20=EC=9E=84=EB=B2=A0=EB=94=94?= =?UTF-8?q?=EB=93=9C=20=EB=A0=88=EB=94=94=EC=8A=A4(=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=9A=A9)=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 --- build.gradle | 4 +++- src/test/resources/application-test.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1c11ca6..60d228a 100644 --- a/build.gradle +++ b/build.gradle @@ -70,8 +70,10 @@ dependencies { //DB runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' + + //Redis + implementation('it.ozimov:embedded-redis:0.7.3') implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.redisson:redisson-spring-boot-starter:3.24.3' //네이버 클라우드 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index e6ea0b4..2ad932c 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -7,7 +7,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: create properties: jakarta: From 988b26ad9b62dc0fc1f4b96e883066d28dd47529 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 15:33:31 +0900 Subject: [PATCH 18/30] =?UTF-8?q?chore:=20=EC=9E=84=EB=B2=A0=EB=94=94?= =?UTF-8?q?=EB=93=9C=20=EB=A0=88=EB=94=94=EC=8A=A4=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 60d228a..af7c396 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ dependencies { runtimeOnly 'com.h2database:h2' //Redis - implementation('it.ozimov:embedded-redis:0.7.3') + implementation('it.ozimov:embedded-redis:0.7.2') implementation 'org.springframework.boot:spring-boot-starter-data-redis' //네이버 클라우드 From bc700263a7a39692950ff123c8d42ba34f3ed249 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 4 Nov 2023 16:22:46 +0900 Subject: [PATCH 19/30] =?UTF-8?q?test:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point/presentation/PointController.java | 17 ++++++++++ .../presentation/PointSearchController.java | 33 ------------------- .../photo/application/PhotoServiceTest.java | 3 ++ .../presentation/PointControllerTest.java | 31 ++++++++++++++--- 4 files changed, 47 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointSearchController.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java index 82b86db..f318648 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java @@ -1,14 +1,20 @@ package com.inq.wishhair.wesharewishhair.point.presentation; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.inq.wishhair.wesharewishhair.global.annotation.FetchAuthInfo; +import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; import com.inq.wishhair.wesharewishhair.global.dto.response.Success; import com.inq.wishhair.wesharewishhair.global.resolver.dto.AuthInfo; +import com.inq.wishhair.wesharewishhair.point.application.PointSearchService; +import com.inq.wishhair.wesharewishhair.point.application.dto.PointResponse; import com.inq.wishhair.wesharewishhair.point.application.dto.PointUseRequest; import com.inq.wishhair.wesharewishhair.point.application.PointService; @@ -30,4 +36,15 @@ public ResponseEntity usePoint( return ResponseEntity.ok(new Success()); } + + private final PointSearchService pointSearchService; + + @GetMapping + public ResponseEntity> findPointHistories( + final @FetchAuthInfo AuthInfo authInfo, + final @PageableDefault Pageable pageable + ) { + + return ResponseEntity.ok(pointSearchService.getPointHistories(authInfo.userId(), pageable)); + } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointSearchController.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointSearchController.java deleted file mode 100644 index a271e49..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointSearchController.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.inq.wishhair.wesharewishhair.point.presentation; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.inq.wishhair.wesharewishhair.global.annotation.FetchAuthInfo; -import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; -import com.inq.wishhair.wesharewishhair.global.resolver.dto.AuthInfo; -import com.inq.wishhair.wesharewishhair.point.application.PointSearchService; -import com.inq.wishhair.wesharewishhair.point.application.dto.PointResponse; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequestMapping("/api/users/point") -@RequiredArgsConstructor -public class PointSearchController { - - private final PointSearchService pointSearchService; - - @GetMapping - public ResponseEntity> findPointHistories( - final @FetchAuthInfo AuthInfo authInfo, - final @PageableDefault Pageable pageable - ) { - - return ResponseEntity.ok(pointSearchService.getPointHistories(authInfo.userId(), pageable)); - } -} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java index c4013d0..00f1fbc 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java @@ -32,6 +32,7 @@ public PhotoServiceTest() { } @Test + @DisplayName("[이미지들을 업로드한다]") void uploadPhotos() throws IOException { //given List files = FileMockingUtils.createMockMultipartFiles(); @@ -48,6 +49,7 @@ void uploadPhotos() throws IOException { } @Test + @DisplayName("[Photo 데이터와 실제 이미지를 리뷰 아이디로 삭제한다]") void deletePhotosByReviewId() { //given Review review = ReviewFixture.getEmptyReview(1L); @@ -64,6 +66,7 @@ void deletePhotosByReviewId() { } @Test + @DisplayName("[특정 리뷰어의 리뷰의 Photo 데이터와 실제 이미지를 삭제한다]") void deletePhotosByWriter() { //given Review review1 = ReviewFixture.getEmptyReview(1L); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java index e50e10f..256d398 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java @@ -6,8 +6,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -20,12 +18,11 @@ import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; -@SpringBootTest -@AutoConfigureMockMvc @DisplayName("[PointController 테스트 - API]") class PointControllerTest extends ApiTestSupport { private static final String POINT_USE_URL = "/api/points/use"; + private static final String POINT_QUERY_URL = "/api/points"; @Autowired private MockMvc mockMvc; @@ -54,4 +51,30 @@ void usePoint() throws Exception { //then result.andExpect(status().isOk()); } + + @Test + @DisplayName("[포인트 로그를 페이징 정보를 통해 조회한다]") + void findPointHistories() throws Exception { + //given + User user = UserFixture.getFixedManUser(); + Long userId = userRepository.save(user).getId(); + pointLogRepository.save(PointLogFixture.getUsePointLog(user)); + + setAuthorization(userId); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .get(POINT_QUERY_URL) + .header(AUTHORIZATION, ACCESS_TOKEN) + ); + + //then + result.andExpectAll( + status().isOk(), + jsonPath("$.paging").exists(), + jsonPath("$.result").isNotEmpty(), + jsonPath("$.result.size()").value(1) + ); + } } \ No newline at end of file From f6291c005c6963cd6e822c6956a0f456d1381e30 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sun, 5 Nov 2023 19:13:23 +0900 Subject: [PATCH 20/30] =?UTF-8?q?test:=20hairstyle=20Domain=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +- .../global/exception/ApiExceptionHandler.java | 3 +- .../application/HairStyleSearchService.java | 2 +- .../HairStyleQueryRepository.java | 3 +- .../query/HairStyleQueryDslRepository.java | 5 +- .../utils/HairRecommendCondition.java | 4 +- .../wesharewishhair/photo/domain/Photo.java | 2 +- .../common/support/ApiTestSupport.java | 7 + .../common/support/RepositoryTestSupport.java | 3 +- .../domain/HairStyleQueryRepositoryTest.java | 169 ++++++++++++++++++ .../hairstyle/domain/HairStyleTest.java | 53 ++++++ .../hairstyle/fixture/HairStyleFixture.java | 45 +++++ src/test/resources/images/hello3.jpg | Bin 0 -> 53411 bytes 13 files changed, 286 insertions(+), 13 deletions(-) rename src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/{application/query => domain}/HairStyleQueryRepository.java (77%) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java create mode 100644 src/test/resources/images/hello3.jpg diff --git a/build.gradle b/build.gradle index af7c396..f45120c 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,8 @@ jacocoTestReport { '**/*Application*', "**/config/**", "**/exception/**", - "**/dto/**" + "**/dto/**", + "**/Q*/**" ]) }) ) diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ApiExceptionHandler.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ApiExceptionHandler.java index b535d1d..82c0d86 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ApiExceptionHandler.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/exception/ApiExceptionHandler.java @@ -17,7 +17,6 @@ import com.inq.wishhair.wesharewishhair.review.presentation.ReviewController; import com.inq.wishhair.wesharewishhair.review.presentation.ReviewSearchController; import com.inq.wishhair.wesharewishhair.point.presentation.PointController; -import com.inq.wishhair.wesharewishhair.point.presentation.PointSearchController; import com.inq.wishhair.wesharewishhair.user.presentation.UserController; import com.inq.wishhair.wesharewishhair.user.presentation.UserInfoController; @@ -26,7 +25,7 @@ ReviewController.class, WishHairController.class, AuthController.class, TokenReissueController.class, MailAuthController.class, UserInfoController.class, LikeReviewController.class, ReviewSearchController.class, - PointSearchController.class, PointController.class + PointController.class }) public class ApiExceptionHandler { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java index c87a0b0..43cb4c1 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java @@ -15,7 +15,7 @@ import com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; -import com.inq.wishhair.wesharewishhair.hairstyle.application.query.HairStyleQueryRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleQueryRepository; import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/query/HairStyleQueryRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepository.java similarity index 77% rename from src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/query/HairStyleQueryRepository.java rename to src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepository.java index 5d95328..153ec75 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/query/HairStyleQueryRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepository.java @@ -1,11 +1,10 @@ -package com.inq.wishhair.wesharewishhair.hairstyle.application.query; +package com.inq.wishhair.wesharewishhair.hairstyle.domain; import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.utils.HairRecommendCondition; public interface HairStyleQueryRepository { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java index d8503ab..484e973 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/query/HairStyleQueryDslRepository.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import com.inq.wishhair.wesharewishhair.hairstyle.application.query.HairStyleQueryRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleQueryRepository; import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.domain.QHairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.QHashTag; @@ -59,7 +59,8 @@ public List findByRecommend( .where( hashTagInTags(condition.getTags()), hairStyleIn(filteredHairStyles), - hairStyle.sex.eq(condition.getSex())) + hairStyle.sex.eq(condition.getSex()) + ) .groupBy(hairStyle.id) .orderBy(mainOrderBy()) .fetch(); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/utils/HairRecommendCondition.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/utils/HairRecommendCondition.java index 31c13d5..2eb0305 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/utils/HairRecommendCondition.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/utils/HairRecommendCondition.java @@ -1,7 +1,5 @@ package com.inq.wishhair.wesharewishhair.hairstyle.utils; -import static lombok.AccessLevel.*; - import java.util.List; import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; @@ -12,7 +10,7 @@ import lombok.Getter; @Getter -@AllArgsConstructor(access = PRIVATE) +@AllArgsConstructor public class HairRecommendCondition { private List tags; diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java index 9a862c9..12aed65 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java @@ -34,7 +34,7 @@ public class Photo { @JoinColumn(name = "review_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private Review review; - @Column(nullable = false, updatable = false, unique = true) + @Column(nullable = false, updatable = false) private String storeUrl; //==생성 메서드==// diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java index 6b310bd..ae2ab27 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java @@ -7,6 +7,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -15,6 +16,7 @@ @SpringBootTest @AutoConfigureMockMvc +@Transactional public abstract class ApiTestSupport { private final ObjectMapper objectMapper = new ObjectMapper(); @@ -28,6 +30,11 @@ public void setAuthorization() { given(authTokenManager.getId(anyString())).willReturn(1L); } + protected void setAuthorization(Long userId) { + given(authTokenManager.generate(any(Long.class))).willReturn(new AuthToken(TOKEN, TOKEN)); + given(authTokenManager.getId(anyString())).willReturn(userId); + } + public String toJson(Object object) throws JsonProcessingException { return objectMapper.writeValueAsString(object); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java index 61f60cc..1f5b077 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java @@ -4,8 +4,9 @@ import org.springframework.context.annotation.Import; import com.inq.wishhair.wesharewishhair.global.config.QueryDslConfig; +import com.inq.wishhair.wesharewishhair.hairstyle.infrastructure.query.HairStyleQueryDslRepository; -@Import(QueryDslConfig.class) +@Import({QueryDslConfig.class, HairStyleQueryDslRepository.class}) @DataJpaTest public abstract class RepositoryTestSupport { } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepositoryTest.java new file mode 100644 index 0000000..93d2d5a --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleQueryRepositoryTest.java @@ -0,0 +1,169 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.domain; + +import static com.inq.wishhair.wesharewishhair.global.utils.PageableGenerator.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Slice; + +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHair; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHairRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.hairstyle.utils.HairRecommendCondition; +import com.inq.wishhair.wesharewishhair.user.domain.entity.Sex; + +@DisplayName("[HairStyleQueryRepository 테스트] - Domain") +public class HairStyleQueryRepositoryTest extends RepositoryTestSupport { + + @Autowired + private HairStyleQueryRepository hairStyleQueryRepository; + @Autowired + private HairStyleRepository hairStyleRepository; + @Autowired + private WishHairRepository wishHairRepository; + + private List hairStyles; + + @BeforeEach + void setUp() { + hairStyles = List.of( + HairStyleFixture.getWomanHairStyle("name1", List.of(Tag.ROUND, Tag.CUTE, Tag.SIMPLE)), + HairStyleFixture.getWomanHairStyle("name2", List.of(Tag.SQUARE, Tag.UPSTAGE, Tag.HARD)), + HairStyleFixture.getWomanHairStyle("name3", List.of(Tag.ROUND, Tag.BANGS, Tag.SIMPLE)), + HairStyleFixture.getWomanHairStyle("name4", List.of(Tag.ROUND, Tag.CURLY, Tag.SIMPLE)) + ); + + hairStyles.forEach(hairStyle -> hairStyleRepository.save(hairStyle)); + } + + private void wishHairStyles(List indexes) { + indexes.forEach(index -> + wishHairRepository.save(WishHair.createWishHair(1L, hairStyles.get(index).getId())) + ); + } + + private void assertHairStylesMatch(List actuals, List expectedList) { + assertThat(actuals).hasSameSizeAs(expectedList); + for (int i = 0; i < actuals.size(); i++) { + HairStyle actual = actuals.get(i); + HairStyle expected = expectedList.get(i); + + assertThat(actual).isEqualTo(expected); + } + } + + @Nested + @DisplayName("헤어스타일을 태그와 성별, 얼굴형 태그를 통해서 헤어스타일을 조회한다") + class findByRecommend { + @Test + @DisplayName("사용자의 얼굴형에 해당되지 않은 헤어스타일은 조회되지 않는다") + void test1() { + //given + HairRecommendCondition condition = new HairRecommendCondition( + List.of(Tag.CUTE, Tag.SIMPLE), Tag.HEART, Sex.WOMAN + ); + + //when + List actual = hairStyleQueryRepository.findByRecommend(condition, getDefaultPageable()); + + //then + assertHairStylesMatch(actual, List.of()); + } + + @Test + @DisplayName("조회된 헤어스타일은 일치하는 해시태그의 개수, 이름으로 정렬된다") + void test4() { + //given + HairRecommendCondition condition = new HairRecommendCondition( + List.of(Tag.CUTE, Tag.SIMPLE), Tag.ROUND, Sex.WOMAN + ); + + //when + List actual = hairStyleQueryRepository.findByRecommend(condition, getDefaultPageable()); + + //then + assertHairStylesMatch(actual, List.of(hairStyles.get(0), hairStyles.get(2), hairStyles.get(3))); + } + } + + @Nested + @DisplayName("얼굴형 헤어스타일 추천 쿼리") + class findByFaceShape { + @Test + @DisplayName("얼굴형 태그로 헤어를 검색하고, 찜 수와 이름으로 정렬한다") + void test5() { + //given + HairRecommendCondition condition = new HairRecommendCondition( + null, Tag.ROUND, Sex.WOMAN + ); + + wishHairStyles(List.of(0, 0, 2, 3)); + + //when + List result = hairStyleQueryRepository.findByFaceShape(condition, getDefaultPageable()); + + //then + assertHairStylesMatch( + result, + List.of( + hairStyles.get(0), + hairStyles.get(2), + hairStyles.get(3) + )); + } + + @Test + @DisplayName("얼굴형 태그 없이 검색 후 찜 수와 이름으로 정렬한다") + void test6() { + //given + HairRecommendCondition condition = new HairRecommendCondition( + null, null, Sex.WOMAN + ); + + wishHairStyles(List.of(0, 1, 1, 2)); + + //when + List result = hairStyleQueryRepository.findByFaceShape(condition, getDefaultPageable()); + + //then + assertHairStylesMatch( + result, + List.of( + hairStyles.get(1), + hairStyles.get(0), + hairStyles.get(2), + hairStyles.get(3) + ) + ); + } + } + + @Test + @DisplayName("찜한 헤어스타일을 생성된 순서로 조회한다") + void success() { + //given + wishHairStyles(List.of(3, 0, 1)); + + //when + Slice result = hairStyleQueryRepository.findByWish(1L, getDefaultPageable()); + + //then + assertThat(result.hasNext()).isFalse(); + assertHairStylesMatch( + result.getContent(), + List.of( + hairStyles.get(1), + hairStyles.get(0), + hairStyles.get(3) + ) + ); + } +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleTest.java new file mode 100644 index 0000000..35f8e26 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/HairStyleTest.java @@ -0,0 +1,53 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.HashTag; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +import com.inq.wishhair.wesharewishhair.photo.domain.Photo; +import com.inq.wishhair.wesharewishhair.user.domain.entity.Sex; + +@DisplayName("[HairStyle 테스트] - Domain") +class HairStyleTest { + + @Test + @DisplayName("[HairStyle 을 생성한다]") + void createHairStyle() { + //given + String name = "name"; + Sex sex = Sex.MAN; + List photoUrls = List.of("url1", "url2"); + List tags = List.of(Tag.CUTE, Tag.BANGS); + + //when + HairStyle actual = HairStyle.createHairStyle( + name, + sex, + photoUrls, + tags + ); + + //then + assertAll( + () -> assertThat(actual.getName()).isEqualTo(name), + () -> assertThat(actual.getSex()).isEqualTo(sex), + () -> { + List hashTags = actual.getHashTags(); + assertThat(hashTags).hasSize(2); + List actualTags = hashTags.stream().map(HashTag::getTag).toList(); + assertThat(actualTags).containsAll(tags); + }, + () -> { + List photos = actual.getPhotos(); + List actualPhotoUrls = photos.stream().map(Photo::getStoreUrl).toList(); + assertThat(actualPhotoUrls).containsAll(photoUrls); + } + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java new file mode 100644 index 0000000..a4ae159 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java @@ -0,0 +1,45 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.fixture; + +import java.util.List; + +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +import com.inq.wishhair.wesharewishhair.user.domain.entity.Sex; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class HairStyleFixture { + + private static final String NAME = "hair_style_name"; + private static final List TAGS = List.of(Tag.PERM, Tag.H_LONG, Tag.SQUARE, Tag.UPSTAGE); + private static final List IMAGE_URLS = List.of("hello1.jpg", "hello2.jpg"); + + public static HairStyle getWomanHairStyle() { + return HairStyle.createHairStyle( + NAME, + Sex.WOMAN, + IMAGE_URLS, + TAGS + ); + } + + public static HairStyle getWomanHairStyle(List tags) { + return HairStyle.createHairStyle( + NAME, + Sex.WOMAN, + IMAGE_URLS, + tags + ); + } + + public static HairStyle getWomanHairStyle(String name, List tags) { + return HairStyle.createHairStyle( + name, + Sex.WOMAN, + IMAGE_URLS, + tags + ); + } +} diff --git a/src/test/resources/images/hello3.jpg b/src/test/resources/images/hello3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bbcf7f0e7a6abf472045d6b9cf1182c2bc9e038 GIT binary patch literal 53411 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 literal 0 HcmV?d00001 From 3ddb96f1d38578d2a1399f7b3859db938e5aece1 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 11 Nov 2023 12:54:11 +0900 Subject: [PATCH 21/30] =?UTF-8?q?refactor:=20=EC=B0=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HairStyleFindService.java | 2 +- .../application/WishHairService.java | 49 ++++++----------- .../hairstyle/domain/wishhair/WishHair.java | 6 --- .../domain/wishhair/WishHairRepository.java | 2 +- .../infrastructure/WishHairJpaRepository.java | 8 +-- .../review/application/ReviewService.java | 2 +- src/main/resources/application.yml | 12 ++--- .../wishhair/wesharewishhair/LikeTest.java | 52 ++++++++++++++++--- 8 files changed, 70 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindService.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindService.java index c2cfb71..815fabc 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindService.java @@ -15,7 +15,7 @@ public class HairStyleFindService { private final HairStyleRepository hairStyleRepository; - public HairStyle findById(Long id) { + public HairStyle getById(Long id) { return hairStyleRepository.findById(id) .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java index 13f8f96..935f933 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java @@ -3,12 +3,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; -import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.WishHairResponse; import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHair; import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHairRepository; +import jakarta.persistence.EntityExistsException; import lombok.RequiredArgsConstructor; @Service @@ -18,55 +17,39 @@ public class WishHairService { private final WishHairRepository wishHairRepository; - @Transactional - public void executeWish( + private boolean existWishHair( final Long hairStyleId, final Long userId ) { - validateDoesNotExistWishHair(hairStyleId, userId); - - wishHairRepository.save(WishHair.createWishHair(userId, hairStyleId)); + return wishHairRepository.existsByHairStyleIdAndUserId(hairStyleId, userId); } @Transactional - public void cancelWish( + public boolean executeWish( final Long hairStyleId, final Long userId ) { - validateDoesWishHairExist(hairStyleId, userId); - - wishHairRepository.deleteByHairStyleIdAndUserId(hairStyleId, userId); - } - - public WishHairResponse checkIsWishing( - final Long hairStyleId, - final Long userId - ) { - return new WishHairResponse(existWishHair(hairStyleId, userId)); - } - - private boolean existWishHair( - final Long hairStyleId, - final Long userId - ) { - return wishHairRepository.existsByHairStyleIdAndUserId(hairStyleId, userId); + try { + wishHairRepository.save(WishHair.createWishHair(userId, hairStyleId)); + } catch (EntityExistsException e) { + return false; + } + return true; } - private void validateDoesWishHairExist( + @Transactional + public boolean cancelWish( final Long hairStyleId, final Long userId ) { - if (!existWishHair(hairStyleId, userId)) { - throw new WishHairException(ErrorCode.WISH_HAIR_NOT_EXIST); - } + int deletedCount = wishHairRepository.deleteByHairStyleIdAndUserId(hairStyleId, userId); + return deletedCount != 0; } - private void validateDoesNotExistWishHair( + public WishHairResponse checkIsWishing( final Long hairStyleId, final Long userId ) { - if (existWishHair(hairStyleId, userId)) { - throw new WishHairException(ErrorCode.WISH_HAIR_ALREADY_EXIST); - } + return new WishHairResponse(existWishHair(hairStyleId, userId)); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHair.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHair.java index 9e785e1..18001b4 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHair.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHair.java @@ -1,14 +1,11 @@ package com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair; -import java.time.LocalDateTime; - import com.inq.wishhair.wesharewishhair.global.auditing.BaseEntity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -22,16 +19,13 @@ public class WishHair extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @JoinColumn private Long hairStyleId; - @JoinColumn private Long userId; private WishHair(final Long hairStyleId, final Long userId) { this.hairStyleId = hairStyleId; this.userId = userId; - this.createdDate = LocalDateTime.now(); } //==Factory method==// diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java index 8e54687..c388f20 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java @@ -4,7 +4,7 @@ public interface WishHairRepository { WishHair save(WishHair wishHair); - void deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); + int deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); boolean existsByHairStyleIdAndUserId(Long hairStyleId, Long userId); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java index de9a6ce..08db093 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java @@ -1,19 +1,13 @@ package com.inq.wishhair.wesharewishhair.hairstyle.infrastructure; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHair; import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHairRepository; public interface WishHairJpaRepository extends WishHairRepository, JpaRepository { - @Modifying - @Query("delete from WishHair w where w.hairStyleId = :hairStyleId and w.userId = :userId") - void deleteByHairStyleIdAndUserId(@Param("hairStyleId") Long hairStyleId, - @Param("userId") Long userId); + int deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); boolean existsByHairStyleIdAndUserId(Long hairStyleId, Long userId); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java index c28240e..f2dee63 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java @@ -42,7 +42,7 @@ public Long createReview(ReviewCreateRequest request, Long userId) { List photoUrls = photoService.uploadPhotos(request.getFiles()); User user = userFindService.findByUserId(userId); - HairStyle hairStyle = hairStyleFindService.findById(request.getHairStyleId()); + HairStyle hairStyle = hairStyleFindService.getById(request.getHairStyleId()); Review review = generateReview(request, photoUrls, user, hairStyle); eventPublisher.publishEvent(new PointChargeEvent(100, userId)); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index be19839..fd37abb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,7 +16,7 @@ spring: default_batch_fetch_size: 100 # show_sql: true # format_sql: true - dialect: org.hibernate.dialect.MySQL8Dialect + dialect: org.hibernate.dialect.MySQLDialect open-in-view: false @@ -29,8 +29,8 @@ spring: mail: host: smtp.gmail.com port: 587 - username: ${MAIL} # namhm23@kyonggi.ac.kr - password: ${MAIL_PW} # qkvpxhpgyuywcbgh + username: ${MAIL} + password: ${MAIL_PW} protocol: smtp properties: mail: @@ -68,9 +68,9 @@ decorator: #JWT key jwt: - secret-key: ${JWT_SECRET_KEY} # wishhairOiJIUzI1NiIvLoAR5cCI6IkpXSCJ9.eyJzdWIiOiIiLCLoCP1lIjoiSm9obiBEV9UiLCJpYXBCusE1MTYyMzkwMjJ9.163aevla8s7d6f987qweahqwculaoxce80k1i2o387tg - access-token-validity: ${ACCESS_TOKEN_VALIDITY} # 1800000 - refresh-token-validity: ${REFRESH_TOKEN_VALIDITY} # 259200000 + secret-key: ${JWT_SECRET_KEY} + access-token-validity: ${ACCESS_TOKEN_VALIDITY} + refresh-token-validity: ${REFRESH_TOKEN_VALIDITY} # 네이버 클라우드 오브젝트 스토리지 cloud: diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java index 0666ac7..cda02c8 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java @@ -11,35 +11,71 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import com.inq.wishhair.wesharewishhair.common.config.EmbeddedRedisConfig; import com.inq.wishhair.wesharewishhair.review.application.LikeReviewService; -// @SpringBootTest +@SpringBootTest +@Import(EmbeddedRedisConfig.class) @DisplayName("[좋아요 동시성 테스트]") class LikeTest { - // @Autowired + @Autowired private LikeReviewService likeReviewService; // @Test - @DisplayName("[100개의 동시요청에서 100개의 좋아요 개수를 기록한다]") - void test() throws InterruptedException { + // @DisplayName("[좋아요 정보가 Redis 에 없을 때, 동시적 요청이 들어오면 동시성 이슈가 발생한다]") + // void fail() throws InterruptedException { + // //given + // int threadCount = 100; + // ExecutorService service = Executors.newFixedThreadPool(threadCount); + // CountDownLatch latch = new CountDownLatch(threadCount); + // + // //when + // for (int i = 0; i < threadCount; i++) { + // service.execute(() -> { + // Random random = new Random(); + // int id = random.nextInt(Integer.MAX_VALUE); + // + // likeReviewService.executeLike(1L, (long)id); + // latch.countDown(); + // }); + // } + // + // latch.await(); + // + // //then -> 동시성 이슈 발생으로 100개의 좋아요 보다 적은 likeCount + // Long likeCount = likeReviewService.getLikeCount(1L); + // assertThat(likeCount).isLessThan(100L); + // } + + @Test + @DisplayName("[좋아요 정보가 Redis 에 있을 때에는 동시성 이슈가 발생하지 않는다.]") + void success() throws InterruptedException { //given - int threadCount = 1000; - ExecutorService service = Executors.newFixedThreadPool(50); + int threadCount = 100; + ExecutorService service = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); + + //데이터가 존재할 때는 동시성 해결 + likeReviewService.executeLike(1L, 1L); + + //when for (int i = 0; i < threadCount; i++) { service.execute(() -> { Random random = new Random(); int id = random.nextInt(Integer.MAX_VALUE); - likeReviewService.executeLike(201L, (long)id); + likeReviewService.executeLike(1L, (long)id); latch.countDown(); }); } latch.await(); + + //then Long likeCount = likeReviewService.getLikeCount(1L); - assertThat(likeCount).isEqualTo(102); + assertThat(likeCount).isEqualTo(101); } } From d3ba9b51d856e1a66244da750f1558d9633550d5 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 11 Nov 2023 12:55:46 +0900 Subject: [PATCH 22/30] =?UTF-8?q?feat:=20Redis=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/EmbeddedRedisConfig.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java new file mode 100644 index 0000000..20ec555 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java @@ -0,0 +1,28 @@ +package com.inq.wishhair.wesharewishhair.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import redis.embedded.RedisServer; + +@TestConfiguration +public class EmbeddedRedisConfig { + + private final RedisServer redisServer; + + public EmbeddedRedisConfig(@Value("${spring.data.redis.port}") int port) { + this.redisServer = new RedisServer(port); + } + + @PostConstruct + public void startRedis() { + this.redisServer.start(); + } + + @PreDestroy + public void stopRedis() { + this.redisServer.stop(); + } +} \ No newline at end of file From 32927bf28cf132db0c2e3f62428382f530ccaf30 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 11 Nov 2023 14:48:26 +0900 Subject: [PATCH 23/30] =?UTF-8?q?feat:=20HairStyle=20Application=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HairStyleSearchService.java | 10 +- .../application/WishHairService.java | 4 +- .../domain/wishhair/WishHairRepository.java | 2 +- .../infrastructure/WishHairJpaRepository.java | 2 +- .../common/support/MockTestSupport.java | 8 + .../application/HairStyleFindServiceTest.java | 66 +++++++ .../HairStyleSearchServiceTest.java | 185 ++++++++++++++++++ .../application/WishHairServiceTest.java | 79 ++++++++ .../hairstyle/fixture/HairStyleFixture.java | 14 +- .../user/fixture/UserFixture.java | 6 + 10 files changed, 359 insertions(+), 17 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/common/support/MockTestSupport.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairServiceTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java index 43cb4c1..f18f7ed 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java @@ -15,23 +15,21 @@ import com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; -import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleQueryRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleResponse; +import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleSimpleResponse; import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleQueryRepository; import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; -import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleResponse; -import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleSimpleResponse; import com.inq.wishhair.wesharewishhair.hairstyle.utils.HairRecommendCondition; -import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import com.inq.wishhair.wesharewishhair.user.application.UserFindService; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; @Service @RequiredArgsConstructor @Transactional(readOnly = true) -@Slf4j public class HairStyleSearchService { private final HairStyleRepository hairStyleRepository; diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java index 935f933..7c241f8 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairService.java @@ -42,8 +42,8 @@ public boolean cancelWish( final Long hairStyleId, final Long userId ) { - int deletedCount = wishHairRepository.deleteByHairStyleIdAndUserId(hairStyleId, userId); - return deletedCount != 0; + wishHairRepository.deleteByHairStyleIdAndUserId(hairStyleId, userId); + return true; } public WishHairResponse checkIsWishing( diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java index c388f20..8e54687 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/domain/wishhair/WishHairRepository.java @@ -4,7 +4,7 @@ public interface WishHairRepository { WishHair save(WishHair wishHair); - int deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); + void deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); boolean existsByHairStyleIdAndUserId(Long hairStyleId, Long userId); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java index 08db093..cec2b79 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java @@ -7,7 +7,7 @@ public interface WishHairJpaRepository extends WishHairRepository, JpaRepository { - int deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); + void deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); boolean existsByHairStyleIdAndUserId(Long hairStyleId, Long userId); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/MockTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/MockTestSupport.java new file mode 100644 index 0000000..d2dc444 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/MockTestSupport.java @@ -0,0 +1,8 @@ +package com.inq.wishhair.wesharewishhair.common.support; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public abstract class MockTestSupport { +} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindServiceTest.java new file mode 100644 index 0000000..2a95f66 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleFindServiceTest.java @@ -0,0 +1,66 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; + +@DisplayName("[HairStyleFindService 테스트] - Application") +class HairStyleFindServiceTest { + + private final HairStyleFindService hairStyleFindService; + private final HairStyleRepository hairStyleRepository; + + public HairStyleFindServiceTest() { + this.hairStyleRepository = Mockito.mock(HairStyleRepository.class); + this.hairStyleFindService = new HairStyleFindService(hairStyleRepository); + } + + @Nested + @DisplayName("[아이디로 HairStyle 을 조회한다]") + class findById { + + @Test + @DisplayName("[성공적으로 조회한다]") + void success() { + //given + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + given(hairStyleRepository.findById(1L)) + .willReturn(Optional.of(hairStyle)); + + //when + HairStyle actual = hairStyleFindService.getById(1L); + + //then + assertThat(actual).isEqualTo(hairStyle); + } + + @Test + @DisplayName("[아이디에 해당하는 HairStyle 이 존재하지 않아 실패한다]") + void fail() { + //given + given(hairStyleRepository.findById(1L)) + .willReturn(Optional.empty()); + + //when + ThrowingCallable when = () -> hairStyleFindService.getById(1L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.NOT_EXIST_KEY.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchServiceTest.java new file mode 100644 index 0000000..c2926ed --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchServiceTest.java @@ -0,0 +1,185 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +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.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; + +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; +import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; +import com.inq.wishhair.wesharewishhair.global.dto.response.Paging; +import com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleResponse; +import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HairStyleSimpleResponse; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleQueryRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.hairstyle.utils.HairRecommendCondition; +import com.inq.wishhair.wesharewishhair.user.application.UserFindService; +import com.inq.wishhair.wesharewishhair.user.domain.entity.FaceShape; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[HairStyleSearchService 테스트]") +class HairStyleSearchServiceTest extends MockTestSupport { + + @InjectMocks + private HairStyleSearchService hairStyleSearchService; + @Mock + private HairStyleRepository hairStyleRepository; + @Mock + private HairStyleQueryRepository hairStyleQueryRepository; + @Mock + private UserFindService userFindService; + + private final List hairStyles = List.of( + HairStyleFixture.getWomanHairStyle(1L), + HairStyleFixture.getWomanHairStyle(2L) + ); + + private void assertHairStyleResponse(HairStyleResponse response, HairStyle hairStyle) { + assertAll( + () -> assertThat(response.hairStyleId()).isEqualTo(hairStyle.getId()), + () -> assertThat(response.name()).isEqualTo(hairStyle.getName()), + () -> assertThat(response.hashTags()).hasSameSizeAs(hairStyle.getHashTags()), + () -> assertThat(response.photos()).hasSameSizeAs(hairStyle.getPhotos()) + ); + } + + @Nested + @DisplayName("[태그와 사용자의 얼굴형을 기반으로 헤어스타일을 추천한다]") + class recommendHair { + + @Test + @DisplayName("[성공적으로 추천한다]") + void success() { + //given + User user = UserFixture.getFixedWomanUser(1L); + user.updateFaceShape(new FaceShape(Tag.ROUND)); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + given(hairStyleQueryRepository.findByRecommend(any(HairRecommendCondition.class), any(Pageable.class))) + .willReturn(hairStyles); + + //when + ResponseWrapper actual = hairStyleSearchService.recommendHair( + List.of(Tag.CUTE, Tag.LIGHT), 1L); + + //then + List result = actual.getResult(); + assertThat(result).hasSameSizeAs(hairStyles); + for (int i = 0; i < result.size(); i++) { + assertHairStyleResponse(result.get(i), hairStyles.get(i)); + } + } + + @Test + @DisplayName("[얼굴형이 존재하지 않는 user 로 실패한다]") + void fail() { + //given + User user = UserFixture.getFixedWomanUser(1L); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + //when + ThrowingCallable when = () -> hairStyleSearchService.recommendHair(List.of(Tag.CUTE, Tag.LIGHT), 1L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.USER_NO_FACE_SHAPE_TAG.getMessage()); + } + } + + @Test + @DisplayName("[홈화면 사용자 맞춤 헤어스타일 추천한다]") + void recommendHairByFaceShape() { + //given + User user = UserFixture.getFixedWomanUser(1L); + user.updateFaceShape(new FaceShape(Tag.ROUND)); + given(userFindService.findByUserId(user.getId())) + .willReturn(user); + + given(hairStyleQueryRepository.findByFaceShape(any(HairRecommendCondition.class), any(Pageable.class))) + .willReturn(hairStyles); + + //when + ResponseWrapper actual = hairStyleSearchService.recommendHairByFaceShape(1L); + + //then + List result = actual.getResult(); + assertThat(result).hasSameSizeAs(hairStyles); + for (int i = 0; i < result.size(); i++) { + assertHairStyleResponse(result.get(i), hairStyles.get(i)); + } + } + + @Test + @DisplayName("[찜한 헤어스타일을 조회한다]") + void findWishHairStyles() { + //given + SliceImpl sliceHairStyles = new SliceImpl<>(hairStyles); + given(hairStyleQueryRepository.findByWish(eq(1L), any(Pageable.class))) + .willReturn(sliceHairStyles); + + //when + PagedResponse actual = hairStyleSearchService.findWishHairStyles( + 1L, PageRequest.of(0, 4) + ); + + //then + Paging paging = actual.getPaging(); + assertAll( + () -> assertThat(paging.getPage()).isZero(), + () -> assertThat(paging.hasNext()).isFalse(), + () -> assertThat(paging.getContentSize()).isEqualTo(2) + ); + + List result = actual.getResult(); + assertThat(result).hasSameSizeAs(hairStyles); + for (int i = 0; i < result.size(); i++) { + assertHairStyleResponse(result.get(i), hairStyles.get(i)); + } + } + + @Test + @DisplayName("[전체 헤어스타일을 조회한다]") + void findAllHairStyle() { + //given + given(hairStyleRepository.findAllByOrderByName()) + .willReturn(hairStyles); + + //when + ResponseWrapper actual = hairStyleSearchService.findAllHairStyle(); + + //then + List result = actual.getResult(); + assertThat(result).hasSameSizeAs(hairStyles); + for (int i = 0; i < result.size(); i++) { + HairStyleSimpleResponse response = result.get(i); + HairStyle hairStyle = hairStyles.get(i); + + assertAll( + () -> assertThat(response.hairStyleId()).isEqualTo(hairStyle.getId()), + () -> assertThat(response.hairStyleName()).isEqualTo(hairStyle.getName()) + ); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairServiceTest.java new file mode 100644 index 0000000..cc19375 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/WishHairServiceTest.java @@ -0,0 +1,79 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.WishHairResponse; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHair; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHairRepository; + +import jakarta.persistence.EntityExistsException; + +@DisplayName("[WishHairService 테스트] - Application") +class WishHairServiceTest extends MockTestSupport { + + @InjectMocks + private WishHairService wishHairService; + @Mock + private WishHairRepository wishHairRepository; + + @Nested + @DisplayName("[헤어스타일 찜한다]") + class executeWish { + + @Test + @DisplayName("[성공적으로 찜하고 true 를 반환한다]") + void success() { + //when + boolean actual = wishHairService.executeWish(1L, 1L); + + //then + assertThat(actual).isTrue(); + } + + @Test + @DisplayName("[이미 찜한 상태여서 false 를 반환한다]") + void fail() { + //given + given(wishHairRepository.save(any(WishHair.class))) + .willThrow(new EntityExistsException()); + + //when + boolean actual = wishHairService.executeWish(1L, 1L); + + //then + assertThat(actual).isFalse(); + } + } + + @Test + @DisplayName("[찜을 취소한다]") + void cancelWish() { + //when + boolean actual = wishHairService.cancelWish(1L, 1L); + + //then + assertThat(actual).isTrue(); + } + + @Test + @DisplayName("[찜한 헤어스타일인지 확인한다]") + void checkIsWishing() { + //given + given(wishHairRepository.existsByHairStyleIdAndUserId(1L, 1L)) + .willReturn(true); + + //when + WishHairResponse actual = wishHairService.checkIsWishing(1L, 1L); + + //then + assertThat(actual.isWishing()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java index a4ae159..38f64ea 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/fixture/HairStyleFixture.java @@ -2,6 +2,8 @@ import java.util.List; +import org.springframework.test.util.ReflectionTestUtils; + import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; import com.inq.wishhair.wesharewishhair.user.domain.entity.Sex; @@ -25,15 +27,13 @@ public static HairStyle getWomanHairStyle() { ); } - public static HairStyle getWomanHairStyle(List tags) { - return HairStyle.createHairStyle( - NAME, - Sex.WOMAN, - IMAGE_URLS, - tags - ); + public static HairStyle getWomanHairStyle(Long id) { + HairStyle hairStyle = getWomanHairStyle(); + ReflectionTestUtils.setField(hairStyle, "id", id); + return hairStyle; } + public static HairStyle getWomanHairStyle(String name, List tags) { return HairStyle.createHairStyle( name, diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java index 932d7c6..e8443c8 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java @@ -45,4 +45,10 @@ public static User getFixedWomanUser() { Sex.WOMAN ); } + + public static User getFixedWomanUser(Long id) { + User user = getFixedWomanUser(); + ReflectionTestUtils.setField(user, "id", id); + return user; + } } From 7a2a9a5d83ce48dd6090553f724e63feb9b47c11 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 11 Nov 2023 15:08:25 +0900 Subject: [PATCH 24/30] =?UTF-8?q?refactor:=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=8F=8C=EC=95=84=EA=B0=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/TokenReissueController.java | 6 ++-- .../auth/presentation/AuthControllerTest.java | 11 ++++++- .../TokenReissueControllerTest.java | 31 +++++++++++-------- .../common/support/ApiTestSupport.java | 21 +++---------- .../presentation/PointControllerTest.java | 8 ++--- 5 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueController.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueController.java index 143df25..d4c70c0 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueController.java @@ -19,10 +19,8 @@ public class TokenReissueController { private final TokenReissueService tokenReissueService; - @PostMapping("/token/reissue") - public ResponseEntity reissueToken( - final @FetchAuthInfo AuthInfo authInfo - ) { + @PostMapping("/tokens/reissue") + public ResponseEntity reissueToken(@FetchAuthInfo AuthInfo authInfo) { TokenResponse response = tokenReissueService.reissueToken(authInfo.userId(), authInfo.token()); return ResponseEntity.ok(response); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java index 0aa46ec..45a48e0 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java @@ -17,6 +17,9 @@ import com.inq.wishhair.wesharewishhair.auth.application.dto.response.LoginResponse; import com.inq.wishhair.wesharewishhair.auth.presentation.dto.request.LoginRequest; import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; @DisplayName("[AuthController 테스트] - API") class AuthControllerTest extends ApiTestSupport { @@ -28,6 +31,8 @@ class AuthControllerTest extends ApiTestSupport { private MockMvc mockMvc; @MockBean private AuthService authService; + @Autowired + private UserRepository userRepository; @Test @DisplayName("[로그인 API 를 호출한다]") @@ -60,11 +65,15 @@ void login() throws Exception { @Test @DisplayName("[로그아웃 API 를 호출한다]") void logout() throws Exception { + //given + User user = UserFixture.getFixedManUser(); + Long userId = userRepository.save(user).getId(); + //when ResultActions result = mockMvc.perform( MockMvcRequestBuilders .post(LOGOUT_URL) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) ); //then diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java index d4d97c2..4eb9142 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/TokenReissueControllerTest.java @@ -1,51 +1,56 @@ package com.inq.wishhair.wesharewishhair.auth.presentation; import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; -import static org.mockito.BDDMockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import com.inq.wishhair.wesharewishhair.auth.application.TokenReissueService; -import com.inq.wishhair.wesharewishhair.auth.application.dto.response.TokenResponse; +import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; +import com.inq.wishhair.wesharewishhair.auth.domain.entity.Token; import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; @DisplayName("[TokenReissueController 테스트] - API") class TokenReissueControllerTest extends ApiTestSupport { - private static final String REISSUE_TOKEN_URL = "/api/token/reissue"; + private static final String REISSUE_TOKEN_URL = "/api/tokens/reissue"; @Autowired private MockMvc mockMvc; - @MockBean - private TokenReissueService tokenReissueService; + @Autowired + private UserRepository userRepository; + @Autowired + private TokenRepository tokenRepository; @Test @DisplayName("[토큰 재발급 API 를 호출한다]") void reissueToken() throws Exception { //given - TokenResponse tokenResponse = new TokenResponse("accessToken", "refreshToken"); - given(tokenReissueService.reissueToken(1L, TOKEN)) - .willReturn(tokenResponse); + User user = UserFixture.getFixedManUser(); + Long userId = userRepository.save(user).getId(); + + String token = getAccessToken(userId); + tokenRepository.save(Token.issue(userId, token)); //when ResultActions result = mockMvc.perform( MockMvcRequestBuilders .post(REISSUE_TOKEN_URL) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + token) ); //then result.andExpectAll( status().isOk(), - jsonPath("$.accessToken").value(tokenResponse.accessToken()), - jsonPath("$.refreshToken").value(tokenResponse.refreshToken()) + jsonPath("$.accessToken").exists(), + jsonPath("$.refreshToken").exists() ); } } \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java index ae2ab27..6750bf0 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java @@ -1,17 +1,12 @@ package com.inq.wishhair.wesharewishhair.common.support; -import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; -import static org.mockito.BDDMockito.*; - -import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.inq.wishhair.wesharewishhair.auth.domain.AuthToken; import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; @SpringBootTest @@ -21,19 +16,13 @@ public abstract class ApiTestSupport { private final ObjectMapper objectMapper = new ObjectMapper(); - @MockBean - protected AuthTokenManager authTokenManager; + @Autowired + private AuthTokenManager authTokenManager; - @BeforeEach - public void setAuthorization() { - given(authTokenManager.generate(any(Long.class))).willReturn(new AuthToken(TOKEN, TOKEN)); - given(authTokenManager.getId(anyString())).willReturn(1L); + protected String getAccessToken(Long userId) { + return authTokenManager.generate(userId).accessToken(); } - protected void setAuthorization(Long userId) { - given(authTokenManager.generate(any(Long.class))).willReturn(new AuthToken(TOKEN, TOKEN)); - given(authTokenManager.getId(anyString())).willReturn(userId); - } public String toJson(Object object) throws JsonProcessingException { return objectMapper.writeValueAsString(object); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java index 256d398..28f8850 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java @@ -36,14 +36,14 @@ class PointControllerTest extends ApiTestSupport { void usePoint() throws Exception { //given User user = UserFixture.getFixedManUser(); - userRepository.save(user); + Long userId = userRepository.save(user).getId(); pointLogRepository.save(PointLogFixture.getUsePointLog(user)); //when ResultActions result = mockMvc.perform( MockMvcRequestBuilders .post(POINT_USE_URL) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) .contentType(MediaType.APPLICATION_JSON) .content(toJson(PointLogFixture.getPointUseRequest())) ); @@ -60,13 +60,11 @@ void findPointHistories() throws Exception { Long userId = userRepository.save(user).getId(); pointLogRepository.save(PointLogFixture.getUsePointLog(user)); - setAuthorization(userId); - //when ResultActions result = mockMvc.perform( MockMvcRequestBuilders .get(POINT_QUERY_URL) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) ); //then From 64e81f89ea430481a2b8da91b616e9886835e57e Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sat, 11 Nov 2023 15:49:18 +0900 Subject: [PATCH 25/30] =?UTF-8?q?test:=20HairStyle=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HairStyleSearchController.java | 10 +- .../presentation/WishHairController.java | 12 +- .../common/support/ApiTestSupport.java | 1 - .../presentation/HairStyleApiTest.java | 140 ++++++++++++++++++ .../presentation/WishHairControllerTest.java | 109 ++++++++++++++ 5 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairControllerTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleSearchController.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleSearchController.java index 44246a4..9c2c2fa 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleSearchController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleSearchController.java @@ -31,8 +31,8 @@ public class HairStyleSearchController { @GetMapping("/recommend") public ResponseWrapper respondRecommendedHairStyle( - final @RequestParam(defaultValue = "ERROR") List tags, - final @FetchAuthInfo AuthInfo authInfo + @RequestParam(defaultValue = "ERROR") List tags, + @FetchAuthInfo AuthInfo authInfo ) { validateHasTag(tags); @@ -41,15 +41,15 @@ public ResponseWrapper respondRecommendedHairStyle( @GetMapping("/home") public ResponseWrapper findHairStyleByFaceShape( - final @FetchAuthInfo AuthInfo authInfo + @FetchAuthInfo AuthInfo authInfo ) { return hairStyleSearchService.recommendHairByFaceShape(authInfo.userId()); } @GetMapping("/wish") public PagedResponse findWishHairStyles( - final @FetchAuthInfo AuthInfo authInfo, - final @PageableDefault Pageable pageable) { + @FetchAuthInfo AuthInfo authInfo, + @PageableDefault Pageable pageable) { return hairStyleSearchService.findWishHairStyles(authInfo.userId(), pageable); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairController.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairController.java index 2c5f8f3..07c1089 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairController.java @@ -25,8 +25,8 @@ public class WishHairController { @PostMapping(path = "{hairStyleId}") public ResponseEntity executeWish( - final @PathVariable Long hairStyleId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long hairStyleId, + @FetchAuthInfo AuthInfo authInfo ) { wishHairService.executeWish(hairStyleId, authInfo.userId()); @@ -36,8 +36,8 @@ public ResponseEntity executeWish( @DeleteMapping(path = "{hairStyleId}") public ResponseEntity cancelWish( - final @PathVariable Long hairStyleId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long hairStyleId, + @FetchAuthInfo AuthInfo authInfo ) { wishHairService.cancelWish(hairStyleId, authInfo.userId()); @@ -46,8 +46,8 @@ public ResponseEntity cancelWish( @GetMapping(path = {"{hairStyleId}"}) public ResponseEntity checkIsWishing( - final @PathVariable Long hairStyleId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long hairStyleId, + @FetchAuthInfo AuthInfo authInfo ) { WishHairResponse result = wishHairService.checkIsWishing(hairStyleId, authInfo.userId()); return ResponseEntity.ok(result); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java index 6750bf0..cf1a2b1 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java @@ -23,7 +23,6 @@ protected String getAccessToken(Long userId) { return authTokenManager.generate(userId).accessToken(); } - public String toJson(Object object) throws JsonProcessingException { return objectMapper.writeValueAsString(object); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java new file mode 100644 index 0000000..932e304 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java @@ -0,0 +1,140 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHair; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHairRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.FaceShape; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[HairStyleApi 테스트]") +class HairStyleApiTest extends ApiTestSupport { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserRepository userRepository; + @Autowired + private HairStyleRepository hairStyleRepository; + @Autowired + private WishHairRepository wishHairRepository; + + List hairStyles = List.of( + HairStyleFixture.getWomanHairStyle("A", List.of(Tag.CUTE, Tag.LIGHT, Tag.OBLONG)), + HairStyleFixture.getWomanHairStyle("B", List.of(Tag.CUTE, Tag.BANGS, Tag.OBLONG)), + HairStyleFixture.getWomanHairStyle("C", List.of(Tag.CUTE, Tag.SIMPLE, Tag.ROUND)) + ); + + @BeforeEach + void setUp() { + hairStyles.forEach(hairStyle -> hairStyleRepository.save(hairStyle)); + } + + @Test + @DisplayName("[헤어스타일 메인 추천 API 를 호출한다]") + void respondRecommendedHairStyle() throws Exception { + //given + User user = UserFixture.getFixedWomanUser(); + user.updateFaceShape(new FaceShape(Tag.OBLONG)); + Long userId = userRepository.save(user).getId(); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/hair_styles/recommend?tags=CUTE&tags=LIGHT") + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.result.size()").value(2) + ); + } + + @Test + @DisplayName("[헤어스타일 홈화면 추천 API 를 호출한다]") + void findHairStyleByFaceShape() throws Exception { + //given + User user = UserFixture.getFixedWomanUser(); + user.updateFaceShape(new FaceShape(Tag.OBLONG)); + Long userId = userRepository.save(user).getId(); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/hair_styles/home") + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.result.size()").value(2) + ); + } + + @Test + @DisplayName("[찜한 헤어스타일 조회 API 를 호출한다]") + void findWishHairStyles() throws Exception { + //given + User user = UserFixture.getFixedWomanUser(); + Long userId = userRepository.save(user).getId(); + + wishHairRepository.save(WishHair.createWishHair(userId, hairStyles.get(0).getId())); + wishHairRepository.save(WishHair.createWishHair(userId, hairStyles.get(2).getId())); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/hair_styles/wish") + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.result.size()").value(2) + ); + } + + @Test + @DisplayName("[모든 헤어스타일 간단한 정보 조회 API 를 호출한다]") + void findAllHairStyles() throws Exception { + //given + User user = UserFixture.getFixedWomanUser(); + Long userId = userRepository.save(user).getId(); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/hair_styles") + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.result.size()").value(3) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairControllerTest.java new file mode 100644 index 0000000..9a475bc --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/WishHairControllerTest.java @@ -0,0 +1,109 @@ +package com.inq.wishhair.wesharewishhair.hairstyle.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHair; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.wishhair.WishHairRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[WishHairApi 테스트]") +class WishHairControllerTest extends ApiTestSupport { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserRepository userRepository; + @Autowired + private HairStyleRepository hairStyleRepository; + @Autowired + private WishHairRepository wishHairRepository; + + private final HairStyle hairStyle = HairStyleFixture.getWomanHairStyle( + "A", List.of(Tag.CUTE, Tag.LIGHT, Tag.OBLONG) + ); + + @BeforeEach + void setUp() { + hairStyleRepository.save(hairStyle); + } + + private Long setUser() { + User user = UserFixture.getFixedWomanUser(); + return userRepository.save(user).getId(); + } + + @Test + @DisplayName("[찜 API 를 호출한다]") + void executeWish() throws Exception { + //given + Long userId = setUser(); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .post("/api/hair_styles/wish/" + hairStyle.getId()) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpect(status().isOk()); + } + + @Test + @DisplayName("[찜 취소 API 를 호출한다]") + void cancelWish() throws Exception { + //given + Long userId = setUser(); + wishHairRepository.save(WishHair.createWishHair(userId, hairStyle.getId())); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/hair_styles/wish/" + hairStyle.getId()) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpect(status().isOk()); + } + + @Test + @DisplayName("[찜 확인 API 를 호출한다]") + void checkIsWishing() throws Exception { + //given + Long userId = setUser(); + wishHairRepository.save(WishHair.createWishHair(userId, hairStyle.getId())); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/hair_styles/wish/" + hairStyle.getId()) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.isWishing").value(true) + ); + } +} \ No newline at end of file From 124489bd89921d85eda1ca6b18a36756f9a63f2e Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Sun, 12 Nov 2023 16:01:04 +0900 Subject: [PATCH 26/30] =?UTF-8?q?test:=20Review=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B3=84=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auditing/BaseEntity.java | 10 -- .../global/utils/PageableGenerator.java | 4 + .../review/application/LikeReviewService.java | 23 +-- .../review/application/ReviewFindService.java | 2 +- .../review/application/ReviewService.java | 6 +- .../review/domain/ReviewRepository.java | 12 +- .../review/domain/entity/Review.java | 24 +-- .../likereview/LikeReviewRepository.java | 6 +- .../LikeReviewJpaRepository.java | 18 +- .../infrastructure/ReviewJpaRepository.java | 37 +--- .../ReviewQueryDslRepository.java | 27 +-- .../common/support/RepositoryTestSupport.java | 3 +- .../domain/ReviewQueryRepositoryTest.java | 159 ++++++++++++++++++ .../review/domain/entity/ReviewTest.java | 108 ++++++++++++ .../review/fixture/ReviewFixture.java | 36 ++++ 15 files changed, 351 insertions(+), 124 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepositoryTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/domain/entity/ReviewTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/auditing/BaseEntity.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/auditing/BaseEntity.java index 3bd4b3d..7357c32 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/auditing/BaseEntity.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/auditing/BaseEntity.java @@ -6,7 +6,6 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; @@ -16,18 +15,9 @@ @Getter public class BaseEntity { - @Column( - nullable = false, - insertable = false, - updatable = false, - columnDefinition = "datetime default CURRENT_TIMESTAMP") @CreatedDate protected LocalDateTime createdDate; - @Column( - nullable = false, - insertable = false, - columnDefinition = "datetime default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP") @LastModifiedDate private LocalDateTime updatedDate; } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/PageableGenerator.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/PageableGenerator.java index 7e46c47..7dc3b0a 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/PageableGenerator.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/PageableGenerator.java @@ -23,4 +23,8 @@ public static Pageable generateSimplePageable(int size) { public static Pageable generateDateDescPageable(int size) { return PageRequest.of(0, size, Sort.by(Sort.Direction.DESC, DATE)); } + + public static Pageable generateDateAscPageable(int size) { + return PageRequest.of(0, size, Sort.by(Sort.Direction.ASC, DATE)); + } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java index e844b56..694e7b5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java @@ -21,6 +21,12 @@ public class LikeReviewService { private final LikeReviewRepository likeReviewRepository; private final RedisUtils redisUtils; + private Long updateLikeCountFromRedis(Long reviewId) { + Long likeCount = likeReviewRepository.countByReviewId(reviewId); + redisUtils.setData(reviewId, likeCount); + return likeCount; + } + @Transactional public boolean executeLike(Long reviewId, Long userId) { try { @@ -41,10 +47,7 @@ public boolean executeLike(Long reviewId, Long userId) { @Transactional public boolean cancelLike(Long reviewId, Long userId) { - int deletedCount = likeReviewRepository.deleteByUserIdAndReviewId(userId, reviewId); - if (deletedCount == 0) { - return false; - } + likeReviewRepository.deleteByUserIdAndReviewId(userId, reviewId); redisUtils.getData(reviewId) .ifPresentOrElse( @@ -56,7 +59,8 @@ public boolean cancelLike(Long reviewId, Long userId) { } public LikeReviewResponse checkIsLiking(Long userId, Long reviewId) { - return new LikeReviewResponse(existLikeReview(userId, reviewId)); + boolean isLiking = likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); + return new LikeReviewResponse(isLiking); } public Long getLikeCount(Long reviewId) { @@ -70,13 +74,4 @@ public List getLikeCounts(List reviewIds) { .toList(); } - private Long updateLikeCountFromRedis(Long reviewId) { - Long likeCount = likeReviewRepository.countByReviewId(reviewId); - redisUtils.setData(reviewId, likeCount); - return likeCount; - } - - private boolean existLikeReview(Long userId, Long reviewId) { - return likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); - } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java index da83c53..fe6aeae 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java @@ -28,6 +28,6 @@ public Review findWithPhotosById(Long id) { } public List findWithPhotosByUserId(Long userId) { - return reviewRepository.findWithPhotosByUserId(userId); + return reviewRepository.findWithPhotosByWriterId(userId); } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java index f2dee63..629dbbd 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java @@ -55,7 +55,7 @@ public void deleteReview(Long reviewId, Long userId) { Review review = reviewFindService.findWithPhotosById(reviewId); validateIsWriter(userId, review); - likeReviewRepository.deleteAllByReview(reviewId); + likeReviewRepository.deleteByReviewId(reviewId); photoService.deletePhotosByReviewId(review); reviewRepository.delete(review); } @@ -75,9 +75,9 @@ public void deleteReviewByWriter(Long userId) { List reviews = reviewFindService.findWithPhotosByUserId(userId); List reviewIds = reviews.stream().map(Review::getId).toList(); - likeReviewRepository.deleteAllByReviews(reviewIds); + likeReviewRepository.deleteByReviewIdIn(reviewIds); photoService.deletePhotosByWriter(reviews); - reviewRepository.deleteAllByWriter(reviewIds); + reviewRepository.deleteByIdIn(reviewIds); } private void validateIsWriter(Long userId, Review review) { diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java index 8930127..fdc93e5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewRepository.java @@ -2,9 +2,6 @@ import java.util.List; import java.util.Optional; -import java.util.Set; - -import org.springframework.data.repository.query.Param; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; @@ -18,14 +15,9 @@ public interface ReviewRepository { Optional findWithPhotosById(Long id); //회원 탈퇴를 위한 사용자가 작성한 리뷰 조회 - List findWithPhotosByUserId(Long userId); + List findWithPhotosByWriterId(Long userId); - void deleteAllByWriter(List reviewIds); + void deleteByIdIn(List reviewIds); void delete(Review review); - - //reviewIds 에 해당되는 리뷰의 좋아요 수 조회 - List countLikeReviewByIdsOrderById(@Param("ids") Set reviewIds); - - void updateLikeCountById(Long id, int likeCount); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java index 4e3f8c8..f5fcbe5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/entity/Review.java @@ -1,6 +1,5 @@ package com.inq.wishhair.wesharewishhair.review.domain.entity; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -12,6 +11,7 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -39,6 +39,7 @@ public class Review extends BaseEntity { @JoinColumn(name = "user_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private User writer; + @Embedded private Contents contents; @Column(nullable = false) @@ -52,21 +53,22 @@ public class Review extends BaseEntity { @JoinColumn(name = "hair_style_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private HairStyle hairStyle; - private Long likeCount; - private Review(User writer, String contents, Score score, List photos, HairStyle hairStyle) { this.writer = writer; this.contents = new Contents(contents); this.score = score; applyPhotos(photos); this.hairStyle = hairStyle; - this.createdDate = LocalDateTime.now(); - this.likeCount = 0L; } //==Factory method==// public static Review createReview( - User user, String contents, Score score, List photos, HairStyle hairStyle) { + User user, + String contents, + Score score, + List photos, + HairStyle hairStyle + ) { return new Review(user, contents, score, photos, hairStyle); } @@ -89,16 +91,6 @@ public void updateReview(Contents contents, Score score, List storeUrls) updatePhotos(storeUrls); } - public void addLike() { - this.likeCount++; - } - - public void cancelLike() { - if (likeCount > 0) { - this.likeCount--; - } - } - private void updateContents(Contents contents) { this.contents = contents; } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java index 3410537..b8b9e19 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java @@ -8,11 +8,11 @@ public interface LikeReviewRepository { Long countByReviewId(Long reviewId); - void deleteAllByReview(Long reviewId); + void deleteByReviewId(Long reviewId); - int deleteByUserIdAndReviewId(Long userId, Long reviewId); + void deleteByUserIdAndReviewId(Long userId, Long reviewId); boolean existsByUserIdAndReviewId(Long userId, Long reviewId); - void deleteAllByReviews(List reviewIds); + void deleteByReviewIdIn(List reviewIds); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java index dd53ba8..1d35535 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/LikeReviewJpaRepository.java @@ -3,8 +3,6 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReview; @@ -12,21 +10,13 @@ public interface LikeReviewJpaRepository extends LikeReviewRepository, JpaRepository { - @Query("select count(l.id) from LikeReview l where l.reviewId = :reviewId") - Long countByReviewId(@Param("reviewId") Long reviewId); + Long countByReviewId(Long reviewId); - @Modifying - @Query("delete from LikeReview l where l.reviewId = :reviewId") - void deleteAllByReview(@Param("reviewId") Long reviewId); + void deleteByReviewId(Long reviewId); - @Modifying - @Query("delete from LikeReview l where l.userId = :userId and l.reviewId = :reviewId") - int deleteByUserIdAndReviewId(@Param("userId") Long userId, - @Param("reviewId") Long reviewId); + void deleteByUserIdAndReviewId(Long userId, Long reviewId); boolean existsByUserIdAndReviewId(Long userId, Long reviewId); - @Modifying - @Query("delete from LikeReview l where l.reviewId in :reviewIds") - void deleteAllByReviews(@Param("reviewIds") List reviewIds); + void deleteByReviewIdIn(@Param("reviewIds") List reviewIds); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java index 0c18359..4200ba5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewJpaRepository.java @@ -2,12 +2,9 @@ import java.util.List; import java.util.Optional; -import java.util.Set; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; @@ -15,35 +12,11 @@ public interface ReviewJpaRepository extends ReviewRepository, JpaRepository { //review find service - 리뷰 단순 조회 - @Query("select distinct r from Review r " + - "left outer join fetch r.photos " + - "where r.id = :id") - Optional findWithPhotosById(@Param("id") Long id); + @EntityGraph(attributePaths = "photos") + Optional findWithPhotosById(Long id); //회원 탈퇴를 위한 사용자가 작성한 리뷰 조회 - @Query("select distinct r from Review r " + - "left outer join fetch r.photos " + - "where r.writer.id = :userId") - List findWithPhotosByUserId(@Param("userId") Long userId); + List findWithPhotosByWriterId(Long writerId); - @Modifying - @Query("delete from Review r where r.id in :reviewIds") - void deleteAllByWriter(@Param("reviewIds") List reviewIds); - - @Query("select case " - + "when l.reviewId is null then 0 " - + "else count(r.id) end " - + "from Review r " - + "left join LikeReview l on r.id = l.reviewId " - + "where r.id in :reviewIds " - + "group by r.id " - + "order by r.id") - List countLikeReviewByIdsOrderById(@Param("reviewIds") Set reviewIds); - - @Modifying - @Query("update Review r SET r.likeCount = :likeCount where r.id = :id") - void updateLikeCountById( - @Param("id") Long id, - @Param("likeCount") int likeCount - ); + void deleteByIdIn(List reviewIds); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java index 62a38dd..9019425 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infrastructure/ReviewQueryDslRepository.java @@ -53,12 +53,10 @@ public Slice findReviewByPaging(Pageable pageable) { List result = factory .select(review) .from(review) - .leftJoin(like).on(like.reviewId.eq(review.id)) .leftJoin(review.hairStyle) .fetchJoin() .leftJoin(review.writer) .fetchJoin() - .groupBy(review.id) .orderBy(applyOrderBy(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1L) @@ -69,22 +67,16 @@ public Slice findReviewByPaging(Pageable pageable) { @Override public Slice findReviewByLike(Long userId, Pageable pageable) { - List filteredReviewId = factory - .select(review.id) - .from(review) - .leftJoin(like).on(review.id.eq(like.reviewId)) - .where(like.userId.eq(userId)) - .groupBy(review.id) - .fetch(); - List result = factory .select(review) .from(review) + .leftJoin(like).on(review.id.eq(like.reviewId)) .leftJoin(review.writer) .fetchJoin() .leftJoin(review.hairStyle) .fetchJoin() - .where(review.id.in(filteredReviewId)) + .where(like.userId.eq(userId)) + .groupBy(review.id) .orderBy(review.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1L) @@ -124,7 +116,6 @@ public List findReviewByCreatedDate() { .leftJoin(review.writer) .fetchJoin() .where(review.createdDate.between(startDate, endDate)) - .orderBy(review.likeCount.desc()) .offset(0) .limit(4) .fetch(); @@ -140,7 +131,6 @@ public List findReviewByHairStyle(Long hairStyleId) { .leftJoin(review.writer) .fetchJoin() .where(review.hairStyle.id.eq(hairStyleId)) - .orderBy(review.likeCount.desc()) .offset(0) .limit(4) .fetch(); @@ -150,13 +140,10 @@ private OrderSpecifier[] applyOrderBy(Pageable pageable) { List> orderBy = new LinkedList<>(); String sort = pageable.getSort().toString().replace(": ", "."); - switch (sort) { - case LIKES_DESC -> { - orderBy.add(review.likeCount.desc()); - orderBy.add(review.id.desc()); - } - case DATE_DESC -> orderBy.add(review.id.desc()); - case DATE_ASC -> orderBy.add(review.id.asc()); + if (sort.equals(DATE_ASC)) { + orderBy.add(review.id.asc()); + } else { + orderBy.add(review.id.desc()); } return orderBy.toArray(OrderSpecifier[]::new); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java index 1f5b077..e66248f 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java @@ -5,8 +5,9 @@ import com.inq.wishhair.wesharewishhair.global.config.QueryDslConfig; import com.inq.wishhair.wesharewishhair.hairstyle.infrastructure.query.HairStyleQueryDslRepository; +import com.inq.wishhair.wesharewishhair.review.infrastructure.ReviewQueryDslRepository; -@Import({QueryDslConfig.class, HairStyleQueryDslRepository.class}) +@Import({QueryDslConfig.class, HairStyleQueryDslRepository.class, ReviewQueryDslRepository.class}) @DataJpaTest public abstract class RepositoryTestSupport { } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepositoryTest.java new file mode 100644 index 0000000..02602be --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/domain/ReviewQueryRepositoryTest.java @@ -0,0 +1,159 @@ +package com.inq.wishhair.wesharewishhair.review.domain; + +import static com.inq.wishhair.wesharewishhair.global.utils.PageableGenerator.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Slice; + +import com.inq.wishhair.wesharewishhair.common.support.RepositoryTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReview; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[ReviewQueryRepository 테스트]") +class ReviewQueryRepositoryTest extends RepositoryTestSupport { + + @Autowired + private ReviewRepository reviewRepository; + @Autowired + private ReviewQueryRepository reviewQueryRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private HairStyleRepository hairStyleRepository; + @Autowired + private LikeReviewRepository likeReviewRepository; + + private List reviews; + private User user; + private HairStyle hairStyle; + + @BeforeEach + void setUp() { + user = userRepository.save(UserFixture.getFixedWomanUser()); + hairStyle = hairStyleRepository.save(HairStyleFixture.getWomanHairStyle()); + reviews = List.of( + ReviewFixture.getReview(hairStyle, user), + ReviewFixture.getReview(hairStyle, user), + ReviewFixture.getReview(hairStyle, user) + ); + + reviews.forEach(review -> reviewRepository.save(review)); + } + + @Test + @DisplayName("[아이디로 리뷰를 조회한다(hairstyle, user 조인)]") + void findReviewById() { + //given + Review review = reviews.get(0); + + //when + Optional actual = reviewQueryRepository.findReviewById(review.getId()); + + //then + assertThat(actual).contains(review); + } + + @Nested + @DisplayName("[전체 리뷰를 페이징 조건에 따라 조회한다]") + class findReviewByPaging { + + @Test + @DisplayName("[오래된순으로 정렬한다]") + void orderDateAsc() { + //when + Slice actual = reviewQueryRepository.findReviewByPaging(generateDateAscPageable(3)); + + //then + assertThat(actual.hasNext()).isFalse(); + assertThat(actual.getContent()).hasSize(3); + assertThat(actual.getContent()).containsExactly(reviews.toArray(Review[]::new)); + } + + @Test + @DisplayName("[최신순으로 정렬한다]") + void orderDateDesc() { + Slice actual = reviewQueryRepository.findReviewByPaging(generateDateDescPageable(3)); + + //then + assertThat(actual.hasNext()).isFalse(); + assertThat(actual.getContent()).hasSize(3); + + Review[] expected = reviews.stream() + .sorted((a, b) -> Long.compare(b.getId(), a.getId())) + .toArray(Review[]::new); + assertThat(actual.getContent()).containsExactly(expected); + } + } + + @Test + @DisplayName("[좋아요한 리뷰를 조회한다]") + void findReviewByLike() { + //given + likeReviewRepository.save( + LikeReview.addLike(user.getId(), reviews.get(0).getId()) + ); + likeReviewRepository.save( + LikeReview.addLike(user.getId(), reviews.get(1).getId()) + ); + + //when + Slice actual = reviewQueryRepository.findReviewByLike(user.getId(), getDefaultPageable()); + + //then + assertThat(actual.hasNext()).isFalse(); + assertThat(actual.getContent()).hasSize(2); + assertThat(actual).containsExactly(reviews.get(1), reviews.get(0)); + } + + @Test + @DisplayName("[특정 작성자의 리뷰를 조회한다]") + void findReviewByUser() { + //when + Slice actual = reviewQueryRepository.findReviewByUser(user.getId(), getDefaultPageable()); + + //then + assertThat(actual.hasNext()).isFalse(); + assertThat(actual.getContent()).hasSize(3); + + Review[] expected = reviews.stream() + .sorted((a, b) -> Long.compare(b.getId(), a.getId())) + .toArray(Review[]::new); + assertThat(actual.getContent()).containsExactly(expected); + } + + @Test + @DisplayName("[지난달에 작성된 리뷰를 조회한다]") + void findReviewByCreatedDate() { + //when + List actual = reviewQueryRepository.findReviewByCreatedDate(); + + //then + assertThat(actual).isEmpty(); + } + + @Test + @DisplayName("[특정 헤어스타일의 리뷰를 조회한다]") + void findReviewByHairStyle() { + //when + List actual = reviewQueryRepository.findReviewByHairStyle(hairStyle.getId()); + + //then + assertThat(actual).containsExactly(reviews.toArray(Review[]::new)); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/domain/entity/ReviewTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/domain/entity/ReviewTest.java new file mode 100644 index 0000000..6eb7ba5 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/domain/entity/ReviewTest.java @@ -0,0 +1,108 @@ +package com.inq.wishhair.wesharewishhair.review.domain.entity; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.photo.domain.Photo; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[Review 테스트]") +class ReviewTest { + + @Nested + @DisplayName("[Review 를 생성한다]") + class createReview { + + @Test + @DisplayName("[성공적으로 생성한다]") + void success() { + //given + User user = UserFixture.getFixedManUser(); + String contents = "contents"; + Score score = Score.S2H; + List photoUrls = List.of("url1", "url2"); + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + + //when + Review actual = Review.createReview( + user, + contents, + score, + photoUrls, + hairStyle + ); + + //then + assertAll( + () -> assertThat(actual.getContentsValue()).isEqualTo(contents), + () -> assertThat(actual.getScore()).isEqualTo(score), + () -> assertThat(actual.getWriter()).isEqualTo(user), + () -> assertThat(actual.getHairStyle()).isEqualTo(hairStyle) + ); + List imageUrls = actual.getPhotos().stream().map(Photo::getStoreUrl).toList(); + assertThat(imageUrls).containsAll(photoUrls); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"fail", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) + @DisplayName("[Contents 의 길이가 부적합해서 실패한다]") + void fail1(String contents) { + //given + User user = UserFixture.getFixedManUser(); + Score score = Score.S2H; + List photoUrls = List.of("url1"); + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + + //when + ThrowingCallable when = () -> Review.createReview( + user, + contents, + score, + photoUrls, + hairStyle + ); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.CONTENTS_INVALID_LENGTH.getMessage()); + } + } + + @Test + @DisplayName("[Review 를 업데이트한다]") + void updateReview() { + //given + Contents contents = new Contents("contents"); + Score score = Score.S1; + List photoUrls = List.of("url1", "url2"); + + Review review = ReviewFixture.getReview(); + + //when + review.updateReview(contents, score, photoUrls); + + //then + assertAll( + () -> assertThat(review.getContents()).isEqualTo(contents), + () -> assertThat(review.getScore()).isEqualTo(score) + ); + List imageUrls = review.getPhotos().stream().map(Photo::getStoreUrl).toList(); + assertThat(imageUrls).containsAll(photoUrls); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java index 434ee5f..f25afd8 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -1,8 +1,15 @@ package com.inq.wishhair.wesharewishhair.review.fixture; +import java.util.List; + import org.springframework.test.util.ReflectionTestUtils; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -10,9 +17,38 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ReviewFixture { + private static final List URLS = List.of("url1", "url2"); + private static final String CONTENTS = "contents"; + public static Review getEmptyReview(Long id) { Review review = new Review(); ReflectionTestUtils.setField(review, "id", id); return review; } + + public static Review getReview() { + User user = UserFixture.getFixedManUser(); + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + + return Review.createReview( + user, + CONTENTS, + Score.S2H, + URLS, + hairStyle + ); + } + + public static Review getReview( + HairStyle hairStyle, + User user + ) { + return Review.createReview( + user, + CONTENTS, + Score.S2H, + URLS, + hairStyle + ); + } } From e2001dbbc1d91fee16f6eb32b5803fb8e25146d1 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Mon, 13 Nov 2023 16:31:32 +0900 Subject: [PATCH 27/30] =?UTF-8?q?test:=20RedisUtils=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/utils/RedisUtils.java | 3 - .../global/utils/RedisUtilsTest.java | 81 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtilsTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java index eebc0c9..be7bc8b 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java @@ -7,10 +7,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; -import lombok.extern.slf4j.Slf4j; - @Component -@Slf4j public class RedisUtils { private final RedisTemplate redisTemplate; diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtilsTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtilsTest.java new file mode 100644 index 0000000..2f44893 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtilsTest.java @@ -0,0 +1,81 @@ +package com.inq.wishhair.wesharewishhair.global.utils; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; + +import com.inq.wishhair.wesharewishhair.common.config.EmbeddedRedisConfig; + +/** + * 주의사항 : 각 테스트 케이스별로 저장하는 데이터를 다르게 해야함(레디스 서버는 복구가 안되기 때문) + */ +@SpringBootTest +@Import(EmbeddedRedisConfig.class) +@DisplayName("[RedisUtils 테스트]") +class RedisUtilsTest { + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private RedisTemplate redisTemplate; + + @Test + @DisplayName("[Redis 서버에 데이터를 저장한다]") + void setData() { + //when + redisUtils.setData(1L, 10L); + + //then + Long expected = redisTemplate.opsForValue().get(String.valueOf(1L)); + assertThat(expected).isEqualTo(10L); + } + + @Test + @DisplayName("[key 해당하는 값을 1 증가 시킨다]") + void increaseData() { + //given + redisUtils.setData(2L, 10L); + + //when + redisUtils.increaseData(2L); + + //then + Long expected = redisTemplate.opsForValue().get(String.valueOf(2L)); + assertThat(expected).isEqualTo(11L); + } + + @Test + @DisplayName("[key 해당하는 값을 1 감소 시킨다]") + void decreaseData() { + //given + redisUtils.setData(3L, 10L); + + //when + redisUtils.decreaseData(3L); + + //then + Long expected = redisTemplate.opsForValue().get(String.valueOf(3L)); + assertThat(expected).isEqualTo(9L); + } + + @Test + @DisplayName("[key 해당하는 값을 가져온다]") + void getData() { + //given + redisUtils.setData(4L, 10L); + + //when + Optional actual = redisUtils.getData(4L); + + //then + assertThat(actual).contains(10L); + } +} \ No newline at end of file From 33ec3dc980849183761d2618a07580098a526b1b Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Tue, 14 Nov 2023 15:41:34 +0900 Subject: [PATCH 28/30] =?UTF-8?q?test:=20Review=20Application=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/LikeReviewService.java | 1 - .../review/application/ReviewFindService.java | 5 - .../application/ReviewSearchService.java | 10 +- .../review/application/ReviewService.java | 68 +++-- .../dto/request/ReviewCreateRequest.java | 20 ++ .../dto/request/ReviewUpdateRequest.java | 20 ++ .../dto/response/ReviewSimpleResponse.java | 4 +- .../presentation/LikeReviewController.java | 12 +- .../review/presentation/ReviewController.java | 16 +- .../presentation/ReviewSearchController.java | 16 +- .../dto/request/ReviewCreateRequest.java | 27 -- .../dto/request/ReviewUpdateRequest.java | 30 -- .../wishhair/wesharewishhair/LikeTest.java | 28 +- .../point/application/PointServiceTest.java | 8 +- .../application/LikeReviewServiceTest.java | 194 +++++++++++++ .../application/ReviewFindServiceTest.java | 83 ++++++ .../application/ReviewSearchServiceTest.java | 266 ++++++++++++++++++ .../review/application/ReviewServiceTest.java | 193 +++++++++++++ .../review/fixture/ReviewFixture.java | 42 ++- .../user/fixture/UserFixture.java | 2 +- .../templates/path-parameters.snippet | 9 - .../restdocs/templates/request-fields.snippet | 11 - .../templates/request-headers.snippet | 8 - .../templates/request-parameters.snippet | 10 - .../templates/response-fields.snippet | 10 - 25 files changed, 886 insertions(+), 207 deletions(-) create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewCreateRequest.java create mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewCreateRequest.java delete mode 100644 src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewUpdateRequest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchServiceTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewServiceTest.java delete mode 100644 src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet delete mode 100644 src/test/resources/org/springframework/restdocs/templates/request-fields.snippet delete mode 100644 src/test/resources/org/springframework/restdocs/templates/request-headers.snippet delete mode 100644 src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet delete mode 100644 src/test/resources/org/springframework/restdocs/templates/response-fields.snippet diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java index 694e7b5..acb759e 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java @@ -73,5 +73,4 @@ public List getLikeCounts(List reviewIds) { .map(this::getLikeCount) .toList(); } - } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java index fe6aeae..45acc51 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindService.java @@ -17,11 +17,6 @@ public class ReviewFindService { private final ReviewRepository reviewRepository; - public Review getById(Long id) { - return reviewRepository.findById(id) - .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); - } - public Review findWithPhotosById(Long id) { return reviewRepository.findWithPhotosById(id) .orElseThrow(() -> new WishHairException(ErrorCode.NOT_EXIST_KEY)); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java index 65f1b60..9d1ba24 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java @@ -30,6 +30,11 @@ public class ReviewSearchService { private final ReviewQueryRepository reviewQueryRepository; private final LikeReviewService likeReviewService; + private List fetchLikeCounts(List result) { + List reviewIds = result.stream().map(Review::getId).toList(); + return likeReviewService.getLikeCounts(reviewIds); + } + /*리뷰 단건 조회*/ @AddisWriter public ReviewDetailResponse findReviewById(Long userId, Long reviewId) { @@ -87,9 +92,4 @@ public ResponseWrapper findReviewByHairStyle(Long userId, Long h return toWrappedReviewResponse(result, likeCounts); } - - private List fetchLikeCounts(List result) { - List reviewIds = result.stream().map(Review::getId).toList(); - return likeReviewService.getLikeCounts(reviewIds); - } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java index 629dbbd..e97bd0b 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewService.java @@ -12,8 +12,8 @@ import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.application.HairStyleFindService; import com.inq.wishhair.wesharewishhair.photo.application.PhotoService; -import com.inq.wishhair.wesharewishhair.review.presentation.dto.request.ReviewCreateRequest; -import com.inq.wishhair.wesharewishhair.review.presentation.dto.request.ReviewUpdateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewCreateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewUpdateRequest; import com.inq.wishhair.wesharewishhair.review.domain.entity.Contents; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; @@ -37,12 +37,33 @@ public class ReviewService { private final HairStyleFindService hairStyleFindService; private final ApplicationEventPublisher eventPublisher; + private void validateIsWriter(Long userId, Review review) { + if (!review.isWriter(userId)) { + throw new WishHairException(ErrorCode.REVIEW_NOT_WRITER); + } + } + + private List refreshPhotos(Review review, List files) { + photoService.deletePhotosByReviewId(review); + return photoService.uploadPhotos(files); + } + + private Review generateReview(ReviewCreateRequest request, List photos, User user, HairStyle hairStyle) { + return Review.createReview( + user, + request.contents(), + request.score(), + photos, + hairStyle + ); + } + @Transactional public Long createReview(ReviewCreateRequest request, Long userId) { - List photoUrls = photoService.uploadPhotos(request.getFiles()); + List photoUrls = photoService.uploadPhotos(request.files()); User user = userFindService.findByUserId(userId); - HairStyle hairStyle = hairStyleFindService.getById(request.getHairStyleId()); + HairStyle hairStyle = hairStyleFindService.getById(request.hairStyleId()); Review review = generateReview(request, photoUrls, user, hairStyle); eventPublisher.publishEvent(new PointChargeEvent(100, userId)); @@ -51,53 +72,38 @@ public Long createReview(ReviewCreateRequest request, Long userId) { } @Transactional - public void deleteReview(Long reviewId, Long userId) { + public boolean deleteReview(Long reviewId, Long userId) { Review review = reviewFindService.findWithPhotosById(reviewId); validateIsWriter(userId, review); likeReviewRepository.deleteByReviewId(reviewId); photoService.deletePhotosByReviewId(review); reviewRepository.delete(review); + + return true; } @Transactional - public void updateReview(ReviewUpdateRequest request, Long userId) { - Review review = reviewFindService.findWithPhotosById(request.getReviewId()); + public boolean updateReview(ReviewUpdateRequest request, Long userId) { + Review review = reviewFindService.findWithPhotosById(request.reviewId()); validateIsWriter(userId, review); - Contents contents = new Contents(request.getContents()); - List storeUrls = refreshPhotos(review, request.getFiles()); - review.updateReview(contents, request.getScore(), storeUrls); + Contents contents = new Contents(request.contents()); + List storeUrls = refreshPhotos(review, request.files()); + review.updateReview(contents, request.score(), storeUrls); + + return true; } @Transactional - public void deleteReviewByWriter(Long userId) { + public boolean deleteReviewByWriter(Long userId) { List reviews = reviewFindService.findWithPhotosByUserId(userId); List reviewIds = reviews.stream().map(Review::getId).toList(); likeReviewRepository.deleteByReviewIdIn(reviewIds); photoService.deletePhotosByWriter(reviews); reviewRepository.deleteByIdIn(reviewIds); - } - - private void validateIsWriter(Long userId, Review review) { - if (!review.isWriter(userId)) { - throw new WishHairException(ErrorCode.REVIEW_NOT_WRITER); - } - } - private List refreshPhotos(Review review, List files) { - photoService.deletePhotosByReviewId(review); - return photoService.uploadPhotos(files); - } - - private Review generateReview(ReviewCreateRequest request, List photos, User user, HairStyle hairStyle) { - return Review.createReview( - user, - request.getContents(), - request.getScore(), - photos, - hairStyle - ); + return true; } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewCreateRequest.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewCreateRequest.java new file mode 100644 index 0000000..e99982a --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewCreateRequest.java @@ -0,0 +1,20 @@ +package com.inq.wishhair.wesharewishhair.review.application.dto.request; + +import java.util.List; + +import org.springframework.web.multipart.MultipartFile; + +import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; + +import jakarta.validation.constraints.NotNull; + +public record ReviewCreateRequest( + @NotNull + String contents, + @NotNull + Score score, + List files, + @NotNull + Long hairStyleId +) { +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java new file mode 100644 index 0000000..1e273e4 --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java @@ -0,0 +1,20 @@ +package com.inq.wishhair.wesharewishhair.review.application.dto.request; + +import java.util.List; + +import org.springframework.web.multipart.MultipartFile; + +import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; + +import jakarta.validation.constraints.NotNull; + +public record ReviewUpdateRequest( + @NotNull + Long reviewId, + @NotNull + String contents, + @NotNull + Score score, + List files +) { +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewSimpleResponse.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewSimpleResponse.java index c2c0e98..27d432b 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewSimpleResponse.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/response/ReviewSimpleResponse.java @@ -11,8 +11,8 @@ public record ReviewSimpleResponse( public ReviewSimpleResponse(Review review) { this( review.getId(), - review.getContentsValue(), - review.getContentsValue(), + review.getWriter().getNicknameValue(), + review.getHairStyle().getName(), review.getContentsValue() ); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java index 3cc4d72..27edb82 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java @@ -25,8 +25,8 @@ public class LikeReviewController { @PostMapping(path = "{reviewId}") public ResponseEntity executeLike( - final @PathVariable Long reviewId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long reviewId, + @FetchAuthInfo AuthInfo authInfo ) { likeReviewService.executeLike(reviewId, authInfo.userId()); @@ -35,8 +35,8 @@ public ResponseEntity executeLike( @DeleteMapping("/{reviewId}") public ResponseEntity cancelLike( - final @PathVariable Long reviewId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long reviewId, + @FetchAuthInfo AuthInfo authInfo ) { likeReviewService.cancelLike(reviewId, authInfo.userId()); @@ -45,8 +45,8 @@ public ResponseEntity cancelLike( @GetMapping(path = "{reviewId}") public ResponseEntity checkIsLiking( - final @FetchAuthInfo AuthInfo authInfo, - final @PathVariable Long reviewId + @FetchAuthInfo AuthInfo authInfo, + @PathVariable Long reviewId ) { LikeReviewResponse result = likeReviewService.checkIsLiking(authInfo.userId(), reviewId); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewController.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewController.java index ad1a79b..bdc3a41 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewController.java @@ -14,8 +14,8 @@ import com.inq.wishhair.wesharewishhair.global.annotation.FetchAuthInfo; import com.inq.wishhair.wesharewishhair.global.dto.response.Success; import com.inq.wishhair.wesharewishhair.global.resolver.dto.AuthInfo; -import com.inq.wishhair.wesharewishhair.review.presentation.dto.request.ReviewCreateRequest; -import com.inq.wishhair.wesharewishhair.review.presentation.dto.request.ReviewUpdateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewCreateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewUpdateRequest; import com.inq.wishhair.wesharewishhair.review.application.ReviewService; import lombok.RequiredArgsConstructor; @@ -29,8 +29,8 @@ public class ReviewController { @PostMapping public ResponseEntity createReview( - final @ModelAttribute ReviewCreateRequest reviewCreateRequest, - final @FetchAuthInfo AuthInfo authInfo + @ModelAttribute ReviewCreateRequest reviewCreateRequest, + @FetchAuthInfo AuthInfo authInfo ) { Long reviewId = reviewService.createReview(reviewCreateRequest, authInfo.userId()); @@ -41,8 +41,8 @@ public ResponseEntity createReview( @DeleteMapping(path = "{reviewId}") public ResponseEntity deleteReview( - final @FetchAuthInfo AuthInfo authInfo, - final @PathVariable Long reviewId + @FetchAuthInfo AuthInfo authInfo, + @PathVariable Long reviewId ) { reviewService.deleteReview(reviewId, authInfo.userId()); @@ -51,8 +51,8 @@ public ResponseEntity deleteReview( @PatchMapping public ResponseEntity updateReview( - final @ModelAttribute ReviewUpdateRequest request, - final @FetchAuthInfo AuthInfo authInfo + @ModelAttribute ReviewUpdateRequest request, + @FetchAuthInfo AuthInfo authInfo ) { reviewService.updateReview(request, authInfo.userId()); diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchController.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchController.java index c7c3583..8661201 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchController.java @@ -31,8 +31,8 @@ public class ReviewSearchController { @GetMapping(path = "{reviewId}") public ResponseEntity findReview( - final @PathVariable Long reviewId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long reviewId, + @FetchAuthInfo AuthInfo authInfo ) { ReviewDetailResponse result = reviewSearchService.findReviewById(authInfo.userId(), reviewId); @@ -41,8 +41,8 @@ public ResponseEntity findReview( @GetMapping public ResponseEntity> findPagingReviews( - final @PageableDefault(sort = LIKES, direction = Sort.Direction.DESC) Pageable pageable, - final @FetchAuthInfo AuthInfo authInfo + @PageableDefault(sort = LIKES, direction = Sort.Direction.DESC) Pageable pageable, + @FetchAuthInfo AuthInfo authInfo ) { PagedResponse result = reviewSearchService.findPagedReviews(authInfo.userId(), pageable); @@ -51,8 +51,8 @@ public ResponseEntity> findPagingReviews( @GetMapping("/my") public ResponseEntity> findMyReviews( - final @PageableDefault(sort = DATE, direction = Sort.Direction.DESC) Pageable pageable, - final @FetchAuthInfo AuthInfo authInfo + @PageableDefault(sort = DATE, direction = Sort.Direction.DESC) Pageable pageable, + @FetchAuthInfo AuthInfo authInfo ) { PagedResponse result = reviewSearchService.findMyReviews(authInfo.userId(), pageable); @@ -66,8 +66,8 @@ public ResponseWrapper findReviewOfMonth() { @GetMapping("/hair_style/{hairStyleId}") public ResponseWrapper findHairStyleReview( - final @PathVariable Long hairStyleId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long hairStyleId, + @FetchAuthInfo AuthInfo authInfo ) { return reviewSearchService.findReviewByHairStyle(authInfo.userId(), hairStyleId); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewCreateRequest.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewCreateRequest.java deleted file mode 100644 index 703a3df..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewCreateRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.inq.wishhair.wesharewishhair.review.presentation.dto.request; - -import java.util.List; - -import org.springframework.web.multipart.MultipartFile; - -import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class ReviewCreateRequest { - - @NotNull - private String contents; - - @NotNull - private Score score; - - private List files; - - @NotNull - private Long hairStyleId; -} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewUpdateRequest.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewUpdateRequest.java deleted file mode 100644 index ce84202..0000000 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/dto/request/ReviewUpdateRequest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.inq.wishhair.wesharewishhair.review.presentation.dto.request; - -import java.util.List; - -import org.springframework.web.multipart.MultipartFile; - -import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; - -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class ReviewUpdateRequest { - - @NotNull - private Long reviewId; - - @NotNull - private String contents; - - @NotNull - private Score score; - - private List files; -} diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java index cda02c8..1dfacf4 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java @@ -24,34 +24,8 @@ class LikeTest { @Autowired private LikeReviewService likeReviewService; - // @Test - // @DisplayName("[좋아요 정보가 Redis 에 없을 때, 동시적 요청이 들어오면 동시성 이슈가 발생한다]") - // void fail() throws InterruptedException { - // //given - // int threadCount = 100; - // ExecutorService service = Executors.newFixedThreadPool(threadCount); - // CountDownLatch latch = new CountDownLatch(threadCount); - // - // //when - // for (int i = 0; i < threadCount; i++) { - // service.execute(() -> { - // Random random = new Random(); - // int id = random.nextInt(Integer.MAX_VALUE); - // - // likeReviewService.executeLike(1L, (long)id); - // latch.countDown(); - // }); - // } - // - // latch.await(); - // - // //then -> 동시성 이슈 발생으로 100개의 좋아요 보다 적은 likeCount - // Long likeCount = likeReviewService.getLikeCount(1L); - // assertThat(likeCount).isLessThan(100L); - // } - @Test - @DisplayName("[좋아요 정보가 Redis 에 있을 때에는 동시성 이슈가 발생하지 않는다.]") + @DisplayName("[100개의 좋아요 동시 요청 모두 반영한다]") void success() throws InterruptedException { //given int threadCount = 100; diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java index 80c106b..6dc12cc 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/application/PointServiceTest.java @@ -47,7 +47,7 @@ class usePoint { @DisplayName("[성공적으로 사용한다]") void success() { //given - User user = UserFixture.getManUserWithId(1L); + User user = UserFixture.getFixedManUser(1L); given(userFindService.findByUserId(user.getId())) .willReturn(user); @@ -67,7 +67,7 @@ void success() { @DisplayName("[이전 PointLog 가 없어서 실패한다]") void fail() { //given - User user = UserFixture.getManUserWithId(1L); + User user = UserFixture.getFixedManUser(1L); given(userFindService.findByUserId(user.getId())) .willReturn(user); @@ -92,7 +92,7 @@ class chargePoint { @DisplayName("[성공적으로 충전한다]") void success1() { //given - User user = UserFixture.getManUserWithId(1L); + User user = UserFixture.getFixedManUser(1L); given(userFindService.findByUserId(user.getId())) .willReturn(user); @@ -111,7 +111,7 @@ void success1() { @DisplayName("[이전 PointLog 가 없어서 0원에서 시작한 PointLog 를 만든다]") void success2() { //given - User user = UserFixture.getManUserWithId(1L); + User user = UserFixture.getFixedManUser(1L); given(userFindService.findByUserId(user.getId())) .willReturn(user); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewServiceTest.java new file mode 100644 index 0000000..ecb7ee4 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewServiceTest.java @@ -0,0 +1,194 @@ +package com.inq.wishhair.wesharewishhair.review.application; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; +import com.inq.wishhair.wesharewishhair.global.utils.RedisUtils; +import com.inq.wishhair.wesharewishhair.review.application.dto.response.LikeReviewResponse; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReview; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; + +import jakarta.persistence.EntityExistsException; + +@DisplayName("[LikeReviewService 테스트]") +class LikeReviewServiceTest extends MockTestSupport { + + @InjectMocks + private LikeReviewService likeReviewService; + @Mock + private LikeReviewRepository likeReviewRepository; + @Mock + private RedisUtils redisUtils; + + @Nested + @DisplayName("[좋아요를 실행한다]") + class executeLike { + + @Nested + @DisplayName("[성공적으로 좋아요 한다]") + class returnTrue { + + @Test + @DisplayName("[레디스에 좋아요 정보가 있어서 기존 값을 증가시킨다]") + void success1() { + //given + given(redisUtils.getData(1L)) + .willReturn(Optional.of(1L)); + + //when + boolean actual = likeReviewService.executeLike(1L, 1L); + + //then + assertThat(actual).isTrue(); + verify(redisUtils, timeout(1)).increaseData(1L); + } + + @Test + @DisplayName("[레디스에 좋아요 정보가 없어서 새로 값을 세팅한다]") + void success2() { + //given + given(redisUtils.getData(1L)) + .willReturn(Optional.empty()); + + given(likeReviewRepository.countByReviewId(1L)) + .willReturn(10L); + + //when + boolean actual = likeReviewService.executeLike(1L, 1L); + + //then + assertThat(actual).isTrue(); + verify(redisUtils, timeout(1)).setData(1L, 10L); + } + } + + @Test + @DisplayName("[이미 좋아요한 상태여서 false 를 반환한다]") + void returnFalse() { + given(likeReviewRepository.save(any(LikeReview.class))) + .willThrow(new EntityExistsException()); + + //when + boolean actual = likeReviewService.executeLike(1L, 1L); + + //then + assertThat(actual).isFalse(); + } + } + + @Nested + @DisplayName("[좋아요를 취소한다]") + class cancelLike { + + @Test + @DisplayName("[레디스에 좋아요 정보가 있어서 기존 값을 감소시킨다]") + void success1() { + //given + given(redisUtils.getData(1L)) + .willReturn(Optional.of(1L)); + + //when + boolean actual = likeReviewService.cancelLike(1L, 1L); + + //then + assertThat(actual).isTrue(); + verify(redisUtils, timeout(1)).decreaseData(1L); + } + + @Test + @DisplayName("[레디스에 좋아요 정보가 없어서 새로운 값을 세팅한다]") + void success2() { + //given + given(redisUtils.getData(1L)) + .willReturn(Optional.empty()); + + given(likeReviewRepository.countByReviewId(1L)) + .willReturn(10L); + + //when + boolean actual = likeReviewService.cancelLike(1L, 1L); + + //then + assertThat(actual).isTrue(); + verify(redisUtils, timeout(1)).setData(1L, 10L); + } + } + + @Test + @DisplayName("[리뷰를 사용자가 좋아요한 상태인지 확인한다]") + void checkIsLiking() { + //given + given(likeReviewRepository.existsByUserIdAndReviewId(1L, 1L)) + .willReturn(true); + + //when + LikeReviewResponse actual = likeReviewService.checkIsLiking(1L, 1L); + + //then + assertThat(actual.isLiking()).isTrue(); + } + + @Nested + @DisplayName("[좋아요 개수를 조회한다]") + class getLikeCount { + + @Test + @DisplayName("[레디스에 좋아요 정보가 있어서 기존 값을 가져온다]") + void success1() { + //given + given(redisUtils.getData(1L)) + .willReturn(Optional.of(10L)); + + //when + Long actual = likeReviewService.getLikeCount(1L); + + //then + assertThat(actual).isEqualTo(10L); + } + + @Test + @DisplayName("[레디스에 좋아요 정보가 없어서 새로운 값을 세팅 후 가져온다]") + void success2() { + //given + given(redisUtils.getData(1L)) + .willReturn(Optional.empty()); + + given(likeReviewRepository.countByReviewId(1L)) + .willReturn(10L); + + //when + Long actual = likeReviewService.getLikeCount(1L); + + //then + assertThat(actual).isEqualTo(10L); + verify(redisUtils, timeout(1)).setData(1L, 10L); + } + } + + @Test + @DisplayName("[다수의 리뷰의 좋아요 개수들을 조회한다]") + void getLikeCounts() { + //given + List reviewIds = List.of(1L, 2L); + + reviewIds.forEach(reviewId -> + given(redisUtils.getData(reviewId)).willReturn(Optional.of(10L)) + ); + + //when + List actual = likeReviewService.getLikeCounts(reviewIds); + + //then + assertThat(actual).contains(10L, 10L); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindServiceTest.java new file mode 100644 index 0000000..979e1ec --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewFindServiceTest.java @@ -0,0 +1,83 @@ +package com.inq.wishhair.wesharewishhair.review.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; + +@DisplayName("[ReviewFindService 테스트]") +class ReviewFindServiceTest extends MockTestSupport { + + @InjectMocks + private ReviewFindService reviewFindService; + @Mock + private ReviewRepository reviewRepository; + + @Nested + @DisplayName("[아이디로 리뷰를 조회한다(photo 조인)]") + class findWithPhotosById { + + @Test + @DisplayName("[성공적으로 조회한다]") + void success() { + //given + Review review = ReviewFixture.getReview(); + given(reviewRepository.findWithPhotosById(1L)) + .willReturn(Optional.of(review)); + + //when + Review actual = reviewFindService.findWithPhotosById(1L); + + //then + assertThat(actual).isEqualTo(review); + } + + @Test + @DisplayName("[아이디에 해당하는 리뷰가 존재하지 않아 실패한다]") + void fail() { + //given + given(reviewRepository.findWithPhotosById(1L)) + .willReturn(Optional.empty()); + + //when + ThrowingCallable when = () -> reviewFindService.findWithPhotosById(1L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.NOT_EXIST_KEY.getMessage()); + } + } + + @Test + @DisplayName("[]") + void findWithPhotosByUserId() { + //given + List reviews = List.of(ReviewFixture.getReview()); + given(reviewRepository.findWithPhotosByWriterId(1L)) + .willReturn(reviews); + + //when + List actual = reviewFindService.findWithPhotosByUserId(1L); + + //then + assertThat(actual) + .hasSameSizeAs(reviews) + .containsAll(reviews); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchServiceTest.java new file mode 100644 index 0000000..8e075b0 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchServiceTest.java @@ -0,0 +1,266 @@ +package com.inq.wishhair.wesharewishhair.review.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; +import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; +import com.inq.wishhair.wesharewishhair.global.dto.response.Paging; +import com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.hairstyle.application.dto.response.HashTagResponse; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.photo.application.dto.response.PhotoInfo; +import com.inq.wishhair.wesharewishhair.review.application.dto.response.LikeReviewResponse; +import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewDetailResponse; +import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewResponse; +import com.inq.wishhair.wesharewishhair.review.application.dto.response.ReviewSimpleResponse; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewQueryRepository; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[ReviewSearchService 테스트]") +class ReviewSearchServiceTest extends MockTestSupport { + + @InjectMocks + private ReviewSearchService reviewSearchService; + @Mock + private ReviewQueryRepository reviewQueryRepository; + @Mock + private LikeReviewService likeReviewService; + + private void assertReviewResponse( + ReviewResponse response, + Review review, + Long likeCount + ) { + assertAll( + () -> assertThat(response.getReviewId()).isEqualTo(review.getId()), + () -> assertThat(response.getLikes()).isEqualTo(likeCount), + () -> assertThat(response.getContents()).isEqualTo(review.getContentsValue()), + () -> assertThat(response.getHairStyleName()).isEqualTo(review.getHairStyle().getName()), + () -> assertThat(response.getScore()).isEqualTo(review.getScore().getValue()), + () -> assertThat(response.getUserNickname()).isEqualTo(review.getWriter().getNicknameValue()), + () -> assertThat(response.getWriterId()).isEqualTo(review.getWriter().getId()), + () -> { + List expectedHashTags = review.getHairStyle().getHashTags() + .stream() + .map(HashTagResponse::new) + .toList(); + + assertThat(response.getHashTags()).containsAll(expectedHashTags); + }, + () -> { + List expectedPhotos = review.getPhotos() + .stream() + .map(photo -> new PhotoInfo(photo.getStoreUrl())) + .toList(); + + assertThat(response.getPhotos()).containsAll(expectedPhotos); + } + ); + } + + private void assertPaging( + Paging actual, + boolean hasNext, + int contentSize + ) { + assertThat(actual.hasNext()).isEqualTo(hasNext); + assertThat(actual.getContentSize()).isEqualTo(contentSize); + } + + private Review createReview(Long id) { + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(id); + User user = UserFixture.getFixedManUser(id); + return ReviewFixture.getReview(id, hairStyle, user); + } + + @Nested + @DisplayName("[리뷰를 단건 조회한다]") + class findReviewById { + + @Test + @DisplayName("[성공적으로 조회한다]") + void success() { + //given + Review review = createReview(1L); + + given(reviewQueryRepository.findReviewById(1L)) + .willReturn(Optional.of(review)); + + given(likeReviewService.getLikeCount(1L)) + .willReturn(10L); + + given(likeReviewService.checkIsLiking(1L, 1L)) + .willReturn(new LikeReviewResponse(true)); + + //when + ReviewDetailResponse actual = reviewSearchService.findReviewById(1L, 1L); + + //then + assertThat(actual.isLiking()).isTrue(); + assertReviewResponse(actual.reviewResponse(), review, 10L); + } + + @Test + @DisplayName("[아이디에 해당하는 리뷰가 존재하지 않아 실패한다]") + void fail() { + //given + given(reviewQueryRepository.findReviewById(1L)) + .willReturn(Optional.empty()); + + //when + ThrowingCallable when = () -> reviewSearchService.findReviewById(1L, 1L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.NOT_EXIST_KEY.getMessage()); + } + } + + @Test + @DisplayName("[전체 리뷰를 조회한다]") + void findPagedReviews() { + //given + Pageable pageable = PageRequest.of(0, 4); + + List reviews = List.of(createReview(1L), createReview(2L)); + Slice reviewSlice = new SliceImpl<>(reviews, pageable, false); + given(reviewQueryRepository.findReviewByPaging(pageable)) + .willReturn(reviewSlice); + + List likeCounts = List.of(10L, 20L); + given(likeReviewService.getLikeCounts(List.of(1L, 2L))) + .willReturn(likeCounts); + + //when + PagedResponse actual = reviewSearchService.findPagedReviews(1L, pageable); + + //then + assertPaging(actual.getPaging(), false, 2); + List result = actual.getResult(); + for (int i = 0; i < result.size(); i++) { + assertReviewResponse(result.get(i), reviews.get(i), likeCounts.get(i)); + } + } + + @Test + @DisplayName("[좋아요한 리뷰를 조회한다]") + void findLikingReviews() { + //given + Pageable pageable = PageRequest.of(0, 4); + + List reviews = List.of(createReview(1L), createReview(2L)); + Slice reviewSlice = new SliceImpl<>(reviews, pageable, false); + given(reviewQueryRepository.findReviewByLike(1L, pageable)) + .willReturn(reviewSlice); + + List likeCounts = List.of(10L, 20L); + given(likeReviewService.getLikeCounts(List.of(1L, 2L))) + .willReturn(likeCounts); + + //when + PagedResponse actual = reviewSearchService.findLikingReviews(1L, pageable); + + //then + assertPaging(actual.getPaging(), false, 2); + List result = actual.getResult(); + for (int i = 0; i < result.size(); i++) { + assertReviewResponse(result.get(i), reviews.get(i), likeCounts.get(i)); + } + } + + @Test + @DisplayName("[내가 작성한 리뷰를 조회한다]") + void findMyReviews() { + //given + Pageable pageable = PageRequest.of(0, 4); + + List reviews = List.of(createReview(1L), createReview(2L)); + Slice reviewSlice = new SliceImpl<>(reviews, pageable, false); + given(reviewQueryRepository.findReviewByUser(1L, pageable)) + .willReturn(reviewSlice); + + List likeCounts = List.of(10L, 20L); + given(likeReviewService.getLikeCounts(List.of(1L, 2L))) + .willReturn(likeCounts); + + //when + PagedResponse actual = reviewSearchService.findMyReviews(1L, pageable); + + //then + assertPaging(actual.getPaging(), false, 2); + List result = actual.getResult(); + for (int i = 0; i < result.size(); i++) { + assertReviewResponse(result.get(i), reviews.get(i), likeCounts.get(i)); + } + } + + @Test + @DisplayName("[이달의 리뷰를 조회한다]") + void findReviewOfMonth() { + //given + List reviews = List.of(createReview(1L), createReview(2L)); + given(reviewQueryRepository.findReviewByCreatedDate()) + .willReturn(reviews); + + //when + ResponseWrapper actual = reviewSearchService.findReviewOfMonth(); + + //then + List result = actual.getResult(); + assertThat(result).hasSameSizeAs(reviews); + for (int i = 0; i < result.size(); i++) { + ReviewSimpleResponse response = result.get(i); + Review review = reviews.get(i); + assertAll( + () -> assertThat(response.reviewId()).isEqualTo(review.getId()), + () -> assertThat(response.userNickname()).isEqualTo(review.getWriter().getNicknameValue()), + () -> assertThat(response.hairStyleName()).isEqualTo(review.getHairStyle().getName()), + () -> assertThat(response.contents()).isEqualTo(review.getContentsValue()) + ); + } + } + + @Test + @DisplayName("[특정 헤어스타일의 리뷰를 조회한다]") + void findReviewByHairStyle() { + List reviews = List.of(createReview(1L)); + given(reviewQueryRepository.findReviewByHairStyle(1L)) + .willReturn(reviews); + + List likeCounts = List.of(10L); + given(likeReviewService.getLikeCounts(List.of(1L))) + .willReturn(likeCounts); + + //when + ResponseWrapper actual = reviewSearchService.findReviewByHairStyle(1L, 1L); + + //then + List result = actual.getResult(); + for (int i = 0; i < result.size(); i++) { + assertReviewResponse(result.get(i), reviews.get(i), likeCounts.get(i)); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewServiceTest.java new file mode 100644 index 0000000..3a1c8b4 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/application/ReviewServiceTest.java @@ -0,0 +1,193 @@ +package com.inq.wishhair.wesharewishhair.review.application; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +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.InjectMocks; +import org.mockito.Mock; +import org.springframework.context.ApplicationEventPublisher; + +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.hairstyle.application.HairStyleFindService; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.photo.application.PhotoService; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewCreateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewUpdateRequest; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; +import com.inq.wishhair.wesharewishhair.review.event.PointChargeEvent; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; +import com.inq.wishhair.wesharewishhair.user.application.UserFindService; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +@DisplayName("[ReviewService 테스트]") +class ReviewServiceTest extends MockTestSupport { + + @InjectMocks + private ReviewService reviewService; + @Mock + private ReviewRepository reviewRepository; + @Mock + private LikeReviewRepository likeReviewRepository; + @Mock + private ReviewFindService reviewFindService; + @Mock + private PhotoService photoService; + @Mock + private UserFindService userFindService; + @Mock + private HairStyleFindService hairStyleFindService; + @Mock + private ApplicationEventPublisher eventPublisher; + + private void mockingFindWithPhotosById() { + User user = UserFixture.getFixedManUser(1L); + Review review = ReviewFixture.getReview(1L, user); + given(reviewFindService.findWithPhotosById(1L)) + .willReturn(review); + } + + @Test + @DisplayName("[리뷰를 생성한다]") + void createReview() throws IOException { + //given + List photoUrls = List.of("url1", "url2"); + given(photoService.uploadPhotos(anyList())) + .willReturn(photoUrls); + + User user = UserFixture.getFixedManUser(1L); + given(userFindService.findByUserId(1L)) + .willReturn(user); + + HairStyle hairstyle = HairStyleFixture.getWomanHairStyle(1L); + given(hairStyleFindService.getById(1L)) + .willReturn(hairstyle); + + Review review = ReviewFixture.getReview(1L); + given(reviewRepository.save(any(Review.class))) + .willReturn(review); + + ReviewCreateRequest request = ReviewFixture.getReviewCreateRequest(); + + //when + Long actual = reviewService.createReview(request, 1L); + + //then + assertThat(actual).isEqualTo(user.getId()); + verify(eventPublisher, times(1)).publishEvent(any(PointChargeEvent.class)); + } + @Nested + @DisplayName("[리뷰를 삭제한다]") + class deleteReview { + + + @Test + @DisplayName("[성공적으로 삭제한다]") + void success() { + //given + mockingFindWithPhotosById(); + + //when + boolean actual = reviewService.deleteReview(1L, 1L); + + //then + assertThat(actual).isTrue(); + } + @Test + @DisplayName("[작성자가 아니라 실패한다]") + void fail() { + //given + mockingFindWithPhotosById(); + + //when + ThrowingCallable when = () -> reviewService.deleteReview(1L, 2L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.REVIEW_NOT_WRITER.getMessage()); + } + + } + + @Nested + @DisplayName("[리뷰 수정한다]") + class updateReview { + + @Test + @DisplayName("[성공적으로 수정한다]") + void success() throws IOException { + //given + mockingFindWithPhotosById(); + + List photoUrls = List.of("url1", "url2"); + given(photoService.uploadPhotos(anyList())) + .willReturn(photoUrls); + + ReviewUpdateRequest request = new ReviewUpdateRequest( + 1L, + "contents", + Score.S4H, + FileMockingUtils.createMockMultipartFiles() + ); + + //when + boolean actual = reviewService.updateReview(request, 1L); + + //then + assertThat(actual).isTrue(); + } + + @Test + @DisplayName("[작성자가 아니라 실패한다]") + void fail() throws IOException { + //given + mockingFindWithPhotosById(); + + ReviewUpdateRequest request = new ReviewUpdateRequest( + 1L, + "contents", + Score.S4H, + FileMockingUtils.createMockMultipartFiles() + ); + + //when + ThrowingCallable when = () -> reviewService.updateReview(request, 2L); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.REVIEW_NOT_WRITER.getMessage()); + } + } + + @Test + @DisplayName("[특정 작성자의 리뷰를 모두 삭제한다]") + void deleteReviewByWriter() { + //given + User user = UserFixture.getFixedManUser(1L); + List reviews = List.of(ReviewFixture.getReview(1L, user)); + given(reviewFindService.findWithPhotosByUserId(1L)) + .willReturn(reviews); + + //when + boolean actual = reviewService.deleteReviewByWriter(1L); + + //then + assertThat(actual).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java index f25afd8..1431fcd 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -1,11 +1,14 @@ package com.inq.wishhair.wesharewishhair.review.fixture; +import java.io.IOException; import java.util.List; import org.springframework.test.util.ReflectionTestUtils; +import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewCreateRequest; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; import com.inq.wishhair.wesharewishhair.user.domain.entity.User; @@ -39,10 +42,26 @@ public static Review getReview() { ); } - public static Review getReview( - HairStyle hairStyle, - User user - ) { + public static Review getReview(Long id) { + Review review = getReview(); + ReflectionTestUtils.setField(review, "id", id); + return review; + } + + public static Review getReview(Long id, User user) { + Review review = Review.createReview( + user, + CONTENTS, + Score.S2H, + URLS, + HairStyleFixture.getWomanHairStyle() + ); + ReflectionTestUtils.setField(review, "id", id); + + return review; + } + + public static Review getReview(HairStyle hairStyle, User user) { return Review.createReview( user, CONTENTS, @@ -51,4 +70,19 @@ public static Review getReview( hairStyle ); } + + public static Review getReview(Long id, HairStyle hairStyle, User user) { + Review review = getReview(hairStyle, user); + ReflectionTestUtils.setField(review, "id", id); + return review; + } + + public static ReviewCreateRequest getReviewCreateRequest() throws IOException { + return new ReviewCreateRequest( + CONTENTS, + Score.S2H, + List.of(FileMockingUtils.createMockMultipartFile("hello1.jpg")), + 1L + ); + } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java index e8443c8..003ecb4 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/fixture/UserFixture.java @@ -30,7 +30,7 @@ public static User getFixedManUser() { ); } - public static User getManUserWithId(Long id) { + public static User getFixedManUser(Long id) { User user = getFixedManUser(); ReflectionTestUtils.setField(user, "id", id); return user; diff --git a/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet b/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet deleted file mode 100644 index 2de85d7..0000000 --- a/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet +++ /dev/null @@ -1,9 +0,0 @@ -[cols="3,6", options=header] -.+{{path}}+ -|=== -|동적 URL 변수|설명 -{{#parameters}} -|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} -|{{#tableCellContent}}{{description}}{{/tableCellContent}} -{{/parameters}} -|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet deleted file mode 100644 index 1674935..0000000 --- a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet +++ /dev/null @@ -1,11 +0,0 @@ -[cols="4,2,2,5,6", options=header] -|=== -|필드명|타입|필수 여부|제약 조건|설명 -{{#fields}} -|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} -|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} -|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}} -|{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} -|{{#tableCellContent}}{{description}}{{/tableCellContent}} -{{/fields}} -|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-headers.snippet b/src/test/resources/org/springframework/restdocs/templates/request-headers.snippet deleted file mode 100644 index 73ddc31..0000000 --- a/src/test/resources/org/springframework/restdocs/templates/request-headers.snippet +++ /dev/null @@ -1,8 +0,0 @@ -[cols="3,6", options=header] -|=== -|헤더명|설명 -{{#headers}} -|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} -|{{#tableCellContent}}{{description}}{{/tableCellContent}} -{{/headers}} -|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet b/src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet deleted file mode 100644 index d88c25c..0000000 --- a/src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet +++ /dev/null @@ -1,10 +0,0 @@ -[cols="3,2,6,6", options=header] -|=== -|Query String|필수 여부|제약 조건|설명 -{{#parameters}} -|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} -|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}} -|{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} -|{{#tableCellContent}}{{description}}{{/tableCellContent}} -{{/parameters}} -|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet deleted file mode 100644 index 668bd16..0000000 --- a/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet +++ /dev/null @@ -1,10 +0,0 @@ -[cols="3,2,6,6", options=header] -|=== -|필드명|타입|제약 조건|설명 -{{#fields}} -|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} -|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} -|{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} -|{{#tableCellContent}}{{description}}{{/tableCellContent}} -{{/fields}} -|=== \ No newline at end of file From 3db47b724b4b6a21a279c1c1348edb8188ca9750 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Tue, 14 Nov 2023 15:54:24 +0900 Subject: [PATCH 29/30] =?UTF-8?q?test:=20Review=20ScoreConverter=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/converter/ScoreConverter.java | 2 +- .../config/converter/ScoreConverterTest.java | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverterTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverter.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverter.java index 8ee196c..12f9b4b 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverter.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverter.java @@ -11,7 +11,7 @@ public class ScoreConverter implements Converter { @Override public Score convert(String value) { switch (value) { - case "0" -> { + case "0.0" -> { return Score.S0; } case "0.5" -> { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverterTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverterTest.java new file mode 100644 index 0000000..59e0dca --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/config/converter/ScoreConverterTest.java @@ -0,0 +1,52 @@ +package com.inq.wishhair.wesharewishhair.review.config.converter; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.ThrowableAssert.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; +import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; + +@DisplayName("[ScoreConverter 테스트]") +class ScoreConverterTest { + + private final ScoreConverter scoreConverter = new ScoreConverter(); + + @Nested + @DisplayName("[리뷰 점수를 enum 으로 변환한다]") + class convert { + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"0.0", "0.5", "1.0", "1.5", "2.0", "2.5", "3.0", "3.5", "4.0", "4.5"}) + @DisplayName("[성공적으로 변환한다]") + void success(String score) { + //when + Score actual = scoreConverter.convert(score); + + //then + assertThat(actual).isNotNull(); + assertThat(actual.getValue()).isEqualTo(score); + } + + @Test + @DisplayName("[잘못된 입력으로 실패한다]") + void fail() { + //given + String score = "10.0"; + + //when + ThrowingCallable when = () -> scoreConverter.convert(score); + + //then + assertThatThrownBy(when) + .isInstanceOf(WishHairException.class) + .hasMessageContaining(ErrorCode.SCORE_MISMATCH.getMessage()); + } + } +} \ No newline at end of file From 1b17d0489221aa1ac269de1637691dcd2848f4c0 Mon Sep 17 00:00:00 2001 From: EunChanNam Date: Tue, 14 Nov 2023 22:10:19 +0900 Subject: [PATCH 30/30] =?UTF-8?q?test:=20Review=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ReviewUpdateRequest.java | 1 + .../presentation/LikeReviewController.java | 3 - .../common/config/EmbeddedRedisConfig.java | 20 +- .../common/support/ApiTestSupport.java | 10 + .../presentation/HairStyleApiTest.java | 2 +- .../review/fixture/ReviewFixture.java | 19 ++ .../LikeReviewControllerTest.java | 89 ++++++++ .../review/presentation/ReviewApiTest.java | 149 ++++++++++++++ .../presentation/ReviewSearchApiTest.java | 190 ++++++++++++++++++ 9 files changed, 477 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewControllerTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewApiTest.java create mode 100644 src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchApiTest.java diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java index 1e273e4..d398c1f 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java @@ -15,6 +15,7 @@ public record ReviewUpdateRequest( String contents, @NotNull Score score, + List files ) { } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java index 27edb82..8ebfd97 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java @@ -28,7 +28,6 @@ public ResponseEntity executeLike( @PathVariable Long reviewId, @FetchAuthInfo AuthInfo authInfo ) { - likeReviewService.executeLike(reviewId, authInfo.userId()); return ResponseEntity.ok(new Success()); } @@ -38,7 +37,6 @@ public ResponseEntity cancelLike( @PathVariable Long reviewId, @FetchAuthInfo AuthInfo authInfo ) { - likeReviewService.cancelLike(reviewId, authInfo.userId()); return ResponseEntity.ok(new Success()); } @@ -48,7 +46,6 @@ public ResponseEntity checkIsLiking( @FetchAuthInfo AuthInfo authInfo, @PathVariable Long reviewId ) { - LikeReviewResponse result = likeReviewService.checkIsLiking(authInfo.userId(), reviewId); return ResponseEntity.ok(result); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java index 20ec555..441c002 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java @@ -1,7 +1,11 @@ package com.inq.wishhair.wesharewishhair.common.config; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.data.redis.core.RedisTemplate; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; @@ -11,14 +15,26 @@ public class EmbeddedRedisConfig { private final RedisServer redisServer; + private final RedisTemplate redisTemplate; - public EmbeddedRedisConfig(@Value("${spring.data.redis.port}") int port) { + public EmbeddedRedisConfig( + @Value("${spring.data.redis.port}") int port, + @Autowired RedisTemplate redisTemplate + ) { this.redisServer = new RedisServer(port); + this.redisTemplate = redisTemplate; } @PostConstruct public void startRedis() { - this.redisServer.start(); + try { + this.redisServer.start(); + } catch (RuntimeException e) { + Set keys = redisTemplate.keys("*"); + if (keys != null) { + redisTemplate.delete(keys); + } + } } @PreDestroy diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java index cf1a2b1..04a1ca6 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/ApiTestSupport.java @@ -8,6 +8,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.inq.wishhair.wesharewishhair.auth.domain.AuthTokenManager; +import com.inq.wishhair.wesharewishhair.user.domain.UserRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; @SpringBootTest @AutoConfigureMockMvc @@ -18,11 +21,18 @@ public abstract class ApiTestSupport { @Autowired private AuthTokenManager authTokenManager; + @Autowired + private UserRepository userRepository; protected String getAccessToken(Long userId) { return authTokenManager.generate(userId).accessToken(); } + protected User saveUser() { + User user = UserFixture.getFixedWomanUser(); + return userRepository.save(user); + } + public String toJson(Object object) throws JsonProcessingException { return objectMapper.writeValueAsString(object); } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java index 932e304..09b6b2b 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java @@ -25,7 +25,7 @@ import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; -@DisplayName("[HairStyleApi 테스트]") +@DisplayName("[HairStyle Api 테스트]") class HairStyleApiTest extends ApiTestSupport { @Autowired diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java index 1431fcd..d8bfa4c 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -9,6 +9,7 @@ import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewCreateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewUpdateRequest; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import com.inq.wishhair.wesharewishhair.review.domain.entity.Score; import com.inq.wishhair.wesharewishhair.user.domain.entity.User; @@ -85,4 +86,22 @@ public static ReviewCreateRequest getReviewCreateRequest() throws IOException { 1L ); } + + public static ReviewCreateRequest getReviewCreateRequest(Long hairStyleId) throws IOException { + return new ReviewCreateRequest( + CONTENTS, + Score.S2H, + List.of(FileMockingUtils.createMockMultipartFile("hello1.jpg")), + hairStyleId + ); + } + + public static ReviewUpdateRequest getReviewUpdateRequest(Long reviewId) throws IOException { + return new ReviewUpdateRequest( + reviewId, + "updateContents", + Score.S5, + FileMockingUtils.createMockMultipartFiles() + ); + } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewControllerTest.java new file mode 100644 index 0000000..0242ec6 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewControllerTest.java @@ -0,0 +1,89 @@ +package com.inq.wishhair.wesharewishhair.review.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.common.config.EmbeddedRedisConfig; +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReview; +import com.inq.wishhair.wesharewishhair.review.domain.likereview.LikeReviewRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; + +@DisplayName("[LikeReview API 테스트]") +@Import(EmbeddedRedisConfig.class) +class LikeReviewControllerTest extends ApiTestSupport { + + @Autowired + private MockMvc mockMvc; + @Autowired + private LikeReviewRepository likeReviewRepository; + + @Test + @DisplayName("[좋아요 실행 API 를 호출한다]") + void executeLike() throws Exception { + //given + User user = saveUser(); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .post("/api/reviews/like/1") + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + ); + + //then + actual.andExpect(status().isOk()); + boolean userExist = likeReviewRepository.existsByUserIdAndReviewId(user.getId(), 1L); + assertThat(userExist).isTrue(); + } + + @Test + @DisplayName("[좋아요 취소 API 를 호출한다]") + void cancelLike() throws Exception { + //given + User user = saveUser(); + likeReviewRepository.save(LikeReview.addLike(user.getId(), 2L)); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/reviews/like/2") + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + ); + + //then + actual.andExpect(status().isOk()); + boolean userExist = likeReviewRepository.existsByUserIdAndReviewId(user.getId(), 2L); + assertThat(userExist).isFalse(); + } + + @Test + @DisplayName("[좋아요 상태 확인 API 를 호출한다]") + void checkIsLiking() throws Exception { + //given + User user = saveUser(); + likeReviewRepository.save(LikeReview.addLike(user.getId(), 3L)); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/reviews/like/3") + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.isLiking").value(true) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewApiTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewApiTest.java new file mode 100644 index 0000000..1d57181 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewApiTest.java @@ -0,0 +1,149 @@ +package com.inq.wishhair.wesharewishhair.review.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoStore; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewCreateRequest; +import com.inq.wishhair.wesharewishhair.review.application.dto.request.ReviewUpdateRequest; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; +import com.inq.wishhair.wesharewishhair.review.infrastructure.ReviewJpaRepository; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; + +@DisplayName("[Review API 테스트]") +class ReviewApiTest extends ApiTestSupport { + + @Autowired + private MockMvc mockMvc; + @Autowired + private ReviewRepository reviewRepository; + @Autowired + private ReviewJpaRepository reviewJpaRepository; + @Autowired + private HairStyleRepository hairStyleRepository; + + @MockBean + private PhotoStore photoStore; + + private Review saveReview(User user) { + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + hairStyleRepository.save(hairStyle); + + Review review = ReviewFixture.getReview(hairStyle, user); + reviewRepository.save(review); + return review; + } + + @Test + @DisplayName("[리뷰 생성 API 를 호출한다]") + void createReview() throws Exception { + //given + User user = saveUser(); + + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + hairStyleRepository.save(hairStyle); + + given(photoStore.uploadFiles(anyList())) + .willReturn(List.of("url1")); + + ReviewCreateRequest request = ReviewFixture.getReviewCreateRequest(hairStyle.getId()); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("contents", request.contents()); + params.add("score", String.valueOf(request.score())); + params.add("hairStyleId", String.valueOf(request.hairStyleId())); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .multipart(HttpMethod.POST, "/api/reviews") + .file((MockMultipartFile)request.files().get(0)) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + .params(params) + ); + + //then + actual.andExpect(status().isCreated()); + assertThat(reviewJpaRepository.findAll()).hasSize(1); + } + + @Test + @DisplayName("[리뷰 삭제 API 를 호출한다]") + void deleteReview() throws Exception { + //given + given(photoStore.deleteFiles(anyList())).willReturn(true); + given(photoStore.uploadFiles(anyList())).willReturn(List.of("url1")); + + User user = saveUser(); + Review review = saveReview(user); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/reviews/" + review.getId()) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + ); + + //then + actual.andExpect(status().isOk()); + assertThat(reviewRepository.findById(review.getId())).isNotPresent(); + } + + @Test + @DisplayName("[리뷰 수정 API 를 호출한다]") + void updateReview() throws Exception { + //given + given(photoStore.uploadFiles(anyList())).willReturn(List.of("url1")); + + User user = saveUser(); + Review review = saveReview(user); + + ReviewUpdateRequest request = ReviewFixture.getReviewUpdateRequest(review.getId()); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("contents", request.contents()); + params.add("score", String.valueOf(request.score())); + params.add("reviewId", String.valueOf(request.reviewId())); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .multipart(HttpMethod.PATCH, "/api/reviews") + .file((MockMultipartFile)request.files().get(0)) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + .params(params) + ); + + //then + actual.andExpect(status().isOk()); + Review findReview = reviewRepository.findById(review.getId()).orElseThrow(); + assertAll( + () -> assertThat(findReview.getScore()).isEqualTo(request.score()), + () -> assertThat(findReview.getContentsValue()).isEqualTo(request.contents()), + () -> assertThat(findReview.getPhotos().get(0).getStoreUrl()).isEqualTo("url1") + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchApiTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchApiTest.java new file mode 100644 index 0000000..f665174 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/presentation/ReviewSearchApiTest.java @@ -0,0 +1,190 @@ +package com.inq.wishhair.wesharewishhair.review.presentation; + +import static com.inq.wishhair.wesharewishhair.common.fixture.AuthFixture.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.inq.wishhair.wesharewishhair.common.config.EmbeddedRedisConfig; +import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyle; +import com.inq.wishhair.wesharewishhair.hairstyle.domain.HairStyleRepository; +import com.inq.wishhair.wesharewishhair.hairstyle.fixture.HairStyleFixture; +import com.inq.wishhair.wesharewishhair.photo.domain.PhotoStore; +import com.inq.wishhair.wesharewishhair.review.domain.ReviewRepository; +import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; +import com.inq.wishhair.wesharewishhair.review.fixture.ReviewFixture; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; + +@DisplayName("[ReviewSearchController 테스트]") +@Import(EmbeddedRedisConfig.class) +class ReviewSearchApiTest extends ApiTestSupport { + + @Autowired + private MockMvc mockMvc; + @Autowired + private ReviewRepository reviewRepository; + @Autowired + private HairStyleRepository hairStyleRepository; + + @MockBean + private PhotoStore photoStore; + + @BeforeEach + void setUp() { + given(photoStore.uploadFiles(anyList())) + .willReturn(List.of("url1")); + } + + private HairStyle saveHairStyle() { + HairStyle hairStyle = HairStyleFixture.getWomanHairStyle(); + return hairStyleRepository.save(hairStyle); + } + + private Review saveReview(User user, HairStyle hairStyle) { + Review review = ReviewFixture.getReview(hairStyle, user); + reviewRepository.save(review); + return review; + } + + @Test + @DisplayName("[리뷰 단건조회 API 를 호출한다]") + void findReview() throws Exception { + //given + User user = saveUser(); + HairStyle hairStyle = saveHairStyle(); + + Review review = saveReview(user, hairStyle); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/reviews/" + review.getId()) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.reviewResponse").exists(), + jsonPath("$.isLiking").value(false) + ); + } + + @Test + @DisplayName("[모든 리뷰를 조회한다]") + void findPagingReviews() throws Exception { + //given + User user = saveUser(); + HairStyle hairStyle = saveHairStyle(); + + saveReview(user, hairStyle); + saveReview(user, hairStyle); + saveReview(user, hairStyle); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/reviews") + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + .param("size", "10") + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.paging.hasNext").value(false), + jsonPath("$.result.size()").value(3) + ); + } + + @Test + @DisplayName("[사용자 작성 리뷰조회 API 를 호출한다]") + void findMyReviews() throws Exception { + //given + User user = saveUser(); + HairStyle hairStyle = saveHairStyle(); + + saveReview(user, hairStyle); + saveReview(user, hairStyle); + saveReview(user, hairStyle); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/reviews/my") + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + .param("size", "10") + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.paging.hasNext").value(false), + jsonPath("$.result.size()").value(3) + ); + } + + @Test + @DisplayName("[이달의 리뷰 조회 API 를 호출한다]") + void findReviewOfMonth() throws Exception { + //given + User user = saveUser(); + HairStyle hairStyle = saveHairStyle(); + + saveReview(user, hairStyle); + saveReview(user, hairStyle); + saveReview(user, hairStyle); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/reviews/month") + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + .param("size", "10") + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.result.size()").value(0) + ); + } + + @Test + @DisplayName("[특정 헤어스타일의 리뷰 조회 API 를 호출한다]") + void findHairStyleReview() throws Exception { + //given + User user = saveUser(); + HairStyle hairStyle = saveHairStyle(); + + saveReview(user, hairStyle); + saveReview(user, hairStyle); + saveReview(user, hairStyle); + + //when + ResultActions actual = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/reviews/hair_style/" + hairStyle.getId()) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) + ); + + //then + actual.andExpectAll( + status().isOk(), + jsonPath("$.result.size()").value(3) + ); + } +} \ No newline at end of file