diff --git a/src/main/java/com/mallang/auth/config/AuthConfig.java b/src/main/java/com/mallang/auth/config/AuthConfig.java index cd86d4aa..af469cb1 100644 --- a/src/main/java/com/mallang/auth/config/AuthConfig.java +++ b/src/main/java/com/mallang/auth/config/AuthConfig.java @@ -41,33 +41,21 @@ public void addInterceptors(InterceptorRegistry registry) { private AuthInterceptor setUpAuthInterceptor() { authInterceptor.setNoAuthRequiredConditions( - UriAndMethodAndParamCondition.builder() - .uriPatterns(Set.of("/members/**")) - .httpMethods(Set.of(GET)) - .build(), - UriAndMethodAndParamCondition.builder() - .uriPatterns(Set.of("/posts/**")) - .httpMethods(Set.of(GET)) - .build(), - UriAndMethodAndParamCondition.builder() - .uriPatterns(Set.of("/categories")) - .httpMethods(Set.of(GET)) - .build(), UriAndMethodAndParamCondition.builder() .uriPatterns(Set.of("/comments/**")) .httpMethods(Set.of(POST, PUT, DELETE)) .params(Map.of("unauthenticated", "true")) .build(), UriAndMethodAndParamCondition.builder() - .uriPatterns(Set.of("/comments")) - .httpMethods(Set.of(GET)) - .build(), - UriAndMethodAndParamCondition.builder() - .uriPatterns(Set.of("/blog-subscribes/*")) - .httpMethods(Set.of(GET)) - .build(), - UriAndMethodAndParamCondition.builder() - .uriPatterns(Set.of("/post-stars/**")) + .uriPatterns(Set.of( + "/members/*", + "/posts/**", + "/categories", + "/comments", + "/blog-subscribes/*", + "/post-stars", + "/abouts" + )) .httpMethods(Set.of(GET)) .build() ); diff --git a/src/main/java/com/mallang/blog/application/AboutService.java b/src/main/java/com/mallang/blog/application/AboutService.java new file mode 100644 index 00000000..829aa5e9 --- /dev/null +++ b/src/main/java/com/mallang/blog/application/AboutService.java @@ -0,0 +1,49 @@ +package com.mallang.blog.application; + +import com.mallang.auth.domain.Member; +import com.mallang.auth.domain.MemberRepository; +import com.mallang.blog.application.command.DeleteAboutCommand; +import com.mallang.blog.application.command.UpdateAboutCommand; +import com.mallang.blog.application.command.WriteAboutCommand; +import com.mallang.blog.domain.About; +import com.mallang.blog.domain.AboutRepository; +import com.mallang.blog.domain.AboutValidator; +import com.mallang.blog.domain.Blog; +import com.mallang.blog.domain.BlogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class AboutService { + + private final MemberRepository memberRepository; + private final BlogRepository blogRepository; + private final AboutRepository aboutRepository; + private final AboutValidator aboutValidator; + + public Long write(WriteAboutCommand command) { + Member member = memberRepository.getById(command.memberId()); + Blog blog = blogRepository.getByNameAndOwnerId(command.blogName(), command.memberId()); + About about = command.toAbout(member, blog); + about.write(aboutValidator); + return aboutRepository.save(about) + .getId(); + } + + public void update(UpdateAboutCommand command) { + About about = aboutRepository.getByIdAndWriterIdAndBlogName( + command.aboutId(), command.memberId(), command.blogName() + ); + about.update(command.content()); + } + + public void delete(DeleteAboutCommand command) { + About about = aboutRepository.getByIdAndWriterIdAndBlogName( + command.aboutId(), command.memberId(), command.blogName() + ); + aboutRepository.delete(about); + } +} diff --git a/src/main/java/com/mallang/blog/application/command/DeleteAboutCommand.java b/src/main/java/com/mallang/blog/application/command/DeleteAboutCommand.java new file mode 100644 index 00000000..08a11bea --- /dev/null +++ b/src/main/java/com/mallang/blog/application/command/DeleteAboutCommand.java @@ -0,0 +1,8 @@ +package com.mallang.blog.application.command; + +public record DeleteAboutCommand( + Long aboutId, + Long memberId, + String blogName +) { +} diff --git a/src/main/java/com/mallang/blog/application/command/UpdateAboutCommand.java b/src/main/java/com/mallang/blog/application/command/UpdateAboutCommand.java new file mode 100644 index 00000000..c84aa637 --- /dev/null +++ b/src/main/java/com/mallang/blog/application/command/UpdateAboutCommand.java @@ -0,0 +1,9 @@ +package com.mallang.blog.application.command; + +public record UpdateAboutCommand( + Long aboutId, + Long memberId, + String blogName, + String content +) { +} diff --git a/src/main/java/com/mallang/blog/application/command/WriteAboutCommand.java b/src/main/java/com/mallang/blog/application/command/WriteAboutCommand.java new file mode 100644 index 00000000..018aceb9 --- /dev/null +++ b/src/main/java/com/mallang/blog/application/command/WriteAboutCommand.java @@ -0,0 +1,15 @@ +package com.mallang.blog.application.command; + +import com.mallang.auth.domain.Member; +import com.mallang.blog.domain.About; +import com.mallang.blog.domain.Blog; + +public record WriteAboutCommand( + Long memberId, + String blogName, + String content +) { + public About toAbout(Member member, Blog blog) { + return new About(blog, content, member); + } +} diff --git a/src/main/java/com/mallang/blog/domain/About.java b/src/main/java/com/mallang/blog/domain/About.java new file mode 100644 index 00000000..e681452d --- /dev/null +++ b/src/main/java/com/mallang/blog/domain/About.java @@ -0,0 +1,46 @@ +package com.mallang.blog.domain; + +import static jakarta.persistence.FetchType.LAZY; + +import com.mallang.auth.domain.Member; +import com.mallang.common.domain.CommonDomainModel; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class About extends CommonDomainModel { + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "blog_id", nullable = false, unique = true) + private Blog blog; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "writer_id", nullable = false) + private Member writer; + + public About(Blog blog, String content, Member writer) { + this.blog = blog; + this.content = content; + this.writer = writer; + } + + public void write(AboutValidator validator) { + validator.validateAlreadyExist(blog); + } + + public void update(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/mallang/blog/domain/AboutRepository.java b/src/main/java/com/mallang/blog/domain/AboutRepository.java new file mode 100644 index 00000000..61739c6d --- /dev/null +++ b/src/main/java/com/mallang/blog/domain/AboutRepository.java @@ -0,0 +1,24 @@ +package com.mallang.blog.domain; + +import com.mallang.blog.exception.NotFoundAboutException; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface AboutRepository extends JpaRepository { + + boolean existsByBlog(Blog blog); + + default About getByIdAndWriterIdAndBlogName(Long aboutId, Long memberId, String blogName) { + return findByWriterIdAndBlogName(aboutId, memberId, blogName) + .orElseThrow(NotFoundAboutException::new); + } + + @Query("SELECT a FROM About a WHERE a.id = :aboutId AND a.writer.id = :writerId AND a.blog.name.value = :blogName") + Optional findByWriterIdAndBlogName( + @Param("aboutId") Long aboutId, + @Param("writerId") Long writerId, + @Param("blogName") String blogName + ); +} diff --git a/src/main/java/com/mallang/blog/domain/AboutValidator.java b/src/main/java/com/mallang/blog/domain/AboutValidator.java new file mode 100644 index 00000000..65f6a4a0 --- /dev/null +++ b/src/main/java/com/mallang/blog/domain/AboutValidator.java @@ -0,0 +1,18 @@ +package com.mallang.blog.domain; + +import com.mallang.blog.exception.AlreadyExistAboutException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class AboutValidator { + + private final AboutRepository aboutRepository; + + public void validateAlreadyExist(Blog blog) { + if (aboutRepository.existsByBlog(blog)) { + throw new AlreadyExistAboutException(); + } + } +} diff --git a/src/main/java/com/mallang/blog/exception/AlreadyExistAboutException.java b/src/main/java/com/mallang/blog/exception/AlreadyExistAboutException.java new file mode 100644 index 00000000..7687a8af --- /dev/null +++ b/src/main/java/com/mallang/blog/exception/AlreadyExistAboutException.java @@ -0,0 +1,12 @@ +package com.mallang.blog.exception; + +import com.mallang.common.execption.ErrorCode; +import com.mallang.common.execption.MallangLogException; +import org.springframework.http.HttpStatus; + +public class AlreadyExistAboutException extends MallangLogException { + + public AlreadyExistAboutException() { + super(new ErrorCode(HttpStatus.CONFLICT, "이미 작성된 ABOUT이 있습니다.")); + } +} diff --git a/src/main/java/com/mallang/blog/exception/NotFoundAboutException.java b/src/main/java/com/mallang/blog/exception/NotFoundAboutException.java new file mode 100644 index 00000000..f965ffb4 --- /dev/null +++ b/src/main/java/com/mallang/blog/exception/NotFoundAboutException.java @@ -0,0 +1,17 @@ +package com.mallang.blog.exception; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import com.mallang.common.execption.ErrorCode; +import com.mallang.common.execption.MallangLogException; + +public class NotFoundAboutException extends MallangLogException { + + public NotFoundAboutException(String message) { + super(new ErrorCode(NOT_FOUND, message)); + } + + public NotFoundAboutException() { + super(new ErrorCode(NOT_FOUND, "존재하지 않는 About입니다.")); + } +} diff --git a/src/main/java/com/mallang/blog/presentation/AboutController.java b/src/main/java/com/mallang/blog/presentation/AboutController.java new file mode 100644 index 00000000..eb0dfe3f --- /dev/null +++ b/src/main/java/com/mallang/blog/presentation/AboutController.java @@ -0,0 +1,66 @@ +package com.mallang.blog.presentation; + +import com.mallang.auth.presentation.support.Auth; +import com.mallang.blog.application.AboutService; +import com.mallang.blog.presentation.request.DeleteAboutRequest; +import com.mallang.blog.presentation.request.UpdateAboutRequest; +import com.mallang.blog.presentation.request.WriteAboutRequest; +import com.mallang.blog.query.AboutQueryService; +import com.mallang.blog.query.data.AboutResponse; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/abouts") +@RestController +public class AboutController { + + private final AboutService aboutService; + private final AboutQueryService aboutQueryService; + + @PostMapping + public ResponseEntity write( + @Auth Long memberId, + @RequestBody WriteAboutRequest request + ) { + Long aboutId = aboutService.write(request.toCommand(memberId)); + return ResponseEntity.created(URI.create("/abouts/" + aboutId)).build(); + } + + @PutMapping("/{id}") + public ResponseEntity update( + @PathVariable("id") Long aboutId, + @Auth Long memberId, + @RequestBody UpdateAboutRequest request + ) { + aboutService.update(request.toCommand(aboutId, memberId)); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete( + @PathVariable("id") Long aboutId, + @Auth Long memberId, + @RequestBody DeleteAboutRequest request + ) { + aboutService.delete(request.toCommand(aboutId, memberId)); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity findByBlogName( + @RequestParam(name = "blogName") String blogName + ) { + return ResponseEntity.ok(aboutQueryService.findByBlogName(blogName)); + } +} diff --git a/src/main/java/com/mallang/blog/presentation/request/DeleteAboutRequest.java b/src/main/java/com/mallang/blog/presentation/request/DeleteAboutRequest.java new file mode 100644 index 00000000..5d2e434e --- /dev/null +++ b/src/main/java/com/mallang/blog/presentation/request/DeleteAboutRequest.java @@ -0,0 +1,11 @@ +package com.mallang.blog.presentation.request; + +import com.mallang.blog.application.command.DeleteAboutCommand; + +public record DeleteAboutRequest( + String blogName +) { + public DeleteAboutCommand toCommand(Long aboutId, Long memberId) { + return new DeleteAboutCommand(aboutId, memberId, blogName); + } +} diff --git a/src/main/java/com/mallang/blog/presentation/request/UpdateAboutRequest.java b/src/main/java/com/mallang/blog/presentation/request/UpdateAboutRequest.java new file mode 100644 index 00000000..674a206f --- /dev/null +++ b/src/main/java/com/mallang/blog/presentation/request/UpdateAboutRequest.java @@ -0,0 +1,12 @@ +package com.mallang.blog.presentation.request; + +import com.mallang.blog.application.command.UpdateAboutCommand; + +public record UpdateAboutRequest( + String blogName, + String content +) { + public UpdateAboutCommand toCommand(Long aboutId, Long memberId) { + return new UpdateAboutCommand(aboutId, memberId, blogName, content); + } +} diff --git a/src/main/java/com/mallang/blog/presentation/request/WriteAboutRequest.java b/src/main/java/com/mallang/blog/presentation/request/WriteAboutRequest.java new file mode 100644 index 00000000..6164ef81 --- /dev/null +++ b/src/main/java/com/mallang/blog/presentation/request/WriteAboutRequest.java @@ -0,0 +1,12 @@ +package com.mallang.blog.presentation.request; + +import com.mallang.blog.application.command.WriteAboutCommand; + +public record WriteAboutRequest( + String blogName, + String content +) { + public WriteAboutCommand toCommand(Long memberId) { + return new WriteAboutCommand(memberId, blogName, content); + } +} diff --git a/src/main/java/com/mallang/blog/query/AboutQueryService.java b/src/main/java/com/mallang/blog/query/AboutQueryService.java new file mode 100644 index 00000000..18ab1624 --- /dev/null +++ b/src/main/java/com/mallang/blog/query/AboutQueryService.java @@ -0,0 +1,19 @@ +package com.mallang.blog.query; + +import com.mallang.blog.query.dao.AboutResponseDao; +import com.mallang.blog.query.data.AboutResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class AboutQueryService { + + private final AboutResponseDao aboutResponseDao; + + public AboutResponse findByBlogName(String blogName) { + return aboutResponseDao.find(blogName); + } +} diff --git a/src/main/java/com/mallang/blog/query/dao/AboutResponseDao.java b/src/main/java/com/mallang/blog/query/dao/AboutResponseDao.java new file mode 100644 index 00000000..4778ee2a --- /dev/null +++ b/src/main/java/com/mallang/blog/query/dao/AboutResponseDao.java @@ -0,0 +1,19 @@ +package com.mallang.blog.query.dao; + +import com.mallang.blog.query.dao.support.AboutQuerySupport; +import com.mallang.blog.query.data.AboutResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Component +public class AboutResponseDao { + + private final AboutQuerySupport aboutQuerySupport; + + public AboutResponse find(String blogName) { + return AboutResponse.from(aboutQuerySupport.getByBlogName(blogName)); + } +} diff --git a/src/main/java/com/mallang/blog/query/dao/support/AboutQuerySupport.java b/src/main/java/com/mallang/blog/query/dao/support/AboutQuerySupport.java new file mode 100644 index 00000000..4880c489 --- /dev/null +++ b/src/main/java/com/mallang/blog/query/dao/support/AboutQuerySupport.java @@ -0,0 +1,19 @@ +package com.mallang.blog.query.dao.support; + +import com.mallang.blog.domain.About; +import com.mallang.blog.exception.NotFoundAboutException; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface AboutQuerySupport extends JpaRepository { + + default About getByBlogName(String blogName) { + return findByBlogName(blogName) + .orElseThrow(NotFoundAboutException::new); + } + + @Query("SELECT a FROM About a WHERE a.blog.name.value = :blogName") + Optional findByBlogName(@Param("blogName") String blogName); +} diff --git a/src/main/java/com/mallang/blog/query/data/AboutResponse.java b/src/main/java/com/mallang/blog/query/data/AboutResponse.java new file mode 100644 index 00000000..940f2b1e --- /dev/null +++ b/src/main/java/com/mallang/blog/query/data/AboutResponse.java @@ -0,0 +1,21 @@ +package com.mallang.blog.query.data; + +import com.mallang.blog.domain.About; +import java.time.LocalDateTime; +import lombok.Builder; + +@Builder +public record AboutResponse( + Long id, + String content, + LocalDateTime createdDate +) { + + public static AboutResponse from(About about) { + return AboutResponse.builder() + .id(about.getId()) + .content(about.getContent()) + .createdDate(about.getCreatedDate()) + .build(); + } +} diff --git a/src/test/java/com/mallang/acceptance/blog/AboutAcceptanceSteps.java b/src/test/java/com/mallang/acceptance/blog/AboutAcceptanceSteps.java new file mode 100644 index 00000000..e0be5712 --- /dev/null +++ b/src/test/java/com/mallang/acceptance/blog/AboutAcceptanceSteps.java @@ -0,0 +1,48 @@ +package com.mallang.acceptance.blog; + +import static com.mallang.acceptance.AcceptanceSteps.given; + +import com.mallang.blog.presentation.request.DeleteAboutRequest; +import com.mallang.blog.presentation.request.UpdateAboutRequest; +import com.mallang.blog.presentation.request.WriteAboutRequest; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +@SuppressWarnings("NonAsciiCharacters") +public class AboutAcceptanceSteps { + + public static ExtractableResponse 블로그_소개_작성_요청( + String 세션_ID, + WriteAboutRequest 소개_작성_요청 + ) { + return given(세션_ID) + .body(소개_작성_요청) + .post("/abouts") + .then().log().all() + .extract(); + } + + public static ExtractableResponse 블로그_소개_수정_요청( + String 세션_ID, + Long 블로그_소개_ID, + UpdateAboutRequest 소개_수정_요청 + ) { + return given(세션_ID) + .body(소개_수정_요청) + .put("/abouts/{id}", 블로그_소개_ID) + .then().log().all() + .extract(); + } + + public static ExtractableResponse 블로그_소개_삭제_요청( + String 세션_ID, + Long 블로그_소개_ID, + DeleteAboutRequest 소개_삭제_요청 + ) { + return given(세션_ID) + .body(소개_삭제_요청) + .delete("/abouts/{id}", 블로그_소개_ID) + .then().log().all() + .extract(); + } +} diff --git a/src/test/java/com/mallang/acceptance/blog/AboutAcceptanceTest.java b/src/test/java/com/mallang/acceptance/blog/AboutAcceptanceTest.java new file mode 100644 index 00000000..cc69fc04 --- /dev/null +++ b/src/test/java/com/mallang/acceptance/blog/AboutAcceptanceTest.java @@ -0,0 +1,172 @@ +package com.mallang.acceptance.blog; + +import static com.mallang.acceptance.AcceptanceSteps.ID를_추출한다; +import static com.mallang.acceptance.AcceptanceSteps.given; +import static com.mallang.acceptance.AcceptanceSteps.본문_없음; +import static com.mallang.acceptance.AcceptanceSteps.생성됨; +import static com.mallang.acceptance.AcceptanceSteps.응답_상태를_검증한다; +import static com.mallang.acceptance.AcceptanceSteps.정상_처리; +import static com.mallang.acceptance.AcceptanceSteps.중복됨; +import static com.mallang.acceptance.AcceptanceSteps.찾을수_없음; +import static com.mallang.acceptance.auth.AuthAcceptanceSteps.회원가입과_로그인_후_세션_ID_반환; +import static com.mallang.acceptance.blog.AboutAcceptanceSteps.블로그_소개_삭제_요청; +import static com.mallang.acceptance.blog.AboutAcceptanceSteps.블로그_소개_수정_요청; +import static com.mallang.acceptance.blog.AboutAcceptanceSteps.블로그_소개_작성_요청; +import static com.mallang.acceptance.blog.BlogAcceptanceSteps.블로그_개설; +import static org.assertj.core.api.Assertions.assertThat; + +import com.mallang.acceptance.AcceptanceTest; +import com.mallang.blog.presentation.request.DeleteAboutRequest; +import com.mallang.blog.presentation.request.UpdateAboutRequest; +import com.mallang.blog.presentation.request.WriteAboutRequest; +import com.mallang.blog.query.data.AboutResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("소개(About) 인수테스트") +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +public class AboutAcceptanceTest extends AcceptanceTest { + + private String 말랑_세션_ID; + private String 동훈_세션_ID; + private String 말랑_블로그_이름; + private WriteAboutRequest 말랑_블로그_소개_작성_요청; + + @Override + @BeforeEach + protected void setUp() { + super.setUp(); + 말랑_세션_ID = 회원가입과_로그인_후_세션_ID_반환("말랑"); + 동훈_세션_ID = 회원가입과_로그인_후_세션_ID_반환("동훈"); + 말랑_블로그_이름 = 블로그_개설(말랑_세션_ID, "mallang-log"); + 말랑_블로그_소개_작성_요청 = new WriteAboutRequest(말랑_블로그_이름, "말랑입니다."); + } + + @Nested + class 소개_작성_API { + + @Test + void 첫_작성이라면_작성된다() { + // when + var 응답 = 블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청); + + // then + 응답_상태를_검증한다(응답, 생성됨); + } + + @Test + void 블로그에_이미_작성된_소개가_있으면_예외() { + // given + 블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청); + + // when + var 응답 = 블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청); + + // then + 응답_상태를_검증한다(응답, 중복됨); + } + + @Test + void 다른_사람의_블로그에_작성하면_예외() { + // when + var 응답 = 블로그_소개_작성_요청(동훈_세션_ID, 말랑_블로그_소개_작성_요청); + + // then + 응답_상태를_검증한다(응답, 찾을수_없음); + } + } + + @Nested + class 소개_수정_API { + + private Long 말랑_블로그_소개_ID; + private UpdateAboutRequest 말랑_블로그_소개_수정_요청; + + @BeforeEach + void setUp() { + 말랑_블로그_소개_ID = ID를_추출한다(블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청)); + 말랑_블로그_소개_수정_요청 = new UpdateAboutRequest(말랑_블로그_이름, "수정입니다."); + } + + @Test + void 자신의_소개라면_수정된다() { + // given + 블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청); + + // when + var 응답 = 블로그_소개_수정_요청(말랑_세션_ID, 말랑_블로그_소개_ID, 말랑_블로그_소개_수정_요청); + + // then + 응답_상태를_검증한다(응답, 정상_처리); + } + + @Test + void 자신의_소개가_아니면_예외() { + // given + 블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청); + + // when + var 응답 = 블로그_소개_수정_요청(동훈_세션_ID, 말랑_블로그_소개_ID, 말랑_블로그_소개_수정_요청); + + // then + 응답_상태를_검증한다(응답, 찾을수_없음); + } + } + + @Nested + class 소개_삭제_API { + + private Long 말랑_블로그_소개_ID; + private DeleteAboutRequest 말랑_블로그_소개_삭제_요청; + + @BeforeEach + void setUp() { + 말랑_블로그_소개_ID = ID를_추출한다(블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청)); + 말랑_블로그_소개_삭제_요청 = new DeleteAboutRequest(말랑_블로그_이름); + } + + @Test + void 자신의_소개라면_삭제된다() { + // when + var 응답 = 블로그_소개_삭제_요청(말랑_세션_ID, 말랑_블로그_소개_ID, 말랑_블로그_소개_삭제_요청); + + // then + 응답_상태를_검증한다(응답, 본문_없음); + } + + @Test + void 자신의_소개가_아니면_예외() { + // when + var 응답 = 블로그_소개_삭제_요청(동훈_세션_ID, 말랑_블로그_소개_ID, 말랑_블로그_소개_삭제_요청); + + // then + 응답_상태를_검증한다(응답, 찾을수_없음); + } + } + + @Nested + class 소개_조회_API { + + @Test + void 소개를_조회한다() { + // given + var 소개_ID = ID를_추출한다(블로그_소개_작성_요청(말랑_세션_ID, 말랑_블로그_소개_작성_요청)); + + // when + var 응답 = given() + .queryParam("blogName", 말랑_블로그_이름) + .get("/abouts") + .then().log().all() + .extract(); + + // then + AboutResponse aboutResponse = 응답.as(AboutResponse.class); + assertThat(aboutResponse.id()).isEqualTo(소개_ID); + } + } +} diff --git a/src/test/java/com/mallang/blog/application/AboutServiceTest.java b/src/test/java/com/mallang/blog/application/AboutServiceTest.java new file mode 100644 index 00000000..f037bc92 --- /dev/null +++ b/src/test/java/com/mallang/blog/application/AboutServiceTest.java @@ -0,0 +1,167 @@ +package com.mallang.blog.application; + +import static com.mallang.auth.MemberFixture.동훈; +import static com.mallang.auth.MemberFixture.말랑; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.mallang.auth.domain.Member; +import com.mallang.auth.domain.MemberRepository; +import com.mallang.blog.application.command.DeleteAboutCommand; +import com.mallang.blog.application.command.UpdateAboutCommand; +import com.mallang.blog.application.command.WriteAboutCommand; +import com.mallang.blog.domain.About; +import com.mallang.blog.domain.AboutRepository; +import com.mallang.blog.domain.Blog; +import com.mallang.blog.domain.BlogRepository; +import com.mallang.blog.exception.AlreadyExistAboutException; +import com.mallang.blog.exception.NotFoundAboutException; +import com.mallang.blog.exception.NotFoundBlogException; +import com.mallang.common.ServiceTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@DisplayName("소개 서비스(AboutService) 은(는)") +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@ServiceTest +class AboutServiceTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private BlogRepository blogRepository; + + @Autowired + private AboutRepository aboutRepository; + + @Autowired + private AboutService aboutService; + + private Member member; + private Member other; + private Blog blog; + + @BeforeEach + void setUp() { + member = memberRepository.save(말랑()); + other = memberRepository.save(동훈()); + blog = blogRepository.save(new Blog("mallang-log", member)); + } + + @Nested + class 소개_작성_시 { + + @Test + void 첫_작성이라면_작성된다() { + // given + WriteAboutCommand command = new WriteAboutCommand(member.getId(), blog.getName(), "안녕하세요"); + + // when & then + assertDoesNotThrow(() -> { + aboutService.write(command); + }); + } + + @Test + void 블로그에_이미_작성된_소개가_있으면_예외() { + // given + WriteAboutCommand command = new WriteAboutCommand(member.getId(), blog.getName(), "안녕하세요"); + aboutService.write(command); + + // when & then + assertThatThrownBy(() -> + aboutService.write(command) + ).isInstanceOf(AlreadyExistAboutException.class); + } + + @Test + void 타인의_블로그에_작서하려는_경우_예외() { + // given + WriteAboutCommand command = new WriteAboutCommand(other.getId(), blog.getName(), "안녕하세요"); + + // when & then + assertThatThrownBy(() -> + aboutService.write(command) + ).isInstanceOf(NotFoundBlogException.class); + } + } + + @Nested + class 소개_수정_시 { + + private Long aboutId; + + @BeforeEach + void setUp() { + WriteAboutCommand command = new WriteAboutCommand(member.getId(), blog.getName(), "안녕하세요"); + aboutId = aboutService.write(command); + } + + @Test + void 자신의_소개라면_수정된다() { + // given + UpdateAboutCommand command = new UpdateAboutCommand(aboutId, member.getId(), blog.getName(), "수정"); + + // when + aboutService.update(command); + + // then + About about = aboutRepository.findById(aboutId).get(); + assertThat(about.getContent()).isEqualTo("수정"); + } + + @Test + void 자신의_소개가_아니면_예외() { + // given + UpdateAboutCommand command = new UpdateAboutCommand(aboutId, other.getId(), blog.getName(), "수정"); + + // when & then + assertThatThrownBy(() -> { + aboutService.update(command); + }).isInstanceOf(NotFoundAboutException.class); + } + } + + @Nested + class 소개_삭제_시 { + + private Long aboutId; + + @BeforeEach + void setUp() { + WriteAboutCommand command = new WriteAboutCommand(member.getId(), blog.getName(), "안녕하세요"); + aboutId = aboutService.write(command); + } + + @Test + void 자신의_소개라면_삭제된다() { + // given + DeleteAboutCommand command = new DeleteAboutCommand(aboutId, member.getId(), blog.getName()); + + // when + aboutService.delete(command); + + // then + assertThat(aboutRepository.findById(aboutId)).isEmpty(); + } + + @Test + void 자신의_소개가_아니면_예외() { + // given + DeleteAboutCommand command = new DeleteAboutCommand(aboutId, other.getId(), blog.getName()); + + // when & then + assertThatThrownBy(() -> { + aboutService.delete(command); + }).isInstanceOf(NotFoundAboutException.class); + } + } +} diff --git a/src/test/java/com/mallang/blog/domain/AboutTest.java b/src/test/java/com/mallang/blog/domain/AboutTest.java new file mode 100644 index 00000000..aa77b7f5 --- /dev/null +++ b/src/test/java/com/mallang/blog/domain/AboutTest.java @@ -0,0 +1,70 @@ +package com.mallang.blog.domain; + +import static com.mallang.auth.MemberFixture.동훈; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; + +import com.mallang.auth.domain.Member; +import com.mallang.blog.exception.AlreadyExistAboutException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("블로그 소개(About) 은(는)") +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class AboutTest { + + private final AboutValidator aboutValidator = mock(AboutValidator.class); + private final Member member = 동훈(); + private final Blog blog = Blog.builder() + .name("mallang") + .owner(member) + .build(); + + @Nested + class 작성_시 { + + @Test + void 이미_작성된_About_이_있으면_예외() { + // given + willThrow(AlreadyExistAboutException.class) + .given(aboutValidator) + .validateAlreadyExist(blog); + About about = new About(blog, "안녕하세요", member); + + // when & then + assertThatThrownBy(() -> { + about.write(aboutValidator); + }).isInstanceOf(AlreadyExistAboutException.class); + } + + @Test + void 작성된_About_이_없다면_성공() { + // given + About about = new About(blog, "안녕하세요", member); + + // when & then + assertDoesNotThrow(() -> { + about.write(aboutValidator); + }); + } + } + + @Test + void 수정_시_내용을_변경한다() { + // given + About about = new About(blog, "안녕하세요", member); + + // when + about.update("1234"); + + // then + assertThat(about.getContent()).isEqualTo("1234"); + } +} diff --git a/src/test/java/com/mallang/blog/domain/AboutValidatorTest.java b/src/test/java/com/mallang/blog/domain/AboutValidatorTest.java new file mode 100644 index 00000000..c8250ece --- /dev/null +++ b/src/test/java/com/mallang/blog/domain/AboutValidatorTest.java @@ -0,0 +1,40 @@ +package com.mallang.blog.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import com.mallang.blog.exception.AlreadyExistAboutException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@DisplayName("소개 검증기(AboutValidator) 은(는)") +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class AboutValidatorTest { + + private final Blog blog1 = mock(Blog.class); + private final Blog blog2 = mock(Blog.class); + private final AboutRepository aboutRepository = mock(AboutRepository.class); + private final AboutValidator aboutValidator = new AboutValidator(aboutRepository); + + @Test + void 블로그에는_하나의_About만_존재해야_한다() { + // given + given(aboutRepository.existsByBlog(blog1)) + .willReturn(true); + given(aboutRepository.existsByBlog(blog2)) + .willReturn(false); + + // when & then + assertThatThrownBy(() -> { + aboutValidator.validateAlreadyExist(blog1); + }).isInstanceOf(AlreadyExistAboutException.class); + assertDoesNotThrow(() -> { + aboutValidator.validateAlreadyExist(blog2); + }); + } +}