diff --git a/build.gradle b/build.gradle index cb0e616..f45120c 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,10 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' + //Redis + implementation('it.ozimov:embedded-redis:0.7.2') + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + //네이버 클라우드 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' @@ -119,7 +123,8 @@ jacocoTestReport { '**/*Application*', "**/config/**", "**/exception/**", - "**/dto/**" + "**/dto/**", + "**/Q*/**" ]) }) ) 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/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/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/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/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/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/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/global/utils/RedisUtils.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java new file mode 100644 index 0000000..be7bc8b --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/utils/RedisUtils.java @@ -0,0 +1,43 @@ +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; + +@Component +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/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/HairStyleSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchService.java index 0c7160a..f390a9f 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.application.query.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 13f8f96..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 @@ -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( - final Long hairStyleId, - final Long userId - ) { - validateDoesWishHairExist(hairStyleId, userId); - - wishHairRepository.deleteByHairStyleIdAndUserId(hairStyleId, userId); - } - - public WishHairResponse checkIsWishing( + public boolean executeWish( 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); - } + wishHairRepository.deleteByHairStyleIdAndUserId(hairStyleId, userId); + return true; } - 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/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/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/infrastructure/WishHairJpaRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/hairstyle/infrastructure/WishHairJpaRepository.java index de9a6ce..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 @@ -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); + void deleteByHairStyleIdAndUserId(Long hairStyleId, Long userId); boolean existsByHairStyleIdAndUserId(Long hairStyleId, Long userId); } 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..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 @@ -6,9 +6,10 @@ 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; +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; @@ -23,6 +24,7 @@ import lombok.RequiredArgsConstructor; +@Repository @Transactional(readOnly = true) @RequiredArgsConstructor public class HairStyleQueryDslRepository implements HairStyleQueryRepository { @@ -57,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/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/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/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/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java b/src/main/java/com/inq/wishhair/wesharewishhair/photo/domain/Photo.java index 81165ba..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 @@ -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,14 +27,14 @@ 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) + @Column(nullable = false, updatable = false) private String storeUrl; //==생성 메서드==// 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/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 3f6d41b..0f0ef74 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.getById(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.getById(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/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/point/presentation/PointController.java b/src/main/java/com/inq/wishhair/wesharewishhair/point/presentation/PointController.java index 91fa489..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,21 +1,27 @@ 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; import lombok.RequiredArgsConstructor; @RestController -@RequestMapping("/api/users/point") +@RequestMapping("/api/points") @RequiredArgsConstructor public class PointController { @@ -26,9 +32,19 @@ public ResponseEntity usePoint( final @RequestBody PointUseRequest request, final @FetchAuthInfo AuthInfo authInfo ) { - pointService.usePoint(request, authInfo.userId()); 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/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/LikeReviewService.java index ad91df7..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 @@ -1,14 +1,16 @@ package com.inq.wishhair.wesharewishhair.review.application; +import java.util.List; + 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.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; @Service @@ -17,38 +19,58 @@ 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 void executeLike(Long reviewId, Long userId) { - validateIsNotLiking(userId, reviewId); + public boolean executeLike(Long reviewId, Long userId) { + try { + likeReviewRepository.save(LikeReview.addLike(userId, reviewId)); + } catch (EntityExistsException e) { + return false; + } + + //락을 걸지않고 값이없으면 좋아요 개수를 로드해서 반영 기능 추가 + redisUtils.getData(reviewId) + .ifPresentOrElse( + likeCount -> redisUtils.increaseData(reviewId), + () -> updateLikeCountFromRedis(reviewId) + ); - likeReviewRepository.save(LikeReview.addLike(userId, reviewId)); + return true; } @Transactional - public void cancelLike(Long reviewId, Long userId) { - validateIsLiking(userId, reviewId); - + public boolean cancelLike(Long reviewId, Long userId) { likeReviewRepository.deleteByUserIdAndReviewId(userId, reviewId); - } - public LikeReviewResponse checkIsLiking(Long userId, Long reviewId) { - return new LikeReviewResponse(existLikeReview(userId, reviewId)); + redisUtils.getData(reviewId) + .ifPresentOrElse( + likeCount -> redisUtils.decreaseData(reviewId), + () -> updateLikeCountFromRedis(reviewId) + ); + + return true; } - private boolean existLikeReview(Long userId, Long reviewId) { - return likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); + public LikeReviewResponse checkIsLiking(Long userId, Long reviewId) { + boolean isLiking = likeReviewRepository.existsByUserIdAndReviewId(userId, reviewId); + return new LikeReviewResponse(isLiking); } - private void validateIsNotLiking(Long userId, Long reviewId) { - if (existLikeReview(userId, reviewId)) { - throw new WishHairException(ErrorCode.REVIEW_ALREADY_LIKING); - } + public Long getLikeCount(Long reviewId) { + return redisUtils.getData(reviewId) + .orElse(updateLikeCountFromRedis(reviewId)); } - private void validateIsLiking(Long userId, Long reviewId) { - if (!existLikeReview(userId, reviewId)) { - throw new WishHairException(ErrorCode.REVIEW_NOT_LIKING); - } + public List getLikeCounts(List reviewIds) { + return reviewIds.stream() + .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 793639f..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 @@ -23,6 +23,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/ReviewSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/ReviewSearchService.java index 50c24b4..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 @@ -17,10 +17,8 @@ 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; import lombok.RequiredArgsConstructor; @@ -29,52 +27,69 @@ @Transactional(readOnly = true) public class ReviewSearchService { - private final ReviewQueryRepository reviewRepository; - private final LikeReviewRepository likeReviewRepository; + 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) { - ReviewQueryResponse queryResponse = reviewRepository.findReviewById(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(queryResponse, isLiking); + return toReviewDetailResponse(review, likeCount, isLiking); } /*전체 리뷰 조회*/ @AddisWriter public PagedResponse findPagedReviews(Long userId, Pageable pageable) { - Slice sliceResult = reviewRepository.findReviewByPaging(pageable); - return toPagedReviewResponse(sliceResult); + Slice sliceResult = reviewQueryRepository.findReviewByPaging(pageable); + + List likeCounts = fetchLikeCounts(sliceResult.getContent()); + + return toPagedReviewResponse(sliceResult, likeCounts); } /*좋아요한 리뷰 조회*/ @AddisWriter public PagedResponse findLikingReviews(Long userId, Pageable pageable) { - Slice sliceResult = reviewRepository.findReviewByLike(userId, pageable); - return toPagedReviewResponse(sliceResult); + Slice sliceResult = reviewQueryRepository.findReviewByLike(userId, pageable); + + List likeCounts = fetchLikeCounts(sliceResult.getContent()); + + return toPagedReviewResponse(sliceResult, likeCounts); } /*나의 리뷰 조회*/ @AddisWriter public PagedResponse findMyReviews(Long userId, Pageable pageable) { - Slice sliceResult = reviewRepository.findReviewByUser(userId, pageable); + Slice sliceResult = reviewQueryRepository.findReviewByUser(userId, pageable); + + List likeCounts = fetchLikeCounts(sliceResult.getContent()); - return toPagedReviewResponse(sliceResult); + return toPagedReviewResponse(sliceResult, likeCounts); } /*이달의 추천 리뷰 조회*/ 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); - return toWrappedReviewResponse(result); + List result = reviewQueryRepository.findReviewByHairStyle(hairStyleId); + + List likeCounts = fetchLikeCounts(result); + + return toWrappedReviewResponse(result, likeCounts); } } 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 22c3e8e..024b075 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.getById(userId); - HairStyle hairStyle = hairStyleFindService.findById(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.deleteAllByReview(reviewId); + 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.deleteAllByReviews(reviewIds); + likeReviewRepository.deleteByReviewIdIn(reviewIds); photoService.deletePhotosByWriter(reviews); - reviewRepository.deleteAllByWriter(reviewIds); - } - - private void validateIsWriter(Long userId, Review review) { - if (!review.isWriter(userId)) { - throw new WishHairException(ErrorCode.REVIEW_NOT_WRITER); - } - } + reviewRepository.deleteByIdIn(reviewIds); - 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..d398c1f --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/application/dto/request/ReviewUpdateRequest.java @@ -0,0 +1,21 @@ +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/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..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,13 +3,14 @@ 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; -import com.inq.wishhair.wesharewishhair.review.application.query.dto.ReviewQueryResponse; import com.inq.wishhair.wesharewishhair.review.domain.entity.Review; import lombok.AccessLevel; @@ -18,16 +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(ReviewQueryResponse queryResponse) { - Review review = queryResponse.review(); + public static ReviewResponse toReviewResponse(Review review, Long likeCount) { return ReviewResponse.builder() .reviewId(review.getId()) @@ -37,14 +47,18 @@ public static ReviewResponse toReviewResponse(ReviewQueryResponse queryResponse) .contents(review.getContentsValue()) .createdDate(review.getCreatedDate()) .photos(toPhotoResponses(review.getPhotos())) - .likes(queryResponse.likes()) + .likes(likeCount) .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, + Long likeCount, + boolean isLiking + ) { + return new ReviewDetailResponse(toReviewResponse(review, likeCount), isLiking); } public static ResponseWrapper toWrappedSimpleResponse(List reviews) { @@ -52,10 +66,17 @@ 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/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/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/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/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..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 @@ -9,13 +9,15 @@ public interface ReviewRepository { Review save(Review review); + Optional findById(Long id); + //review find service - 리뷰 단순 조회 Optional findWithPhotosById(Long id); //회원 탈퇴를 위한 사용자가 작성한 리뷰 조회 - List findWithPhotosByUserId(Long userId); + List findWithPhotosByWriterId(Long userId); - void deleteAllByWriter(List reviewIds); + void deleteByIdIn(List reviewIds); void delete(Review review); } 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..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; @@ -11,23 +10,25 @@ 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; 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.OneToMany; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class Review extends BaseEntity { @Id @@ -35,21 +36,21 @@ 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; + @Embedded private Contents contents; @Column(nullable = false) @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") + @JoinColumn(name = "hair_style_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) private HairStyle hairStyle; private Review(User writer, String contents, Score score, List photos, HairStyle hairStyle) { @@ -58,12 +59,16 @@ private Review(User writer, String contents, Score score, List photos, H this.score = score; applyPhotos(photos); this.hairStyle = hairStyle; - this.createdDate = LocalDateTime.now(); } //==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); } 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..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,7 +4,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,16 +13,15 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "reviewId"})) public class LikeReview { @Id @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/domain/likereview/LikeReviewRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/domain/likereview/LikeReviewRepository.java index 73eb145..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 @@ -6,11 +6,13 @@ public interface LikeReviewRepository { LikeReview save(LikeReview likeReview); - void deleteAllByReview(Long reviewId); + Long countByReviewId(Long reviewId); + + void deleteByReviewId(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 27d04e3..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,19 +10,13 @@ public interface LikeReviewJpaRepository extends LikeReviewRepository, JpaRepository { - @Modifying - @Query("delete from LikeReview l where l.reviewId = :reviewId") - void deleteAllByReview(@Param("reviewId") Long reviewId); + Long countByReviewId(Long reviewId); - @Modifying - @Query("delete from LikeReview l " + - "where l.userId = :userId and l.reviewId = :reviewId") - void deleteByUserIdAndReviewId(@Param("userId") Long userId, - @Param("reviewId") Long reviewId); + void deleteByReviewId(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 216b10a..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 @@ -3,10 +3,8 @@ import java.util.List; import java.util.Optional; +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; @@ -14,18 +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); + 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 b7df7d6..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 @@ -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,24 +66,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(assembleReviewProjection()) + public Slice findReviewByLike(Long userId, Pageable pageable) { + 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()) @@ -108,22 +86,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,57 +111,44 @@ 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()) .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()) .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.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); } - 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/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/presentation/LikeReviewController.java index 3cc4d72..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 @@ -25,30 +25,27 @@ 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()); return ResponseEntity.ok(new Success()); } @DeleteMapping("/{reviewId}") public ResponseEntity cancelLike( - final @PathVariable Long reviewId, - final @FetchAuthInfo AuthInfo authInfo + @PathVariable Long reviewId, + @FetchAuthInfo AuthInfo authInfo ) { - likeReviewService.cancelLike(reviewId, authInfo.userId()); return ResponseEntity.ok(new Success()); } @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); return ResponseEntity.ok(result); } 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/main/resources/application.yml b/src/main/resources/application.yml index 6c35476..fd37abb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,21 +9,28 @@ 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 +# show_sql: true +# format_sql: true + dialect: org.hibernate.dialect.MySQLDialect open-in-view: false + data: + redis: + host: localhost + port: 6379 + expire-time: 21600000 + 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: @@ -46,23 +53,24 @@ 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: - 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 new file mode 100644 index 0000000..1dfacf4 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/LikeTest.java @@ -0,0 +1,55 @@ +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 org.springframework.context.annotation.Import; + +import com.inq.wishhair.wesharewishhair.common.config.EmbeddedRedisConfig; +import com.inq.wishhair.wesharewishhair.review.application.LikeReviewService; + +@SpringBootTest +@Import(EmbeddedRedisConfig.class) +@DisplayName("[좋아요 동시성 테스트]") +class LikeTest { + + @Autowired + private LikeReviewService likeReviewService; + + @Test + @DisplayName("[100개의 좋아요 동시 요청 모두 반영한다]") + void success() throws InterruptedException { + //given + 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(1L, (long)id); + latch.countDown(); + }); + } + + latch.await(); + + //then + Long likeCount = likeReviewService.getLikeCount(1L); + assertThat(likeCount).isEqualTo(101); + } +} 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 index b1b1d45..d3291a1 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/application/MailAuthServiceTest.java @@ -10,10 +10,8 @@ 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; @@ -22,7 +20,6 @@ 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 { 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 deleted file mode 100644 index 95a15fa..0000000 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/infrastructure/jwt/JwtTokenProviderTest.java +++ /dev/null @@ -1,115 +0,0 @@ -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/presentation/AuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/AuthControllerTest.java index 5bf94e4..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 @@ -6,9 +6,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.beans.factory.annotation.Autowired; 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; @@ -16,17 +17,22 @@ 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; +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; -@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; + @Autowired + private UserRepository userRepository; @Test @DisplayName("[로그인 API 를 호출한다]") @@ -59,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/MailAuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/presentation/MailAuthControllerTest.java index a121670..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 @@ -4,9 +4,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.beans.factory.annotation.Autowired; 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; @@ -14,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 { @@ -26,6 +25,8 @@ class MailAuthControllerTest extends ApiTestSupport { 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 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 fce4a19..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,50 +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.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; +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.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.global.config.SecurityConfig; +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; -@WebMvcTest(value = {TokenReissueController.class, SecurityConfig.class}) @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"; - @MockBean - private TokenReissueService tokenReissueService; + @Autowired + private MockMvc mockMvc; + @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/config/EmbeddedRedisConfig.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java new file mode 100644 index 0000000..441c002 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/config/EmbeddedRedisConfig.java @@ -0,0 +1,44 @@ +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; +import redis.embedded.RedisServer; + +@TestConfiguration +public class EmbeddedRedisConfig { + + private final RedisServer redisServer; + private final RedisTemplate redisTemplate; + + public EmbeddedRedisConfig( + @Value("${spring.data.redis.port}") int port, + @Autowired RedisTemplate redisTemplate + ) { + this.redisServer = new RedisServer(port); + this.redisTemplate = redisTemplate; + } + + @PostConstruct + public void startRedis() { + try { + this.redisServer.start(); + } catch (RuntimeException e) { + Set keys = redisTemplate.keys("*"); + if (keys != null) { + redisTemplate.delete(keys); + } + } + } + + @PreDestroy + public void stopRedis() { + this.redisServer.stop(); + } +} \ 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 a4b1132..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 @@ -1,32 +1,36 @@ 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.mock.mockito.MockBean; -import org.springframework.test.web.servlet.MockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +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; +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 +@Transactional public abstract class ApiTestSupport { private final ObjectMapper objectMapper = new ObjectMapper(); @Autowired - protected MockMvc mockMvc; + private AuthTokenManager authTokenManager; + @Autowired + private UserRepository userRepository; - @MockBean - protected AuthTokenManager authTokenManager; + protected String getAccessToken(Long userId) { + return authTokenManager.generate(userId).accessToken(); + } - @BeforeEach - public void setAuthorization() { - given(authTokenManager.generate(any(Long.class))).willReturn(new AuthToken(TOKEN, TOKEN)); - given(authTokenManager.getId(anyString())).willReturn(1L); + protected User saveUser() { + User user = UserFixture.getFixedWomanUser(); + return userRepository.save(user); } public String toJson(Object object) throws JsonProcessingException { 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/common/support/RepositoryTestSupport.java b/src/test/java/com/inq/wishhair/wesharewishhair/common/support/RepositoryTestSupport.java index 61f60cc..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 @@ -4,8 +4,10 @@ import org.springframework.context.annotation.Import; 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) +@Import({QueryDslConfig.class, HairStyleQueryDslRepository.class, ReviewQueryDslRepository.class}) @DataJpaTest public abstract class RepositoryTestSupport { } 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 index 2ff3a2f..daf94d7 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/common/utils/FileMockingUtils.java @@ -12,14 +12,16 @@ import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class FileMockingUtils { +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(String fileName) throws IOException { - try (FileInputStream stream = new FileInputStream(FILE_PATH + fileName)) { + 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); } } 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 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..f280b36 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/application/HairStyleSearchServiceTest.java @@ -0,0 +1,184 @@ +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.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(Tag.ROUND); + given(userFindService.getById(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.getById(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(Tag.ROUND); + given(userFindService.getById(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/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..38f64ea --- /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 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; + +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(Long id) { + HairStyle hairStyle = getWomanHairStyle(); + ReflectionTestUtils.setField(hairStyle, "id", id); + return hairStyle; + } + + + public static HairStyle getWomanHairStyle(String name, List tags) { + return HairStyle.createHairStyle( + name, + Sex.WOMAN, + IMAGE_URLS, + tags + ); + } +} 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..1ee4e53 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/hairstyle/presentation/HairStyleApiTest.java @@ -0,0 +1,139 @@ +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("[HairStyle Api 테스트]") +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(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(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 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..00f1fbc --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/photo/application/PhotoServiceTest.java @@ -0,0 +1,87 @@ +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 + @DisplayName("[이미지들을 업로드한다]") + 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 + @DisplayName("[Photo 데이터와 실제 이미지를 리뷰 아이디로 삭제한다]") + 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 + @DisplayName("[특정 리뷰어의 리뷰의 Photo 데이터와 실제 이미지를 삭제한다]") + 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/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/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..fd15b2d --- /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.getFixedManUser(1L); + given(userFindService.getById(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.getFixedManUser(1L); + given(userFindService.getById(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.getFixedManUser(1L); + given(userFindService.getById(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.getFixedManUser(1L); + given(userFindService.getById(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/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..e4db238 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/fixture/PointLogFixture.java @@ -0,0 +1,45 @@ +package com.inq.wishhair.wesharewishhair.point.fixture; + +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; +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); + } + + 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..28f8850 --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/point/presentation/PointControllerTest.java @@ -0,0 +1,78 @@ +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.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; + +@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; + @Autowired + private PointLogRepository pointLogRepository; + @Autowired + private UserRepository userRepository; + + @Test + @DisplayName("[포인트 사용 API 를 호출한다]") + void usePoint() throws Exception { + //given + User user = UserFixture.getFixedManUser(); + Long userId = userRepository.save(user).getId(); + pointLogRepository.save(PointLogFixture.getUsePointLog(user)); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .post(POINT_USE_URL) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(PointLogFixture.getPointUseRequest())) + ); + + //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)); + + //when + ResultActions result = mockMvc.perform( + MockMvcRequestBuilders + .get(POINT_QUERY_URL) + .header(AUTHORIZATION, BEARER + getAccessToken(userId)) + ); + + //then + result.andExpectAll( + status().isOk(), + jsonPath("$.paging").exists(), + jsonPath("$.result").isNotEmpty(), + jsonPath("$.result.size()").value(1) + ); + } +} \ No newline at end of file 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..18b4b3b --- /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.getById(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/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 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 new file mode 100644 index 0000000..d8bfa4c --- /dev/null +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/fixture/ReviewFixture.java @@ -0,0 +1,107 @@ +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.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; +import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@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(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, + Score.S2H, + URLS, + 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 + ); + } + + 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 diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/application/UserServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/application/UserServiceTest.java index 9723ff3..38d40e8 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/application/UserServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/application/UserServiceTest.java @@ -7,19 +7,16 @@ import java.io.IOException; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.function.Executable; -import org.mockito.BDDMockito; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.multipart.MultipartFile; import com.inq.wishhair.wesharewishhair.auth.domain.TokenRepository; +import com.inq.wishhair.wesharewishhair.common.support.MockTestSupport; import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; import com.inq.wishhair.wesharewishhair.global.dto.response.SimpleResponseWrapper; import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; @@ -30,12 +27,10 @@ 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; import com.inq.wishhair.wesharewishhair.user.presentation.dto.request.UserUpdateRequest; -@ExtendWith(MockitoExtension.class) @DisplayName("[UserService 테스트] - Application") -class UserServiceTest { +class UserServiceTest extends MockTestSupport { @InjectMocks private UserService userService; 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 22ff69b..dab80b5 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 @@ -2,7 +2,10 @@ import java.util.ArrayList; -import com.inq.wishhair.wesharewishhair.hairstyle.domain.hashtag.Tag; +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.application.dto.response.MyPageResponse; import com.inq.wishhair.wesharewishhair.user.application.dto.response.UserInfo; import com.inq.wishhair.wesharewishhair.user.application.dto.response.UserInformation; @@ -19,6 +22,7 @@ @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"; @@ -34,6 +38,12 @@ public static User getFixedManUser() { ); } + public static User getFixedManUser(Long id) { + User user = getFixedManUser(); + ReflectionTestUtils.setField(user, "id", id); + return user; + } + public static User getFixedWomanUser() { return User.of( EMAIL, @@ -44,34 +54,40 @@ public static User getFixedWomanUser() { ); } + public static User getFixedWomanUser(Long id) { + User user = getFixedWomanUser(); + ReflectionTestUtils.setField(user, "id", id); + return user; + } + public static SignUpRequest getSignUpRequest() { return new SignUpRequest( - EMAIL, - PW, - NAME, - NICKNAME, + "newMail@naver.com", + "hello1234!", + "name", + "nickname", Sex.MAN ); } public static PasswordRefreshRequest getPasswordRefreshRequest() { return new PasswordRefreshRequest( - EMAIL, - "newPassword1234!" + "hello@naver.com", + "newPassword1234@" ); } public static UserUpdateRequest getUserUpdateRequest() { return new UserUpdateRequest( - "newNick", - Sex.WOMAN + "nickname", + Sex.MAN ); } public static PasswordUpdateRequest getPasswordUpdateRequest() { return new PasswordUpdateRequest( PW, - "newPassword1234!" + "newPw1234!" ); } @@ -93,9 +109,8 @@ public static UserInformation getUserInformation() { } public static UserInfo getUserInfo() { - User user = getFixedManUser(); - user.updateFaceShape(Tag.ROUND); - - return new UserInfo(user); + return new UserInfo( + getFixedManUser() + ); } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserControllerTest.java index f68acc6..032a39d 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserControllerTest.java @@ -6,26 +6,26 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; 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.web.multipart.MultipartFile; import com.inq.wishhair.wesharewishhair.common.support.ApiTestSupport; import com.inq.wishhair.wesharewishhair.common.utils.FileMockingUtils; -import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; import com.inq.wishhair.wesharewishhair.user.application.UserService; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; import com.inq.wishhair.wesharewishhair.user.presentation.dto.request.PasswordRefreshRequest; import com.inq.wishhair.wesharewishhair.user.presentation.dto.request.PasswordUpdateRequest; import com.inq.wishhair.wesharewishhair.user.presentation.dto.request.SignUpRequest; import com.inq.wishhair.wesharewishhair.user.presentation.dto.request.UserUpdateRequest; -@WebMvcTest(value = {UserController.class, SecurityConfig.class}) @DisplayName("[UserController 테스트] - API") class UserControllerTest extends ApiTestSupport { @@ -34,6 +34,9 @@ class UserControllerTest extends ApiTestSupport { private static final String PASSWORD_UPDATE_URL = BASE_URL + "/password"; private static final String UPDATE_FACE_SHAPE_URL = BASE_URL + "/face_shape"; + @Autowired + private MockMvc mockMvc; + @MockBean private UserService userService; @@ -59,11 +62,14 @@ void signUp() throws Exception { @Test @DisplayName("[회원탈퇴 API 를 호출한다]") void deleteUser() throws Exception { + //given + User user = saveUser(); + //then ResultActions actual = mockMvc.perform( MockMvcRequestBuilders .delete(BASE_URL) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then @@ -92,6 +98,7 @@ void refreshPassword() throws Exception { @DisplayName("[회원정보 수정 API 를 호출한다]") void updateUser() throws Exception { //given + User user = saveUser(); UserUpdateRequest request = UserFixture.getUserUpdateRequest(); //when @@ -100,7 +107,7 @@ void updateUser() throws Exception { .patch(BASE_URL) .contentType(MediaType.APPLICATION_JSON) .content(toJson(request)) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then @@ -111,6 +118,7 @@ void updateUser() throws Exception { @DisplayName("[비밀번호 변경 API 를 호출한다]") void updatePassword() throws Exception { //given + User user = saveUser(); PasswordUpdateRequest request = UserFixture.getPasswordUpdateRequest(); //when @@ -119,7 +127,7 @@ void updatePassword() throws Exception { .patch(PASSWORD_UPDATE_URL) .contentType(MediaType.APPLICATION_JSON) .content(toJson(request)) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then @@ -130,6 +138,7 @@ void updatePassword() throws Exception { @DisplayName("[얼굴형 정보 업데이트 API 를 호출한다]") void updateFaceShape() throws Exception { //given + User user = saveUser(); MultipartFile file = FileMockingUtils.createMockMultipartFile("hello1.jpg"); //when @@ -137,7 +146,7 @@ void updateFaceShape() throws Exception { MockMvcRequestBuilders .multipart(HttpMethod.PATCH, UPDATE_FACE_SHAPE_URL) .file((MockMultipartFile)file) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserInfoControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserInfoControllerTest.java index 90c47cb..ca2e419 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserInfoControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/user/presentation/UserInfoControllerTest.java @@ -6,20 +6,20 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +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.common.support.ApiTestSupport; -import com.inq.wishhair.wesharewishhair.global.config.SecurityConfig; import com.inq.wishhair.wesharewishhair.user.application.UserInfoService; import com.inq.wishhair.wesharewishhair.user.application.dto.response.MyPageResponse; import com.inq.wishhair.wesharewishhair.user.application.dto.response.UserInfo; import com.inq.wishhair.wesharewishhair.user.application.dto.response.UserInformation; +import com.inq.wishhair.wesharewishhair.user.domain.entity.User; import com.inq.wishhair.wesharewishhair.user.fixture.UserFixture; -@WebMvcTest(value = {UserInfoController.class, SecurityConfig.class}) @DisplayName("[UserInfoController 테스트] - API") class UserInfoControllerTest extends ApiTestSupport { @@ -28,6 +28,8 @@ class UserInfoControllerTest extends ApiTestSupport { private static final String USER_INFORMATION = BASE_URL + "/info"; private static final String USER_HOME_INFO = BASE_URL + "/home_info"; + @Autowired + private MockMvc mockMvc; @MockBean private UserInfoService userInfoService; @@ -35,15 +37,17 @@ class UserInfoControllerTest extends ApiTestSupport { @DisplayName("[마이페이지 정보 조회 API 를 호출한다]") void getMyPageInfo() throws Exception { //given + User user = saveUser(); + MyPageResponse response = UserFixture.getMyPageResponse(); - given(userInfoService.getMyPageInfo(1L)) + given(userInfoService.getMyPageInfo(user.getId())) .willReturn(response); //when ResultActions actual = mockMvc.perform( MockMvcRequestBuilders .get(MY_PAGE_URL) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then @@ -60,15 +64,17 @@ void getMyPageInfo() throws Exception { @DisplayName("[회원정보 조회 API 를 호출한다]") void getUserInformation() throws Exception { //given + User user = saveUser(); + UserInformation response = UserFixture.getUserInformation(); - given(userInfoService.getUserInformation(1L)) + given(userInfoService.getUserInformation(user.getId())) .willReturn(response); //when ResultActions actual = mockMvc.perform( MockMvcRequestBuilders .get(USER_INFORMATION) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then @@ -85,15 +91,17 @@ void getUserInformation() throws Exception { @DisplayName("[홈페이지 회원정보 조회 API 를 호출한다]") void getUserInfo() throws Exception { //given + User user = saveUser(); + UserInfo response = UserFixture.getUserInfo(); - given(userInfoService.getUserInfo(1L)) + given(userInfoService.getUserInfo(user.getId())) .willReturn(response); //when ResultActions actual = mockMvc.perform( MockMvcRequestBuilders .get(USER_HOME_INFO) - .header(AUTHORIZATION, ACCESS_TOKEN) + .header(AUTHORIZATION, BEARER + getAccessToken(user.getId())) ); //then diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index cb8df18..2ad932c 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,13 +1,13 @@ 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: jpa: hibernate: - ddl-auto: update + ddl-auto: create properties: jakarta: @@ -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/images/hello3.jpg b/src/test/resources/images/hello3.jpg new file mode 100644 index 0000000..3bbcf7f Binary files /dev/null and b/src/test/resources/images/hello3.jpg differ 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