diff --git a/samples/restdocs-api-spec-sample/build.gradle b/samples/restdocs-api-spec-sample/build.gradle index 62e07c0..9061faa 100755 --- a/samples/restdocs-api-spec-sample/build.gradle +++ b/samples/restdocs-api-spec-sample/build.gradle @@ -1,4 +1,5 @@ buildscript { + ext.kotlin_version = '2.1.20' ext { springBootVersion = '3.0.0' } @@ -9,16 +10,19 @@ buildscript { dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("com.epages:restdocs-api-spec-gradle-plugin:0.19.2") + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'java' +apply plugin: 'kotlin' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'com.epages.restdocs-api-spec' -sourceCompatibility = 17 -targetCompatibility = 17 +kotlin { + jvmToolchain(17) +} repositories { mavenCentral() @@ -32,6 +36,7 @@ dependencies { implementation('org.springframework.boot:spring-boot-starter-data-jpa') implementation('org.springframework.boot:spring-boot-starter-data-rest') implementation('org.springframework.boot:spring-boot-starter-validation') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" runtimeOnly('com.h2database:h2') testImplementation('org.junit.jupiter:junit-jupiter-engine') diff --git a/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/BaseIntegrationTest.java b/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/BaseIntegrationTest.java index fd04962..48f8823 100644 --- a/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/BaseIntegrationTest.java +++ b/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/BaseIntegrationTest.java @@ -16,7 +16,7 @@ @SpringBootTest @Transactional @AutoConfigureMockMvc -class BaseIntegrationTest { +public class BaseIntegrationTest { @Autowired MockMvc mockMvc; diff --git a/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/CartIntegrationKtTest.kt b/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/CartIntegrationKtTest.kt new file mode 100644 index 0000000..08154a5 --- /dev/null +++ b/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/CartIntegrationKtTest.kt @@ -0,0 +1,149 @@ +package com.epages.restdocs.apispec.sample + +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document +import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName +import com.epages.restdocs.apispec.ResourceDocumentation.resource +import com.epages.restdocs.apispec.ResourceSnippetParameters.Companion.builder +import org.hamcrest.Matchers +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs +import org.springframework.data.rest.webmvc.RestMediaTypes +import org.springframework.http.HttpHeaders +import org.springframework.restdocs.hypermedia.HypermediaDocumentation +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.restdocs.payload.PayloadDocumentation +import org.springframework.restdocs.snippet.Snippet +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers + +@AutoConfigureRestDocs +@ExtendWith(SpringExtension::class) +class CartIntegrationKtTest : BaseIntegrationTest() { + + private var cartId: String? = null + + @Test + fun should_create_cart() { + whenCartIsCreated() + + resultActions + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo(document(identifier = "carts-create", snippets = arrayOf(resource("Create a cart")))) + } + + @Test + fun should_add_product_to_cart() { + givenCart() + givenProduct() + + whenProductIsAddedToCart() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + document( + identifier = "cart-add-product", + snippets = arrayOf(resource("Add products to a cart")) + ) + ) + } + + @Test + fun should_get_cartKt() { + givenCartWithProduct() + + whenCartIsRetrieved() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("products", Matchers.hasSize(1))) + .andExpect(MockMvcResultMatchers.jsonPath("products[0].quantity", Matchers.`is`(1))) + .andExpect(MockMvcResultMatchers.jsonPath("products[0].product.name", Matchers.notNullValue())) + .andExpect(MockMvcResultMatchers.jsonPath("total", Matchers.notNullValue())) + .andDo( + document( + identifier = "cart-get", + snippets = arrayOf( + resource( + builder() + .description("Get a cart by id") + .pathParameters( + parameterWithName("id").description("the cart id") + ) + .responseFields( + PayloadDocumentation.fieldWithPath("total") + .description("Total amount of the cart."), + PayloadDocumentation.fieldWithPath("products") + .description("The product line item of the cart."), + PayloadDocumentation.subsectionWithPath("products[]._links.product") + .description("Link to the product."), + PayloadDocumentation.fieldWithPath("products[].quantity") + .description("The quantity of the line item."), + PayloadDocumentation.subsectionWithPath("products[].product") + .description("The product the line item relates to."), + PayloadDocumentation.subsectionWithPath("_links").description("Links section.") + ) + .links( + HypermediaDocumentation.linkWithRel("self").ignored(), + HypermediaDocumentation.linkWithRel("order").description("Link to order the cart.") + ) + .build() + ) + ) + ) + ) + } + + @Test + fun should_order_cart() { + givenCartWithProduct() + + whenCartIsOrdered() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(document(identifier = "cart-order", snippets = arrayOf(resource("Order a cart")))) + } + + private fun whenProductIsAddedToCart() { + resultActions = mockMvc.perform( + RestDocumentationRequestBuilders.post("/carts/{id}/products", cartId) + .contentType(RestMediaTypes.TEXT_URI_LIST) + .content(entityLinks.linkForItemResource(Product::class.java, productId).toUri().toString()) + ) + } + + private fun whenCartIsCreated() { + resultActions = mockMvc.perform(RestDocumentationRequestBuilders.post("/carts")) + + val location = resultActions.andReturn().response.getHeader(HttpHeaders.LOCATION) + cartId = location!!.substring(location!!.lastIndexOf("/") + 1) + } + + private fun whenCartIsRetrieved() { + resultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get("/carts/{id}", cartId) + .accept(RestMediaTypes.HAL_JSON) + ) + .andDo(MockMvcResultHandlers.print()) + } + + private fun whenCartIsOrdered() { + resultActions = mockMvc.perform(RestDocumentationRequestBuilders.post("/carts/{id}/order", cartId)) + } + + private fun givenCart() { + whenCartIsCreated() + resultActions.andExpect(MockMvcResultMatchers.status().isCreated()) + } + + private fun givenCartWithProduct() { + givenCart() + givenProduct() + whenProductIsAddedToCart() + + resultActions.andExpect(MockMvcResultMatchers.status().isOk()) + } +} diff --git a/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/ProductRestIntegrationKtTest.kt b/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/ProductRestIntegrationKtTest.kt new file mode 100644 index 0000000..f110a95 --- /dev/null +++ b/samples/restdocs-api-spec-sample/src/test/java/com/epages/restdocs/apispec/sample/ProductRestIntegrationKtTest.kt @@ -0,0 +1,229 @@ +package com.epages.restdocs.apispec.sample + +import com.epages.restdocs.apispec.ConstrainedFields +import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import org.hamcrest.Matchers +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.data.rest.webmvc.RestMediaTypes +import org.springframework.http.MediaType +import org.springframework.restdocs.headers.HeaderDocumentation +import org.springframework.restdocs.hypermedia.HypermediaDocumentation +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders +import org.springframework.restdocs.payload.PayloadDocumentation +import org.springframework.restdocs.request.RequestDocumentation +import org.springframework.restdocs.snippet.Snippet +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers + +@AutoConfigureRestDocs +@ExtendWith(SpringExtension::class) +class ProductRestIntegrationKtTest : BaseIntegrationTest() { + + @Autowired + private val objectMapper: ObjectMapper? = null + + private val fields = ConstrainedFields(Product::class.java) + + @Test + fun should_get_products() { + givenProduct() + givenProduct("Fancy Shirt", "15.10") + givenProduct("Fancy Shoes", "75.95") + + whenProductsAreRetrieved() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("_embedded.products", Matchers.hasSize(2))) + .andDo( + document( + identifier = "products-get", snippets = arrayOf( + PayloadDocumentation.responseFields( + PayloadDocumentation.subsectionWithPath("_embedded.products[].name") + .description("The name of the product."), + PayloadDocumentation.fieldWithPath("_embedded.products[].price") + .description("The price of the product."), + PayloadDocumentation.subsectionWithPath("_embedded.products[]._links") + .description("The product links."), + PayloadDocumentation.fieldWithPath("page.size").description("The size of one page."), + PayloadDocumentation.fieldWithPath("page.totalElements") + .description("The total number of elements found."), + PayloadDocumentation.fieldWithPath("page.totalPages") + .description("The total number of pages."), + PayloadDocumentation.fieldWithPath("page.number").description("The current page number."), + PayloadDocumentation.fieldWithPath("page").description("Paging information"), + PayloadDocumentation.subsectionWithPath("_links").description("Links section") + ), + + HypermediaDocumentation.links( + HypermediaDocumentation.linkWithRel("first").description("Link to the first page"), + HypermediaDocumentation.linkWithRel("next").description("Link to the next page"), + HypermediaDocumentation.linkWithRel("last").description("Link to the next page"), + HypermediaDocumentation.linkWithRel("self").ignored(), + HypermediaDocumentation.linkWithRel("profile").ignored() + ), + HeaderDocumentation.requestHeaders( + HeaderDocumentation.headerWithName("accept").description("accept header") + ), + RequestDocumentation.queryParameters( + RequestDocumentation.parameterWithName("page").description("The page to be requested."), + RequestDocumentation.parameterWithName("size") + .description("Parameter determining the size of the requested page."), + RequestDocumentation.parameterWithName("sort") + .description("Information about sorting items.") + ) + ) + ) + ) + } + + @Test + fun should_get_product() { + givenProduct() + + whenProductIsRetrieved() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("name", Matchers.notNullValue())) + .andExpect(MockMvcResultMatchers.jsonPath("price", Matchers.notNullValue())) + .andDo( + document( + identifier = "product-get", snippets = arrayOf( + PayloadDocumentation.responseFields( + fields.withPath("name").description("The name of the product."), + fields.withPath("price").description("The price of the product."), + PayloadDocumentation.subsectionWithPath("_links").description("Links section") + ) + ) + ) + ) + } + + @Test + fun should_create_product() { + givenProductPayload() + + whenProductIsCreated() + + resultActions + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andDo( + document( + identifier = "products-create", snippets = arrayOf( + PayloadDocumentation.requestFields( + fields.withPath("name").description("The name of the product."), + fields.withPath("price").description("The price of the product.") + ) + ) + ) + ) + } + + @Test + fun should_update_product() { + givenProduct() + givenProductPayload("Updated name", "12.12") + + whenProductIsPatched() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + document( + identifier = "product-patch", snippets = arrayOf( + PayloadDocumentation.requestFields( + fields.withPath("name").description("The name of the product."), + fields.withPath("price").description("The price of the product.") + ) + ) + ) + ) + } + + @Test + fun should_fail_to_update_product_with_negative_price() { + givenProduct() + givenProductPayload("Updated name", "-12.12") + + whenProductIsPatched() + + resultActions + .andExpect(MockMvcResultMatchers.status().isBadRequest()) + .andDo(document("product-patch-constraint-violation")) + } + + @Test + fun should_partially_update_product() { + givenProduct() + givenPatchPayload() + + whenProductIsPatchedJsonPatch() + + resultActions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + document( + identifier = "product-patch-json-patch", snippets = arrayOf( + PayloadDocumentation.requestFields( + fields.withPath("[].op").description("Patch operation."), + fields.withPath("[].path").description("The path of the field."), + fields.withPath("[].value").description("The value to assign.") + ) + ) + ) + ) + } + + private fun givenPatchPayload() { + json = objectMapper!!.writeValueAsString( + ImmutableList.of( + ImmutableMap.of( + "op", "replace", + "path", "/name", + "value", "Fancy socks" + ) + ) + ) + } + + private fun whenProductIsRetrieved() { + resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get("/products/{id}", productId)) + } + + private fun whenProductIsPatched() { + resultActions = mockMvc.perform( + RestDocumentationRequestBuilders.patch("/products/{id}", productId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(json) + ) + } + + private fun whenProductIsPatchedJsonPatch() { + resultActions = mockMvc.perform( + RestDocumentationRequestBuilders.patch("/products/{id}", productId) + .contentType(RestMediaTypes.JSON_PATCH_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(json) + ) + } + + private fun whenProductsAreRetrieved() { + resultActions = mockMvc.perform( + RestDocumentationRequestBuilders.get("/products") + .header("accept", RestMediaTypes.HAL_JSON) + .param("page", "0") + .param("size", "2") + .param("sort", "name asc") + ) + .andDo(MockMvcResultHandlers.print()) + } +}