From 185ea824563e7c9a5d05f2889b80dcf708ce1dc5 Mon Sep 17 00:00:00 2001 From: jiwookim5757 <61129436+jiwookim5757@users.noreply.github.com> Date: Thu, 23 Jul 2020 20:35:18 +0900 Subject: [PATCH] =?UTF-8?q?=ED=83=9C=EA=B7=B8=20api=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#31] Implement domain layer - Add domain `Post` - Add domain `Tag` * [#31] Obey to checkstyle * [#develop] add other content api * [#62] add tag-api module settings * [#62] Implement tag api * [#62] delete duplicated settings * [#62] refactor codes * [#62] add custom exception and exception controller - TagNotFoundException.java - ExceptionController.java * [#62] delete unused statement * [#62] add Tag tests * [#62] add TagContent tests * [#62] fix variable name - 'tableContent' to 'contentType' * [#62] add tagContent test * [#62] change tagContent api * [#62] add TagService test * [#62] add TagController test * [#62] add tag test fixture * [#62] fix service - list to page * [#62] add restdocs Co-authored-by: Jiwoo,Kim --- .../exception/ExceptionController.java | 18 ++ tag-api/build.gradle | 27 +++ tag-api/docs/docinfo.html | 5 + tag-api/docs/index.adoc | 42 +++++ .../exception/ExceptionController.java | 14 ++ .../java/community/tag/TagApplication.java | 11 ++ .../java/community/tag/api/TagController.java | 51 ++++++ .../tag/api/dto/TagContentRequestDto.java | 14 ++ .../tag/api/dto/TagContentResponseDto.java | 21 +++ .../community/tag/api/dto/TagRequestDto.java | 10 ++ .../community/tag/api/dto/TagResponseDto.java | 19 +++ .../main/java/community/tag/domain/Tag.java | 31 ++++ .../java/community/tag/domain/TagContent.java | 38 +++++ .../tag/domain/TagContentRepository.java | 12 ++ .../community/tag/domain/TagRepository.java | 11 ++ .../tag/exception/TagNotFoundException.java | 9 + .../community/tag/service/TagService.java | 77 +++++++++ .../community/tag/api/TagControllerTest.java | 149 +++++++++++++++++ .../tag/api/dto/TagContentRequestDtoTest.java | 21 +++ .../tag/api/dto/TagRequestDtoTest.java | 27 +++ .../tag/api/dto/TagResponseDtoTest.java | 9 + .../community/tag/domain/TagContentTest.java | 92 +++++++++++ .../java/community/tag/domain/TagTest.java | 45 +++++ .../community/tag/service/TagServiceTest.java | 154 ++++++++++++++++++ 24 files changed, 907 insertions(+) create mode 100644 app/app-content/src/main/java/org/kiworkshop/community/content/simplelife/exception/ExceptionController.java create mode 100644 tag-api/build.gradle create mode 100644 tag-api/docs/docinfo.html create mode 100644 tag-api/docs/index.adoc create mode 100644 tag-api/src/main/java/community/exception/ExceptionController.java create mode 100644 tag-api/src/main/java/community/tag/TagApplication.java create mode 100644 tag-api/src/main/java/community/tag/api/TagController.java create mode 100644 tag-api/src/main/java/community/tag/api/dto/TagContentRequestDto.java create mode 100644 tag-api/src/main/java/community/tag/api/dto/TagContentResponseDto.java create mode 100644 tag-api/src/main/java/community/tag/api/dto/TagRequestDto.java create mode 100644 tag-api/src/main/java/community/tag/api/dto/TagResponseDto.java create mode 100644 tag-api/src/main/java/community/tag/domain/Tag.java create mode 100644 tag-api/src/main/java/community/tag/domain/TagContent.java create mode 100644 tag-api/src/main/java/community/tag/domain/TagContentRepository.java create mode 100644 tag-api/src/main/java/community/tag/domain/TagRepository.java create mode 100644 tag-api/src/main/java/community/tag/exception/TagNotFoundException.java create mode 100644 tag-api/src/main/java/community/tag/service/TagService.java create mode 100644 tag-api/src/test/java/community/tag/api/TagControllerTest.java create mode 100644 tag-api/src/test/java/community/tag/api/dto/TagContentRequestDtoTest.java create mode 100644 tag-api/src/test/java/community/tag/api/dto/TagRequestDtoTest.java create mode 100644 tag-api/src/test/java/community/tag/api/dto/TagResponseDtoTest.java create mode 100644 tag-api/src/test/java/community/tag/domain/TagContentTest.java create mode 100644 tag-api/src/test/java/community/tag/domain/TagTest.java create mode 100644 tag-api/src/test/java/community/tag/service/TagServiceTest.java diff --git a/app/app-content/src/main/java/org/kiworkshop/community/content/simplelife/exception/ExceptionController.java b/app/app-content/src/main/java/org/kiworkshop/community/content/simplelife/exception/ExceptionController.java new file mode 100644 index 00000000..32a4b44e --- /dev/null +++ b/app/app-content/src/main/java/org/kiworkshop/community/content/simplelife/exception/ExceptionController.java @@ -0,0 +1,18 @@ +package org.kiworkshop.community.content.simplelife.exception; + +import org.kiworkshop.community.common.dto.ApiError; +import org.kiworkshop.community.common.exception.CommonExceptionController; +import org.kiworkshop.community.content.simplelife.article.exception.SimpleArticleNotFoundException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +@ControllerAdvice +@ResponseBody +public class ExceptionController extends CommonExceptionController { + @Override + @ExceptionHandler(SimpleArticleNotFoundException.class) + public ApiError handleNotFound(RuntimeException e) { + return super.handleNotFound(e); + } +} diff --git a/tag-api/build.gradle b/tag-api/build.gradle new file mode 100644 index 00000000..30c421e5 --- /dev/null +++ b/tag-api/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' +} + +group 'org.kiworkshop' +version '0.0.1-SNAPSHOT' + +sourceCompatibility = 11 + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':common') + + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + runtimeOnly 'com.h2database:h2' +} + +asciidoctor { + inputs.dir snippetsDir + sourceDir 'docs' + outputDir = file('build/docs') + dependsOn test +} \ No newline at end of file diff --git a/tag-api/docs/docinfo.html b/tag-api/docs/docinfo.html new file mode 100644 index 00000000..32a00711 --- /dev/null +++ b/tag-api/docs/docinfo.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/tag-api/docs/index.adoc b/tag-api/docs/index.adoc new file mode 100644 index 00000000..cee9eeb4 --- /dev/null +++ b/tag-api/docs/index.adoc @@ -0,0 +1,42 @@ += Kiworkshop community Mother-API Docs +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +[[overview]] += Overview + +[[overview-http-verbs]] +== HTTP verbs +[cols="20%,80%"] +|=== +| Verb | Usage + +| `GET` +| Used to retrieve a resource + +| `POST` +| Used to create a new resource + +| `PUT` +| Used to update an existing resource, full updates only + +| `DELETE` +| Used to delete an existing resource +|=== + += Resources + +[[resources-notice]] +== Notice API + +[[resource-myangPost-notice]] + +=== Read a notice +A `GET` request to read a notice. + +operation::/tags/create-a-tag[snippets='http-request,path-parameters,http-response,response-fields'] + diff --git a/tag-api/src/main/java/community/exception/ExceptionController.java b/tag-api/src/main/java/community/exception/ExceptionController.java new file mode 100644 index 00000000..140d0649 --- /dev/null +++ b/tag-api/src/main/java/community/exception/ExceptionController.java @@ -0,0 +1,14 @@ +package community.exception; + +import community.common.dto.ApiError; +import community.common.exception.CommonExceptionController; +import community.tag.exception.TagNotFoundException; +import org.springframework.web.bind.annotation.ExceptionHandler; + +public class ExceptionController extends CommonExceptionController { + @Override + @ExceptionHandler(TagNotFoundException.class) + protected ApiError handleNotFound(RuntimeException e) { + return super.handleNotFound(e); + } +} diff --git a/tag-api/src/main/java/community/tag/TagApplication.java b/tag-api/src/main/java/community/tag/TagApplication.java new file mode 100644 index 00000000..b3f368ac --- /dev/null +++ b/tag-api/src/main/java/community/tag/TagApplication.java @@ -0,0 +1,11 @@ +package community.tag; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TagApplication { + public static void main(String[] args) { + SpringApplication.run(TagApplication.class, args); + } +} diff --git a/tag-api/src/main/java/community/tag/api/TagController.java b/tag-api/src/main/java/community/tag/api/TagController.java new file mode 100644 index 00000000..049f0fef --- /dev/null +++ b/tag-api/src/main/java/community/tag/api/TagController.java @@ -0,0 +1,51 @@ +package community.tag.api; + +import community.tag.api.dto.TagContentRequestDto; +import community.tag.api.dto.TagContentResponseDto; +import community.tag.api.dto.TagRequestDto; +import community.tag.api.dto.TagResponseDto; +import community.tag.service.TagService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +@CrossOrigin +@RequiredArgsConstructor +@RestController +@RequestMapping("/tags") +public class TagController { + private final TagService tagService; + + @GetMapping + public Page readTagPage( + @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable + ) { + return tagService.readTagPage(pageable); + } + + @PostMapping + public Long create(@RequestBody @Valid TagRequestDto tagRequestDto) { + return tagService.createTagIfAbsent(tagRequestDto); + } + + @GetMapping("/{contentType}/{contentId}") + public List readTags(@PathVariable String contentType, @PathVariable Long contentId) { + return tagService.readTagsByTagContent(contentType, contentId); + } + + @GetMapping("/{tagName}") + public List readTagContents(@PathVariable String tagName) { + return tagService.readTagContentsByTag(tagName); + } + + @PostMapping("/tagContents") + public List createTagContents(@RequestBody @Valid TagContentRequestDto tagContentRequestDto) { + return tagService.createTagContents(tagContentRequestDto); + } +} diff --git a/tag-api/src/main/java/community/tag/api/dto/TagContentRequestDto.java b/tag-api/src/main/java/community/tag/api/dto/TagContentRequestDto.java new file mode 100644 index 00000000..bcf308e9 --- /dev/null +++ b/tag-api/src/main/java/community/tag/api/dto/TagContentRequestDto.java @@ -0,0 +1,14 @@ +package community.tag.api.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +public class TagContentRequestDto { + private @NotEmpty List<@NotEmpty String> tagNames; + private @NotEmpty String contentType; + private @NotNull Long contentId; +} diff --git a/tag-api/src/main/java/community/tag/api/dto/TagContentResponseDto.java b/tag-api/src/main/java/community/tag/api/dto/TagContentResponseDto.java new file mode 100644 index 00000000..48b8bb5e --- /dev/null +++ b/tag-api/src/main/java/community/tag/api/dto/TagContentResponseDto.java @@ -0,0 +1,21 @@ +package community.tag.api.dto; + +import community.tag.domain.TagContent; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class TagContentResponseDto { + private String contentType; + private Long contentId; + + private TagContentResponseDto(TagContent tagContent) { + this.contentType = tagContent.getContentType(); + this.contentId = tagContent.getContentId(); + } + + public static TagContentResponseDto from(TagContent tagContent) { + return new TagContentResponseDto(tagContent); + } +} diff --git a/tag-api/src/main/java/community/tag/api/dto/TagRequestDto.java b/tag-api/src/main/java/community/tag/api/dto/TagRequestDto.java new file mode 100644 index 00000000..4b156521 --- /dev/null +++ b/tag-api/src/main/java/community/tag/api/dto/TagRequestDto.java @@ -0,0 +1,10 @@ +package community.tag.api.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotEmpty; + +@Getter +public class TagRequestDto { + private @NotEmpty String name; +} diff --git a/tag-api/src/main/java/community/tag/api/dto/TagResponseDto.java b/tag-api/src/main/java/community/tag/api/dto/TagResponseDto.java new file mode 100644 index 00000000..1aa6ca0f --- /dev/null +++ b/tag-api/src/main/java/community/tag/api/dto/TagResponseDto.java @@ -0,0 +1,19 @@ +package community.tag.api.dto; + +import community.tag.domain.Tag; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class TagResponseDto { + private String name; + + private TagResponseDto(Tag tag) { + this.name = tag.getName(); + } + + public static TagResponseDto from(Tag tag) { + return new TagResponseDto(tag); + } +} diff --git a/tag-api/src/main/java/community/tag/domain/Tag.java b/tag-api/src/main/java/community/tag/domain/Tag.java new file mode 100644 index 00000000..0a0077b2 --- /dev/null +++ b/tag-api/src/main/java/community/tag/domain/Tag.java @@ -0,0 +1,31 @@ +package community.tag.domain; + +import community.common.model.BaseEntity; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.Assert; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Getter +@Entity +@NoArgsConstructor +public class Tag extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + private Tag(String name) { + Assert.hasLength(name, "name should not be empty"); + + this.name = name; + } + + public static Tag of(String name) { + return new Tag(name); + } +} diff --git a/tag-api/src/main/java/community/tag/domain/TagContent.java b/tag-api/src/main/java/community/tag/domain/TagContent.java new file mode 100644 index 00000000..13bcba73 --- /dev/null +++ b/tag-api/src/main/java/community/tag/domain/TagContent.java @@ -0,0 +1,38 @@ +package community.tag.domain; + +import community.common.model.BaseEntity; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.Assert; + +import javax.persistence.*; + +@Getter +@Entity +@NoArgsConstructor +public class TagContent extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne + @JoinColumn(name = "tag_id") + private Tag tag; + private String contentType; + private Long contentId; + + @Builder + private TagContent( + Tag tag, + String contentType, + Long contentId + ) { + Assert.notNull(tag, "tag should not be null."); + Assert.hasLength(contentType, "contentType should not be empty."); + Assert.notNull(contentId, "contentId should not be null."); + + this.tag = tag; + this.contentType = contentType; + this.contentId = contentId; + } +} diff --git a/tag-api/src/main/java/community/tag/domain/TagContentRepository.java b/tag-api/src/main/java/community/tag/domain/TagContentRepository.java new file mode 100644 index 00000000..ff8894bf --- /dev/null +++ b/tag-api/src/main/java/community/tag/domain/TagContentRepository.java @@ -0,0 +1,12 @@ +package community.tag.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TagContentRepository extends JpaRepository { + + List findByContentTypeAndContentId(String contentType, Long contentId); + + List findByTag(Tag tag); +} diff --git a/tag-api/src/main/java/community/tag/domain/TagRepository.java b/tag-api/src/main/java/community/tag/domain/TagRepository.java new file mode 100644 index 00000000..2969a502 --- /dev/null +++ b/tag-api/src/main/java/community/tag/domain/TagRepository.java @@ -0,0 +1,11 @@ +package community.tag.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TagRepository extends JpaRepository { + boolean existsTagByName(String name); + + Optional findByName(String name); +} diff --git a/tag-api/src/main/java/community/tag/exception/TagNotFoundException.java b/tag-api/src/main/java/community/tag/exception/TagNotFoundException.java new file mode 100644 index 00000000..ca6158ad --- /dev/null +++ b/tag-api/src/main/java/community/tag/exception/TagNotFoundException.java @@ -0,0 +1,9 @@ +package community.tag.exception; + +import javax.persistence.EntityNotFoundException; + +public class TagNotFoundException extends EntityNotFoundException { + public TagNotFoundException(String name) { + super("post id " + name + " has not been found"); + } +} diff --git a/tag-api/src/main/java/community/tag/service/TagService.java b/tag-api/src/main/java/community/tag/service/TagService.java new file mode 100644 index 00000000..002996cd --- /dev/null +++ b/tag-api/src/main/java/community/tag/service/TagService.java @@ -0,0 +1,77 @@ +package community.tag.service; + +import community.tag.api.dto.TagContentRequestDto; +import community.tag.api.dto.TagContentResponseDto; +import community.tag.domain.TagContent; +import community.tag.domain.TagContentRepository; +import community.tag.api.dto.TagRequestDto; +import community.tag.api.dto.TagResponseDto; +import community.tag.domain.Tag; +import community.tag.domain.TagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class TagService { + private final TagRepository tagRepository; + private final TagContentRepository tagContentRepository; + + public Page readTagPage(Pageable pageable) { + return tagRepository.findAll(pageable).map(TagResponseDto::from); + } + + public Long createTagIfAbsent(TagRequestDto tagRequestDto) { + Tag tag = createTagIfAbsent(tagRequestDto.getName()); + return tagRepository.save(tag).getId(); + } + + public List readTagContentsByTag(String tagName) { + Tag tag = findTagByTagName(tagName); + return tagContentRepository.findByTag(tag).stream() + .map(TagContentResponseDto::from) + .collect(Collectors.toList()); + } + + private Tag findTagByTagName(String tagName) { + return tagRepository.findByName(tagName).orElseThrow(IllegalArgumentException::new); + } + + public List createTagContents(TagContentRequestDto tagContentRequestDto) { + String contentType = tagContentRequestDto.getContentType(); + Long contentId = tagContentRequestDto.getContentId(); + List tagContents = tagContentRequestDto.getTagNames().stream() + .map(this::createTagIfAbsent) + .map(tag -> createTagContent(tag, contentType, contentId)) + .collect(Collectors.toList()); + + return tagContentRepository.saveAll(tagContents).stream() + .map(TagContent::getId) + .collect(Collectors.toList()); + } + + private Tag createTagIfAbsent(String name) { + return tagRepository.findByName(name) + .orElse(Tag.of(name)); + } + + private TagContent createTagContent(Tag tag, String contentType, Long contentId) { + return TagContent.builder().tag(tag) + .contentType(contentType) + .contentId(contentId).build(); + } + + public List readTagsByTagContent(String contentType, Long contentId) { + List tagContents = tagContentRepository.findByContentTypeAndContentId(contentType, contentId); + + return tagContents.stream() + .map(TagContent::getTag) + .map(TagResponseDto::from) + .collect(Collectors.toList()); + } +} diff --git a/tag-api/src/test/java/community/tag/api/TagControllerTest.java b/tag-api/src/test/java/community/tag/api/TagControllerTest.java new file mode 100644 index 00000000..984ecde1 --- /dev/null +++ b/tag-api/src/test/java/community/tag/api/TagControllerTest.java @@ -0,0 +1,149 @@ +package community.tag.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import community.tag.api.dto.TagContentRequestDto; +import community.tag.api.dto.TagRequestDto; +import community.tag.api.dto.TagResponseDto; +import community.tag.service.TagService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static community.common.constants.Constants.pageResponseFieldDescriptors; +import static community.tag.api.dto.TagContentRequestDtoTest.getTagContentRequestDtoFixture; +import static community.tag.api.dto.TagRequestDtoTest.getTagRequestDtoFixture; +import static community.tag.api.dto.TagResponseDtoTest.getTagResponseFixture; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(TagController.class) +@ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs +public class TagControllerTest { + private @Autowired MockMvc mvc; + private @MockBean TagService tagService; + private @Autowired ObjectMapper objectMapper; + + private FieldDescriptor[] requestFieldDescriptors = new FieldDescriptor[]{ + fieldWithPath("name").description("Name of a tag to create") + .attributes(key("constraints").value("Not Empty")) + }; + + private FieldDescriptor[] responseFieldDescriptors = new FieldDescriptor[]{ + fieldWithPath("name").description("Name of a tag") + }; + + private FieldDescriptor[] tagContentRequestFieldDescriptors = new FieldDescriptor[] { + fieldWithPath("tagNames").description("Names of tag") + .attributes(key("constraints").value("Not Empty")), + fieldWithPath("contentType").description("Content Type of a tag") + .attributes(key("constraints").value("Not Empty")), + fieldWithPath("contentId").description("Content Id of a tag") + .attributes(key("constraints").value("Not Empty")) + }; + + @Test + void readAll_ValidInput_ValidOutput() throws Exception { + // given + List tagResponseDtos = new ArrayList<>(); + final var numTags = 10L; + String tagName = "tag"; + for (long i = 0; i < numTags; i++) { + tagResponseDtos.add(getTagResponseFixture(tagName.concat(Long.toString(i)))); + } + Collections.reverse(tagResponseDtos); + PageImpl tagResponseDtoPage = new PageImpl<>( + tagResponseDtos, + PageRequest.of(0, 10, Sort.Direction.DESC, "id"), + 10); + given(tagService.readTagPage(any(Pageable.class))).willReturn(tagResponseDtoPage); + + // expect + this.mvc.perform(get("/tags")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.pageable.sort.sorted").value(true)) + .andExpect(jsonPath("$.pageable.pageSize").value(10)) + .andDo(document("tags/read-tags", + preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + requestParameters( + parameterWithName("page").description("The page to retrieve").optional() + .attributes(key("constraints").value("Nullable, Default: 1")), + parameterWithName("size").description("Entries size per page").optional() + .attributes(key("constraints").value("Nullable, Default: 10")), + parameterWithName("sort") + .description("Sorting option. format -> '{columnName},{asc|desc}'").optional() + .attributes(key("constraints").value("Nullable, Default: id,desc"))), + responseFields(pageResponseFieldDescriptors) + .andWithPrefix("content[].", responseFieldDescriptors))); + } + + @Test + void create_ValidInput_ValidOutput() throws Exception{ + // given + TagRequestDto tagRequestDto = getTagRequestDtoFixture(); + given(tagService.createTagIfAbsent(any(TagRequestDto.class))).willReturn(1L); + + // expect + this.mvc.perform(post("/tags") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(tagRequestDto))) + .andExpect(status().isOk()) + .andDo(document("tags/create-a-tag", + preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + requestFields(requestFieldDescriptors))); + } + + @Test + void create_NullFields_StatusBadRequest() throws Exception { + // given + TagRequestDto tagRequestDto = new TagRequestDto(); + + // expect + this.mvc.perform(post("/tags") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(tagRequestDto))) + .andExpect(status().isBadRequest()); + } + + @Test + void createTagContents_ValidInput_ValidOutput() throws Exception { + // given + TagContentRequestDto tagContentRequestDto = getTagContentRequestDtoFixture(); + given(tagService.createTagContents(any(TagContentRequestDto.class))).willReturn(Arrays.asList(1L, 2L)); + + // expect + this.mvc.perform(post("/tags/tagContents") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(tagContentRequestDto))) + .andExpect(status().isOk()) + .andDo(document("tags/create-tag-contents", + preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + requestFields(tagContentRequestFieldDescriptors))); + } +} diff --git a/tag-api/src/test/java/community/tag/api/dto/TagContentRequestDtoTest.java b/tag-api/src/test/java/community/tag/api/dto/TagContentRequestDtoTest.java new file mode 100644 index 00000000..eb1f080e --- /dev/null +++ b/tag-api/src/test/java/community/tag/api/dto/TagContentRequestDtoTest.java @@ -0,0 +1,21 @@ +package community.tag.api.dto; + +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.List; + +public class TagContentRequestDtoTest { + + public static TagContentRequestDto getTagContentRequestDtoFixture() { + TagContentRequestDto tagContentRequestDto = new TagContentRequestDto(); + List tagNames = Arrays.asList("tag1", "tag2"); + + ReflectionTestUtils.setField(tagContentRequestDto, "tagNames", tagNames); + ReflectionTestUtils.setField(tagContentRequestDto, "contentType", "simpelife"); + ReflectionTestUtils.setField(tagContentRequestDto, "contentId", 1L); + + return tagContentRequestDto; + } + +} \ No newline at end of file diff --git a/tag-api/src/test/java/community/tag/api/dto/TagRequestDtoTest.java b/tag-api/src/test/java/community/tag/api/dto/TagRequestDtoTest.java new file mode 100644 index 00000000..f9c0f475 --- /dev/null +++ b/tag-api/src/test/java/community/tag/api/dto/TagRequestDtoTest.java @@ -0,0 +1,27 @@ +package community.tag.api.dto; + +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.List; + +public class TagRequestDtoTest { + + public static List getTagRequestDtosFixture() { + TagRequestDto tagRequestDto1 = new TagRequestDto(); + TagRequestDto tagRequestDto2 = new TagRequestDto(); + + ReflectionTestUtils.setField(tagRequestDto1, "name", "tag1"); + ReflectionTestUtils.setField(tagRequestDto2, "name", "tag2"); + + return Arrays.asList(tagRequestDto1, tagRequestDto2); + } + + public static TagRequestDto getTagRequestDtoFixture() { + TagRequestDto tagRequestDto = new TagRequestDto(); + + ReflectionTestUtils.setField(tagRequestDto, "name", "tag"); + + return tagRequestDto; + } +} diff --git a/tag-api/src/test/java/community/tag/api/dto/TagResponseDtoTest.java b/tag-api/src/test/java/community/tag/api/dto/TagResponseDtoTest.java new file mode 100644 index 00000000..2b5d4be5 --- /dev/null +++ b/tag-api/src/test/java/community/tag/api/dto/TagResponseDtoTest.java @@ -0,0 +1,9 @@ +package community.tag.api.dto; + +import static community.tag.domain.TagTest.getTagFixture; + +public class TagResponseDtoTest { + public static TagResponseDto getTagResponseFixture(String name) throws Exception { + return TagResponseDto.from(getTagFixture(name)); + } +} diff --git a/tag-api/src/test/java/community/tag/domain/TagContentTest.java b/tag-api/src/test/java/community/tag/domain/TagContentTest.java new file mode 100644 index 00000000..07ebf981 --- /dev/null +++ b/tag-api/src/test/java/community/tag/domain/TagContentTest.java @@ -0,0 +1,92 @@ +package community.tag.domain; + +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static community.tag.domain.TagTest.getTagFixture; +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenThrownBy; + +public class TagContentTest { + public static TagContent getTagContentsFixture(Long tagContentId, String tagName) { + Tag tag = getTagFixture(tagName); + TagContent tagContent = TagContent.builder() + .tag(tag) + .contentType("simplelife") + .contentId(1L).build(); + ReflectionTestUtils.setField(tagContent, "id", tagContentId); + return tagContent; + } + + public static List getTagContentsFixture(Tag tag) { + return Stream.of( + TagContent.builder().tag(tag).contentType("simplelife").contentId(1L).build(), + TagContent.builder().tag(tag).contentType("mjarticle").contentId(1L).build() + ).collect(Collectors.toList()); + } + + @Test + void build_ValidInput_ValidOutput() { + // given + Tag tag = getTagFixture(); + String contentType = "simplelife"; + Long contentId = 1L; + + // when + TagContent tagContent = TagContent.builder() + .tag(tag) + .contentType(contentType) + .contentId(contentId).build(); + + // then + then(tagContent).hasNoNullFieldsOrPropertiesExcept("id", "createdAt", "updatedAt") + .hasFieldOrPropertyWithValue("tag", tag) + .hasFieldOrPropertyWithValue("contentType", contentType) + .hasFieldOrPropertyWithValue("contentId", contentId); + } + + @Test + void build_NullTag_ThrowException() { + // then + thenThrownBy(() -> + TagContent.builder() + .tag(null) + .contentType("simplelife") + .contentId(1L).build() + ).isInstanceOf(IllegalArgumentException.class); + } + + + @Test + void build_EmptyContentType_ThrowException() { + // given + Tag tag = getTagFixture(); + + // then + thenThrownBy(() -> + TagContent.builder() + .tag(tag) + .contentType("") + .contentId(1L).build() + ).isInstanceOf(IllegalArgumentException.class); + } + + + @Test + void build_NullContentId_ThrowException() { + // given + Tag tag = getTagFixture(); + + // then + thenThrownBy(() -> + TagContent.builder() + .tag(tag) + .contentType("simplelife") + .contentId(null).build() + ).isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/tag-api/src/test/java/community/tag/domain/TagTest.java b/tag-api/src/test/java/community/tag/domain/TagTest.java new file mode 100644 index 00000000..138eb494 --- /dev/null +++ b/tag-api/src/test/java/community/tag/domain/TagTest.java @@ -0,0 +1,45 @@ +package community.tag.domain; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenThrownBy; + +public class TagTest { + public static List getTagsFixture() { + return Stream.of( + Tag.of("tag1"), + Tag.of("tag2") + ).collect(Collectors.toList()); + } + + public static Tag getTagFixture() { + return Tag.of("tag"); + } + + public static Tag getTagFixture(String name) { + return Tag.of(name); + } + + @Test + void of_ValidInput_ValidOutput() { + // when + Tag tag = Tag.of("tag"); + + // then + then(tag).hasNoNullFieldsOrPropertiesExcept("id", "createdAt", "updatedAt") + .hasFieldOrPropertyWithValue("name", "tag"); + } + + @Test + void of_InvalidInput_ThrowException() { + // given + thenThrownBy(() -> + Tag.of("") + ).isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/tag-api/src/test/java/community/tag/service/TagServiceTest.java b/tag-api/src/test/java/community/tag/service/TagServiceTest.java new file mode 100644 index 00000000..6b128562 --- /dev/null +++ b/tag-api/src/test/java/community/tag/service/TagServiceTest.java @@ -0,0 +1,154 @@ +package community.tag.service; + +import community.tag.api.dto.TagContentRequestDto; +import community.tag.api.dto.TagContentResponseDto; +import community.tag.api.dto.TagRequestDto; +import community.tag.api.dto.TagResponseDto; +import community.tag.domain.Tag; +import community.tag.domain.TagContent; +import community.tag.domain.TagContentRepository; +import community.tag.domain.TagRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static community.tag.api.dto.TagContentRequestDtoTest.getTagContentRequestDtoFixture; +import static community.tag.api.dto.TagRequestDtoTest.getTagRequestDtoFixture; +import static community.tag.domain.TagContentTest.getTagContentsFixture; +import static community.tag.domain.TagTest.getTagFixture; +import static community.tag.domain.TagTest.getTagsFixture; +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public class TagServiceTest { + private TagService tagService; + + private @Mock TagRepository tagRepository; + private @Mock TagContentRepository tagContentRepository; + + @BeforeEach + void setUp() { + tagService = new TagService(tagRepository, tagContentRepository); + } + + @Test + void readAllTags_ValidInput_ValidOutput() { + // given + final Long numTags = 10L; + String tagName = "tag"; + List tags = new ArrayList<>(); + + for (long i = 0; i < numTags; i++) { + tags.add(getTagFixture(tagName.concat(Long.toString(i)))); + } + + PageImpl tagPage = new PageImpl<>(tags); + given(tagRepository.findAll(any(Pageable.class))).willReturn(tagPage); + + // when + Page tagResponseDtoPage = tagService.readTagPage( + PageRequest.of(0, numTags.intValue()) + ); + + // then + then(tagResponseDtoPage.getTotalElements()).isEqualTo(numTags); + } + + @Test + void createTagIfAbsent_ValidTagInput_ValidOutput() { + // given + TagRequestDto tagRequestDto = getTagRequestDtoFixture(); + Tag tag = Tag.of(tagRequestDto.getName()); + ReflectionTestUtils.setField(tag, "id", 1L); + given(tagRepository.findByName(any(String.class))).willReturn(Optional.of(tag)); + given(tagRepository.save(any(Tag.class))).willReturn(tag); + + // when + Long tagId = tagService.createTagIfAbsent(tagRequestDto); + + // then + then(tagId).isEqualTo(tag.getId()); + then(tag.getName()).isEqualTo(tagRequestDto.getName()); + } + + @Test + void createTagIfAbsent_AbsentTagInput_ValidOutput() { + // given + TagRequestDto tagRequestDto = getTagRequestDtoFixture(); + Tag tag = Tag.of(tagRequestDto.getName()); + ReflectionTestUtils.setField(tag, "id", 1L); + given(tagRepository.findByName(any(String.class))).willReturn(Optional.empty()); + given(tagRepository.save(any(Tag.class))).willReturn(tag); + + // when + Long tagId = tagService.createTagIfAbsent(tagRequestDto); + + // then + then(tagId).isEqualTo(tag.getId()); + } + + @Test + void readTagContentsByTag_ValidInput_ValidOutput() { + // given + Tag tag = getTagFixture(); + List tagContents = getTagContentsFixture(tag); + + given(tagRepository.findByName(any(String.class))).willReturn(Optional.of(tag)); + given(tagContentRepository.findByTag(any(Tag.class))).willReturn(tagContents); + + // when + List tagContentResponseDtos = tagService.readTagContentsByTag(tag.getName()); + + // then + then(tagContentResponseDtos.get(0).getContentType()).isEqualTo(tagContents.get(0).getContentType()); + then(tagContentResponseDtos.get(0).getContentId()).isEqualTo(tagContents.get(0).getContentId()); + + // and then + then(tagContentResponseDtos.get(1).getContentType()).isEqualTo(tagContents.get(1).getContentType()); + then(tagContentResponseDtos.get(1).getContentId()).isEqualTo(tagContents.get(1).getContentId()); + } + + @Test + void createTagContents_ValidInput_ValidOutput() { + // given + TagContentRequestDto tagContentRequestDto = getTagContentRequestDtoFixture(); + List tagContentsToSave = Stream.of( + getTagContentsFixture(1L, "tag1"), + getTagContentsFixture(2L, "tag2") + ).collect(Collectors.toList()); + given(tagContentRepository.saveAll(any())).willReturn(tagContentsToSave); + + // when + List tagContentIds = tagService.createTagContents(tagContentRequestDto); + + // then + then(tagContentIds).isEqualTo(Arrays.asList(1L, 2L)); + } + + // Todo + @Test + void readTagsByTagContent_ValidInput_ValidOutput() { + // given + + // when + + // then + } + +}