From 3f6224bd757915224d90d4a1cf2cc32b59f55d1d Mon Sep 17 00:00:00 2001 From: juhyeon Date: Tue, 22 Aug 2023 08:30:34 +0900 Subject: [PATCH 01/12] =?UTF-8?q?build=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/test/resources/application-test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/test/resources/application-test.yml diff --git a/app/src/test/resources/application-test.yml b/app/src/test/resources/application-test.yml new file mode 100644 index 00000000..21d9b9fa --- /dev/null +++ b/app/src/test/resources/application-test.yml @@ -0,0 +1,16 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create + h2: + console: + enabled: true + +jwt: + secret: "12345678901234567890123456789010" From 0e21d04bce4898aa3b107920a25783f0395f93c4 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Tue, 22 Aug 2023 08:31:29 +0900 Subject: [PATCH 02/12] =?UTF-8?q?build=20:=20REST=20DOCS=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 991a8b2a..7f4f0a13 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,11 @@ * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle * User Manual available at https://docs.gradle.org/6.7/userguide/building_java_projects.html */ - +buildscript { + ext { + asciidocVersion = "2.0.6.RELEASE" + } +} plugins { // Apply the application plugin to add support for building a CLI application in Java. id 'application' @@ -13,6 +17,13 @@ plugins { // Spring id 'org.springframework.boot' version '2.3.5.RELEASE' id 'io.spring.dependency-management' version '1.0.10.RELEASE' + + // asciidoctor + id "org.asciidoctor.jvm.convert" version "3.3.2" +} + +ext { + snippetsDir = file('build/generated-snippets') } sourceCompatibility = '1.8' @@ -22,6 +33,10 @@ configurations { runtimeClasspath { extendsFrom developmentOnly } + compileOnly { + extendsFrom annotationProcessor + } + asciidoctorExt } repositories { @@ -78,6 +93,10 @@ dependencies { testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } + + //RES DOC Lib + asciidoctorExt "org.springframework.restdocs:spring-restdocs-asciidoctor:${asciidocVersion}" + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:${asciidocVersion}" } application { @@ -89,3 +108,20 @@ tasks.named('test') { // Use junit platform for unit tests. useJUnitPlatform() } +test { + outputs.dir snippetsDir +} + +asciidoctor { + inputs.dir snippetsDir + configurations 'asciidoctorExt' + dependsOn test +} + +bootJar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + println ">>>>" + asciidoctor.outputDir + into 'static/docs' + } +} From 5fd59ba9f788b4b240e75484403a1fb85d6fc641 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Tue, 22 Aug 2023 08:55:59 +0900 Subject: [PATCH 03/12] =?UTF-8?q?doc=20:=20REST=20DOCS=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20adoc=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20rest-doc=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/docs/asciidoc/index.adoc | 12 +-- .../controllers/ProductControllerDocTest.java | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java diff --git a/app/src/docs/asciidoc/index.adoc b/app/src/docs/asciidoc/index.adoc index b5fb9b8b..d675dd6f 100644 --- a/app/src/docs/asciidoc/index.adoc +++ b/app/src/docs/asciidoc/index.adoc @@ -1,17 +1,9 @@ = 고양이 장난감 가게 API -== GET /products - -상품 목록을 JSON 형태로 돌려준다. - -include::{snippets}/get-products/http-request.adoc[] - -include::{snippets}/get-products/http-response.adoc[] - == GET /product/{id} 상품에 대한 자세한 정보를 JSON 형태로 돌려준다. -include::{snippets}/get-product/http-request.adoc[] +include::{snippets}/product-inquiry/http-request.adoc[] -include::{snippets}/get-product/http-response.adoc[] +include::{snippets}/product-inquiry/http-response.adoc[] diff --git a/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java b/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java new file mode 100644 index 00000000..f90b2d6d --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java @@ -0,0 +1,79 @@ +package com.codesoom.assignment.controllers; + +import com.codesoom.assignment.domain.Product; +import com.codesoom.assignment.domain.ProductRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.http.MediaType.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@ExtendWith(RestDocumentationExtension.class) +public class ProductControllerDocTest { + + @Autowired + private ProductRepository productRepository; + + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + @BeforeEach + public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) + .build(); + } + @Test + @DisplayName("상품 단건 조회 테스트") + void RESTDOC_상품_단건_조회() throws Exception { + // given + Product product = Product.builder() + .name("상품명") + .price(1000) + .maker("제조사") + .imageUrl("이미지 URL") + .build(); + + productRepository.save(product); + + // expect + mockMvc.perform(get("/products/{productId}",1L) + .accept(APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andDo(document("product-inquiry",pathParameters( + parameterWithName("productId").description("상품 아이디") + ), + responseFields( + fieldWithPath("id").description("상품 아이디"), + fieldWithPath("name").description("상품명"), + fieldWithPath("price").description("가격"), + fieldWithPath("maker").description("제조사"), + fieldWithPath("imageUrl").description("이미지 URL") + ) + )); + } + +} From 89e267fd02f51eb667be6052d8e025866f5ac614 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Wed, 23 Aug 2023 08:23:40 +0900 Subject: [PATCH 04/12] =?UTF-8?q?build=20:=20index.adoc=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=98=20=EA=B2=B0=EA=B3=BC=EA=B0=80=20docs/index.h?= =?UTF-8?q?tml=20=EA=B2=BD=EB=A1=9C=EC=97=90=20=EB=82=98=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B9=8C=EB=93=9C=EC=84=A4=EC=A0=95=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7f4f0a13..83d5ba55 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,8 +120,8 @@ asciidoctor { bootJar { dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - println ">>>>" + asciidoctor.outputDir - into 'static/docs' + copy {// 생성된 html 파일 복사 + from asciidoctor.outputDir + into "src/main/resources/static/docs" } } From 8170a29ca93fa924cd74f3468f374fcb55870072 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Wed, 23 Aug 2023 08:24:16 +0900 Subject: [PATCH 05/12] =?UTF-8?q?doc=20:=20index.adoc=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EA=B5=AC=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/docs/asciidoc/index.adoc | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/docs/asciidoc/index.adoc b/app/src/docs/asciidoc/index.adoc index d675dd6f..241e9a82 100644 --- a/app/src/docs/asciidoc/index.adoc +++ b/app/src/docs/asciidoc/index.adoc @@ -1,9 +1,23 @@ = 고양이 장난감 가게 API +:toc: left +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toclevels: 1 +:sectlinks: -== GET /product/{id} - +== 상품 단건 조회 상품에 대한 자세한 정보를 JSON 형태로 돌려준다. +=== 요청 include::{snippets}/product-inquiry/http-request.adoc[] +include::{snippets}/product-inquiry/path-parameters.adoc[] + +=== 응답 include::{snippets}/product-inquiry/http-response.adoc[] + +include::{snippets}/product-inquiry/response-fields.adoc[] + +=== curl +include::{snippets}/product-inquiry/curl-request.adoc[] From 6b00b79b6021b62d68c68c9adf9b175c236eb783 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Thu, 24 Aug 2023 09:00:58 +0900 Subject: [PATCH 06/12] =?UTF-8?q?doc=20:=20RestDocs,Test=20=ED=94=BD?= =?UTF-8?q?=EC=8A=A4=EC=B3=90=20helper=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assignment/utils/RestDocsSupporter.java | 69 ++++++++++++++ .../codesoom/assignment/utils/TestHelper.java | 89 +++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 app/src/test/java/com/codesoom/assignment/utils/RestDocsSupporter.java create mode 100644 app/src/test/java/com/codesoom/assignment/utils/TestHelper.java diff --git a/app/src/test/java/com/codesoom/assignment/utils/RestDocsSupporter.java b/app/src/test/java/com/codesoom/assignment/utils/RestDocsSupporter.java new file mode 100644 index 00000000..5a593947 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/utils/RestDocsSupporter.java @@ -0,0 +1,69 @@ +package com.codesoom.assignment.utils; + +import com.codesoom.assignment.application.AuthenticationService; +import com.codesoom.assignment.application.ProductService; +import com.codesoom.assignment.application.UserService; +import com.codesoom.assignment.domain.Role; +import com.codesoom.assignment.security.UserAuthentication; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +@WebMvcTest +@ExtendWith(RestDocumentationExtension.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public class RestDocsSupporter { + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected ProductService productService; + + @MockBean + protected UserService userService; + + @MockBean + protected AuthenticationService authenticationService; + + @MockBean + protected JwtUtil jwtUtil; + + @Autowired + protected MockMvc mockMvc; + + @BeforeEach + void setUp(final WebApplicationContext context, + final RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(MockMvcRestDocumentation.documentationConfiguration(restDocumentation)) + .alwaysDo(MockMvcResultHandlers.print()) + .addFilters(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true)) + .build(); + + SecurityContextHolder.getContext() + .setAuthentication(getUserAuthentication()); + } + + private Authentication getUserAuthentication() { + return new UserAuthentication(1L, + List.of(new Role("USER"), new Role("ADMIN"))); + } +} diff --git a/app/src/test/java/com/codesoom/assignment/utils/TestHelper.java b/app/src/test/java/com/codesoom/assignment/utils/TestHelper.java new file mode 100644 index 00000000..ae3fab24 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/utils/TestHelper.java @@ -0,0 +1,89 @@ +package com.codesoom.assignment.utils; + +import com.codesoom.assignment.domain.Product; +import com.codesoom.assignment.domain.User; +import com.codesoom.assignment.dto.ProductData; +import org.junit.jupiter.params.provider.Arguments; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.stream.Stream; + +public class TestHelper { + + public static final String VALID_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.neCsyNLzy3lQ4o2yliotWT06FwSGZagaHpKdAkjnGGw"; + public static final String OTHER_USER_VALID_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjJ9.i-iHszAs6H2JFTdm3vOVuN18tb_w6n2FqEYIRtr6gaU"; + public static final String INVALID_TOKEN = VALID_TOKEN + "INVALID"; + public static final String SECRET = "12345678901234567890123456789010"; + public static final String AUTH_NAME = "AUTH_NAME"; + public static final String AUTH_EMAIL = "auth@foo.com"; + public static final String INVALID_EMAIL = AUTH_EMAIL + "INVALID"; + public static final String AUTH_PASSWORD = "12345678"; + public static final String TEST_PRODUCT_NAME = "쥐돌이"; + public static final String TEST_UPDATE_PRODUCT_NAME = "쥐순이"; + public static final String TEST_PRODUCT_MAKER = "냥이월드"; + public static final int TEST_PRODUCT_PRICE = 5000; + public static final String INVALID_PASSWORD = AUTH_PASSWORD + "INVALID"; + public static final MockHttpServletRequest INVALID_SERVLET_REQUEST = new MockHttpServletRequest(); + private static final String TEST_LONG_PASSWORD = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut dignissim ex vitae congue congue. Nunc fermentum tellus leo. Donec malesuada, dolor non euismod suscipit, quam elit scelerisque ligula, in finibus eros justo eu justo. Duis tempor porta odio, id finibus nibh pellentesque congue. Ut et velit eget nibh tincidunt porta et id risus. Vestibulum suscipit ullamcorper varius. Proin eget arcu quam. Cras id feugiat libero. Integer auctor sem nec tempor pellentesque. Donec tempor molestie ex in viverra. Aliquam nec purus consequat purus ullamcorper tristique eu sodales erat. Nunc vitae accumsan orci. Vestibulum dictum ante non hendrerit convallis. Ut eu interdum nisl.\n" + + "\n" + + "Vestibulum et tellus tortor. Maecenas vulputate urna eu massa mattis, eu vulputate magna pretium. Vestibulum at sapien vitae mi tempus elementum at eget ante. Morbi risus dolor, eleifend eu ante sed, commodo aliquam augue. Pellentesque aliquet, tellus ultrices fermentum bibendum, turpis urna mollis mauris, sagittis posuere dolor mi et enim. Quisque mollis vulputate est vel eleifend. Donec nec sollicitudin massa. Sed mattis posuere metus sed dictum. Pellentesque varius est a arcu vulputate sollicitudin.\n" + + "\n" + + "Cras ac diam vehicula, elementum mauris tempus, accumsan lacus. Sed lectus diam, hendrerit a consequat id, eleifend eget libero. Praesent laoreet tempor magna et imperdiet. Aenean dictum non velit id lacinia. Donec congue ante dui, id rutrum ex accumsan at. Nulla ut massa elementum, posuere nunc sit amet, ornare nisl. Pellentesque in dui ipsum. Vivamus placerat velit sit amet tempus efficitur.\n" + + "\n" + + "Donec auctor lacus sit amet neque luctus, vitae tincidunt tortor lobortis. Fusce aliquam sem ut magna sollicitudin, ac vulputate est placerat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam scelerisque augue elit, at bibendum libero efficitur ac. Sed fringilla purus pretium tortor condimentum imperdiet. Praesent in nibh lacinia, euismod enim eu, bibendum felis. Aliquam quis placerat ipsum. Integer dictum volutpat."; + + public static final User AUTH_USER = User.builder() + .name(AUTH_NAME) + .email(AUTH_EMAIL) + .password(AUTH_PASSWORD) + .build(); + + public static final Product TEST_PRODUCT = Product.builder() + .id(1L) + .name(TEST_PRODUCT_NAME) + .maker(TEST_PRODUCT_MAKER) + .price(TEST_PRODUCT_PRICE) + .build(); + + public static final ProductData TEST_PRODUCT_DATA = ProductData.builder() + .name(TEST_PRODUCT_NAME) + .maker(TEST_PRODUCT_MAKER) + .price(TEST_PRODUCT_PRICE) + .build(); + + public static Stream provideInvalidProductRequests() { + return Stream.of( + Arguments.of(ProductData.builder().name("").maker("").price(0).build()), + Arguments.of(ProductData.builder().name("").maker(TEST_PRODUCT_MAKER).price(TEST_PRODUCT_PRICE).build()), + Arguments.of(ProductData.builder().name(TEST_PRODUCT_NAME).maker("").price(TEST_PRODUCT_PRICE).build()), + Arguments.of(ProductData.builder().name(TEST_PRODUCT_NAME).maker(TEST_PRODUCT_MAKER).price(null).build()) + ); + } + + public static final ProductData UPDATE_PRODUCT_DATA = ProductData.builder() + .name(TEST_UPDATE_PRODUCT_NAME) + .maker(TEST_PRODUCT_MAKER) + .price(TEST_PRODUCT_PRICE) + .build(); + + public static final Product UPDATED_PRODUCT = Product.builder() + .id(1L) + .name(TEST_UPDATE_PRODUCT_NAME) + .maker(TEST_PRODUCT_MAKER) + .price(TEST_PRODUCT_PRICE) + .build(); + + public static MockHttpServletRequest getInvalidTokenServletRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + INVALID_TOKEN); + return request; + } + + public static MockHttpServletRequest getValidTokenServletRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + VALID_TOKEN); + return request; + } + + +} From c8892be0e7c8f2786d82bb69c1b9886c3e472784 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Thu, 24 Aug 2023 09:01:31 +0900 Subject: [PATCH 07/12] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=EC=95=8A=EB=8A=94=20=EC=9A=94=EC=B2=AD=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/codesoom/assignment/dto/ProductData.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/codesoom/assignment/dto/ProductData.java b/app/src/main/java/com/codesoom/assignment/dto/ProductData.java index ebf6fd5c..2fb2e09e 100644 --- a/app/src/main/java/com/codesoom/assignment/dto/ProductData.java +++ b/app/src/main/java/com/codesoom/assignment/dto/ProductData.java @@ -16,7 +16,6 @@ @NoArgsConstructor @AllArgsConstructor public class ProductData { - private Long id; @NotBlank @Mapping("name") From 794bdfa98b606ac94534782327686547fe2ef662 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Thu, 24 Aug 2023 09:02:15 +0900 Subject: [PATCH 08/12] =?UTF-8?q?doc:=20REST=20DOCS=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20=EC=83=9D=EC=84=B1,=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ProductControllerDocTest.java | 173 ++++++++++++------ 1 file changed, 120 insertions(+), 53 deletions(-) diff --git a/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java b/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java index f90b2d6d..c5be2928 100644 --- a/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java +++ b/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java @@ -1,71 +1,102 @@ package com.codesoom.assignment.controllers; -import com.codesoom.assignment.domain.Product; -import com.codesoom.assignment.domain.ProductRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; +import com.codesoom.assignment.dto.ProductData; +import com.codesoom.assignment.utils.RestDocsSupporter; +import com.google.common.net.HttpHeaders; import org.junit.jupiter.api.DisplayName; 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.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.test.web.servlet.ResultActions; + + +import java.util.Arrays; + +import static com.codesoom.assignment.utils.TestHelper.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; import static org.springframework.http.MediaType.*; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.restdocs.request.RequestDocumentation.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles("test") -@ExtendWith(RestDocumentationExtension.class) -public class ProductControllerDocTest { - - @Autowired - private ProductRepository productRepository; - - MockMvc mockMvc; - - @Autowired - ObjectMapper objectMapper; - @BeforeEach - public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) - .apply(documentationConfiguration(restDocumentation)) - .build(); + +@DisplayName("ProductController 테스트") +@SuppressWarnings("NonAsciiCharacters") +public class ProductControllerDocTest extends RestDocsSupporter { + + @Test + @DisplayName("상품 전체 조회 테스트") + void RESTDOC_상품_전체_조회() throws Exception { + given(productService.getProducts()) + .willReturn(Arrays.asList(TEST_PRODUCT)); + + ResultActions result = mockMvc.perform(get("/products") + .accept(APPLICATION_JSON)); + + result.andExpect(status().isOk()) + .andDo( + document("product-inquiry-all", + responseFields( + fieldWithPath("[].id").description("상품 아이디"), + fieldWithPath("[].name").description("상품명"), + fieldWithPath("[].price").description("가격"), + fieldWithPath("[].maker").description("제조사"), + fieldWithPath("[].imageUrl").description("이미지 URL") + ) + )); } + @Test @DisplayName("상품 단건 조회 테스트") void RESTDOC_상품_단건_조회() throws Exception { - // given - Product product = Product.builder() - .name("상품명") - .price(1000) - .maker("제조사") - .imageUrl("이미지 URL") - .build(); - - productRepository.save(product); - - // expect - mockMvc.perform(get("/products/{productId}",1L) - .accept(APPLICATION_JSON)) - .andDo(print()) - .andExpect(status().isOk()) - .andDo(document("product-inquiry",pathParameters( - parameterWithName("productId").description("상품 아이디") - ), + given(productService.getProduct(anyLong())) + .willReturn(TEST_PRODUCT); + + ResultActions result = mockMvc.perform(get("/products/{productId}", 1L) + .accept(APPLICATION_JSON)); + + result.andExpect(status().isOk()) + .andDo(document("product-inquiry", pathParameters( + parameterWithName("productId").description("조회할 상품 아이디") + ), + responseFields( + fieldWithPath("id").description("상품 아이디"), + fieldWithPath("name").description("상품명"), + fieldWithPath("price").description("가격"), + fieldWithPath("maker").description("제조사"), + fieldWithPath("imageUrl").description("이미지 URL") + ) + )); + } + + @Test + @DisplayName("상품 생성 테스트") + void RESTDOC_상품_생성() throws Exception { + given(productService.createProduct(any(ProductData.class))) + .willReturn(TEST_PRODUCT); + + ResultActions result = mockMvc.perform(post("/products") + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(TEST_PRODUCT_DATA)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + VALID_TOKEN)); + + + result.andExpect(status().isCreated()) + .andDo(document("product-create", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("회원 인증 토큰") + ), + requestFields( + fieldWithPath("name").description("상품명"), + fieldWithPath("price").description("가격"), + fieldWithPath("maker").description("제조사"), + fieldWithPath("imageUrl").description("이미지 URL") + ), responseFields( fieldWithPath("id").description("상품 아이디"), fieldWithPath("name").description("상품명"), @@ -76,4 +107,40 @@ public void setUp(WebApplicationContext webApplicationContext, RestDocumentation )); } + + @Test + @DisplayName("상품 수정 테스트") + void RESTDOC_상품_수정() throws Exception { + given(productService.updateProduct(anyLong(), any(ProductData.class))) + .willReturn(UPDATED_PRODUCT); + + ResultActions result = mockMvc.perform(patch("/products/{productId}", 1L) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + VALID_TOKEN) + .content(objectMapper.writeValueAsString(UPDATE_PRODUCT_DATA))); + + result.andExpect(status().isOk()) + .andDo(document("product-update", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("회원 인증 토큰") + ), + pathParameters( + parameterWithName("productId").description("수정 할 상품 아이디") + ), + requestFields( + fieldWithPath("name").description("상품명"), + fieldWithPath("price").description("가격"), + fieldWithPath("maker").description("제조사"), + fieldWithPath("imageUrl").description("이미지 URL") + ), + responseFields( + fieldWithPath("id").description("상품 아이디"), + fieldWithPath("name").description("상품명"), + fieldWithPath("price").description("가격"), + fieldWithPath("maker").description("제조사"), + fieldWithPath("imageUrl").description("이미지 URL") + ) + )); + } } From dcc3ff512de5acee77dbead368ea73f64a5fe1e3 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Fri, 25 Aug 2023 08:28:55 +0900 Subject: [PATCH 09/12] =?UTF-8?q?doc:=20REST=20DOC=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ProductControllerDocTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java b/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java index c5be2928..5aaedfec 100644 --- a/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java +++ b/app/src/test/java/com/codesoom/assignment/controllers/ProductControllerDocTest.java @@ -143,4 +143,24 @@ public class ProductControllerDocTest extends RestDocsSupporter { ) )); } + + @Test + void RESTDOC_상품_삭제() throws Exception { + given(productService.deleteProduct(anyLong())) + .willReturn(TEST_PRODUCT); + + ResultActions result = mockMvc.perform(delete("/products/{productId}", 1L) + .accept(APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + VALID_TOKEN)); + + result.andExpect(status().isOk()) + .andDo(document("product-delete", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("회원 인증 토큰") + ), + pathParameters( + parameterWithName("productId").description("삭제 할 상품 아이디") + ) + )); + } } From 2c9672cc2ce5ea7b49ef6f959518a164d04ef943 Mon Sep 17 00:00:00 2001 From: juhyeon Date: Fri, 25 Aug 2023 08:39:25 +0900 Subject: [PATCH 10/12] =?UTF-8?q?doc:=20REST=20DOC=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/docs/asciidoc/index.adoc | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/src/docs/asciidoc/index.adoc b/app/src/docs/asciidoc/index.adoc index 241e9a82..8f3ab1ab 100644 --- a/app/src/docs/asciidoc/index.adoc +++ b/app/src/docs/asciidoc/index.adoc @@ -21,3 +21,61 @@ include::{snippets}/product-inquiry/response-fields.adoc[] === curl include::{snippets}/product-inquiry/curl-request.adoc[] + +== 상품 목록 조회 +상품 전체 목록에 대한 정보를 JSON 형태로 돌려준다. + +=== 요청 +include::{snippets}/product-inquiry-all/http-request.adoc[] + +=== 응답 +include::{snippets}/product-inquiry-all/http-response.adoc[] + +include::{snippets}/product-inquiry-all/response-fields.adoc[] + +=== curl +include::{snippets}/product-inquiry-all/curl-request.adoc[] + +== 상품 생성 +상품 생성 후 생성한 상품에 대한 정보를 JSON 형태로 돌려준다. + +=== 요청 +include::{snippets}/product-create/http-request.adoc[] + +=== 응답 +include::{snippets}/product-create/http-response.adoc[] + +include::{snippets}/product-create/response-fields.adoc[] + +=== curl +include::{snippets}/product-create/curl-request.adoc[] + +== 상품 수정 +상품 수정 후 생성한 상품에 대한 정보를 JSON 형태로 돌려준다. + +=== 요청 +include::{snippets}/product-update/http-request.adoc[] + +include::{snippets}/product-update/path-parameters.adoc[] + +=== 응답 +include::{snippets}/product-update/http-response.adoc[] + +include::{snippets}/product-update/response-fields.adoc[] + +=== curl +include::{snippets}/product-update/curl-request.adoc[] + +== 상품 삭제 +상품을 삭제할 아이디를 받으면 상품을 삭제한다. + +=== 요청 +include::{snippets}/product-delete/http-request.adoc[] + +include::{snippets}/product-delete/path-parameters.adoc[] + +=== 응답 +include::{snippets}/product-delete/http-response.adoc[] + +=== curl +include::{snippets}/product-delete/curl-request.adoc[] From 705c5cf75c52d4011b82cb69fe3190fb8bd613a1 Mon Sep 17 00:00:00 2001 From: tmxhsk99 Date: Sat, 26 Aug 2023 14:00:41 +0900 Subject: [PATCH 11/12] =?UTF-8?q?build=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=83=9D=EC=84=B1=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..55511bf3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +# 어떤 이미지로부터 만들지 선택합니다. 이 이미지는 우리 프로젝트를 빌드를 하기 위해서만 사용되는 이미지입니다. 빌드 하기 위해서 모든 파일을 컨테이너로 복사한 후 빌드를 실행합니다. 빌드를 하는 용도로만 사용되고 더 이상 사용되지 않습니다. +FROM openjdk:11 AS builder +# 현재 폴더에서 컨테이너의 현재 폴더로 복사합니다. +COPY . . +# 명령어를 실행합니다. +RUN sed -i 's/\r//' ./gradlew +RUN ["./gradlew", "assemble"] +# 이 이미지가 우리가 실제로 실행시킬 이미지입니다. 마찬가지로 openjdk로 이미지를 사용합니다. +FROM openjdk:11 +# 위의 빌드 이미지로부터 만들어진 jar 파일을 복사합니다. +COPY --from=builder /app/build/libs/app.jar . +# 서버를 실행시키는 명령어입니다. +CMD ["java", "-jar", "app.jar"] From dd06aa88e00cef0688dc28cb3c58dd8b1dae24a0 Mon Sep 17 00:00:00 2001 From: tmxhsk99 Date: Sat, 26 Aug 2023 14:01:09 +0900 Subject: [PATCH 12/12] =?UTF-8?q?build=20:=20DB=20=EB=B3=80=EA=B2=BD=20=20?= =?UTF-8?q?h2=20->=20mariadb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 2 ++ app/src/main/resources/application.yml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 83d5ba55..663736cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,8 @@ dependencies { //RES DOC Lib asciidoctorExt "org.springframework.restdocs:spring-restdocs-asciidoctor:${asciidocVersion}" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:${asciidocVersion}" + + implementation 'org.mariadb.jdbc:mariadb-java-client' } application { diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 4fc23938..a4d3f518 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -1,6 +1,8 @@ spring: datasource: - url: jdbc:h2:~/data/demo + url: jdbc:mariadb://mariadb:3306/test + username: root + password: root1234 jpa: hibernate: ddl-auto: update