From d4c2d4b6714d8fb79d89978bef8c3f7503220e36 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Fri, 7 Dec 2018 09:42:32 +0100 Subject: [PATCH 01/10] add REST Assured Support --- restdocs-api-spec/build.gradle.kts | 1 + .../RestAssuredRestDocumentationWrapper.kt | 197 +++++++++++++ .../apispec/ResourceSnippetIntegrationTest.kt | 80 +++--- ...RestDocumentationWrapperIntegrationTest.kt | 267 ++++++++++++++++++ 4 files changed, 508 insertions(+), 37 deletions(-) create mode 100644 restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt create mode 100644 restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt diff --git a/restdocs-api-spec/build.gradle.kts b/restdocs-api-spec/build.gradle.kts index 9513829d..23ac269f 100755 --- a/restdocs-api-spec/build.gradle.kts +++ b/restdocs-api-spec/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { compile(kotlin("reflect")) compile("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion") + compile("org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion") compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt new file mode 100644 index 00000000..fa108e3a --- /dev/null +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt @@ -0,0 +1,197 @@ +package com.epages.restdocs.apispec + +import org.springframework.restdocs.headers.HeaderDescriptor +import org.springframework.restdocs.headers.RequestHeadersSnippet +import org.springframework.restdocs.headers.ResponseHeadersSnippet +import org.springframework.restdocs.hypermedia.LinkDescriptor +import org.springframework.restdocs.hypermedia.LinksSnippet +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor +import org.springframework.restdocs.payload.FieldDescriptor +import org.springframework.restdocs.payload.RequestFieldsSnippet +import org.springframework.restdocs.payload.ResponseFieldsSnippet +import org.springframework.restdocs.request.ParameterDescriptor +import org.springframework.restdocs.request.PathParametersSnippet +import org.springframework.restdocs.request.RequestParametersSnippet +import org.springframework.restdocs.restassured3.RestAssuredRestDocumentation +import org.springframework.restdocs.restassured3.RestDocumentationFilter +import org.springframework.restdocs.snippet.Snippet + +import java.util.function.Function + +/** + * Convenience class to migrate to restdocs-openapi in a non-invasive way. + * It it a wrapper and replacement for RestAssuredRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. + */ +object RestAssuredRestDocumentationWrapper { + + @JvmOverloads @JvmStatic + fun document( + identifier: String, + resourceDetails: ResourceSnippetDetails, + requestPreprocessor: OperationRequestPreprocessor? = null, + responsePreprocessor: OperationResponsePreprocessor? = null, + snippetFilter: Function, List> = Function.identity(), + vararg snippets: Snippet + ): RestDocumentationFilter { + + val enhancedSnippets = + enhanceSnippetsWithResourceSnippet( + resourceDetails = resourceDetails, + snippetFilter = snippetFilter, + snippets = *snippets + ) + + if (requestPreprocessor != null && responsePreprocessor != null) { + return RestAssuredRestDocumentation.document( + identifier, + requestPreprocessor, + responsePreprocessor, + *enhancedSnippets + ) + } else if (requestPreprocessor != null) { + return RestAssuredRestDocumentation.document(identifier, requestPreprocessor, *enhancedSnippets) + } else if (responsePreprocessor != null) { + return RestAssuredRestDocumentation.document(identifier, responsePreprocessor, *enhancedSnippets) + } + + return RestAssuredRestDocumentation.document(identifier, *enhancedSnippets) + } + + @JvmOverloads @JvmStatic + fun document( + identifier: String, + description: String? = null, + summary: String? = null, + privateResource: Boolean = false, + deprecated: Boolean = false, + requestPreprocessor: OperationRequestPreprocessor? = null, + responsePreprocessor: OperationResponsePreprocessor? = null, + snippetFilter: Function, List> = Function.identity(), + vararg snippets: Snippet + ): RestDocumentationFilter { + return document( + identifier = identifier, + resourceDetails = ResourceSnippetParametersBuilder() + .description(description) + .summary(summary) + .privateResource(privateResource) + .deprecated(deprecated), + requestPreprocessor = requestPreprocessor, + responsePreprocessor = responsePreprocessor, + snippetFilter = snippetFilter, + snippets = *snippets + ) + } + + @JvmStatic + fun document( + identifier: String, + requestPreprocessor: OperationRequestPreprocessor, + vararg snippets: Snippet + ): RestDocumentationFilter { + return document(identifier, null, null, false, false, requestPreprocessor, snippets = *snippets) + } + + @JvmStatic + fun document( + identifier: String, + description: String, + privateResource: Boolean, + vararg snippets: Snippet + ): RestDocumentationFilter { + return document(identifier, description, null, privateResource, snippets = *snippets) + } + + @JvmStatic + fun resourceDetails(): ResourceSnippetDetails { + return ResourceSnippetParametersBuilder() + } + + internal fun enhanceSnippetsWithResourceSnippet( + resourceDetails: ResourceSnippetDetails, + snippetFilter: Function, List>, + vararg snippets: Snippet + ): Array { + + val enhancedSnippets = if (snippets.none { it is ResourceSnippet }) { // No ResourceSnippet, so we configure our own based on the info of the other snippets + val resourceParameters = createBuilder(resourceDetails) + .requestFields( + snippets.filter { it is RequestFieldsSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + ) + .responseFields( + snippets.filter { it is ResponseFieldsSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + ) + .links( + snippets.filter { it is LinksSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + ) + .requestParameters( + *snippets.filter { it is RequestParametersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .pathParameters( + *snippets.filter { it is PathParametersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .requestHeaders( + *snippets.filter { it is RequestHeadersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .responseHeaders( + *snippets.filter { it is ResponseHeadersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .build() + snippets.toList() + ResourceDocumentation.resource(resourceParameters) + } else snippets.toList() + + return snippetFilter.apply(enhancedSnippets).toTypedArray() + } + + internal fun createBuilder(resourceDetails: ResourceSnippetDetails): ResourceSnippetParametersBuilder { + return when (resourceDetails) { + is ResourceSnippetParametersBuilder -> resourceDetails + else -> ResourceSnippetParametersBuilder() + .description(resourceDetails.description) + .summary(resourceDetails.summary) + .privateResource(resourceDetails.privateResource) + .deprecated(resourceDetails.deprecated) + .tags(*resourceDetails.tags.toTypedArray()) + } + } +} diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt index 610b77ad..68c3fabb 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -12,6 +12,7 @@ import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.ConfigurableApplicationContext import org.springframework.hateoas.Link import org.springframework.hateoas.MediaTypes.HAL_JSON import org.springframework.hateoas.Resource @@ -97,43 +98,7 @@ open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMv .andDo(document(operationName, buildFullResourceSnippet())) } - protected fun buildFullResourceSnippet(): ResourceSnippet { - return resource( - ResourceSnippetParameters.builder() - .description("description") - .summary("summary") - .deprecated(true) - .privateResource(true) - .requestFields(fieldDescriptors()) - .responseFields(fieldDescriptors().and(fieldWithPath("id").description("id"))) - .requestHeaders( - headerWithName("X-Custom-Header").description("A custom header"), - headerWithName(ACCEPT).description("Accept") - ) - .responseHeaders( - headerWithName("X-Custom-Header").description("A custom header"), - headerWithName(CONTENT_TYPE).description("ContentType") - ) - .pathParameters( - parameterWithName("someId").description("some id"), - parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) - ) - .links( - linkWithRel("self").description("some"), - linkWithRel("multiple").description("multiple") - ) - .build() - ) - } - protected fun fieldDescriptors(): FieldDescriptors { - val fields = ConstrainedFields(TestDataHolder::class.java) - return ResourceDocumentation.fields( - fields.withPath("comment").description("the comment").optional(), - fields.withPath("flag").description("the flag"), - fields.withMappedPath("count", "count").description("the count") - ) - } protected fun givenEndpointInvoked(flagValue: String = "true") { resultActions = mockMvc.perform( @@ -162,8 +127,9 @@ open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMv @SpringBootApplication internal open class TestApplication { + lateinit var applicaitonContext : ConfigurableApplicationContext fun main(args: Array) { - SpringApplication.run(TestApplication::class.java, *args) + applicaitonContext = SpringApplication.run(TestApplication::class.java, *args) } @RestController @@ -199,3 +165,43 @@ open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMv val id: String? = null ) } + + +fun fieldDescriptors(): FieldDescriptors { + val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) + return ResourceDocumentation.fields( + fields.withPath("comment").description("the comment").optional(), + fields.withPath("flag").description("the flag"), + fields.withMappedPath("count", "count").description("the count") + ) +} + +fun buildFullResourceSnippet(): ResourceSnippet { + return resource( + ResourceSnippetParameters.builder() + .description("description") + .summary("summary") + .deprecated(true) + .privateResource(true) + .requestFields(fieldDescriptors()) + .responseFields(fieldDescriptors().and(fieldWithPath("id").description("id"))) + .requestHeaders( + headerWithName("X-Custom-Header").description("A custom header"), + headerWithName(ACCEPT).description("Accept") + ) + .responseHeaders( + headerWithName("X-Custom-Header").description("A custom header"), + headerWithName(CONTENT_TYPE).description("ContentType") + ) + .pathParameters( + parameterWithName("someId").description("some id"), + parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) + ) + .links( + linkWithRel("self").description("some"), + linkWithRel("multiple").description("multiple") + ) + .build() + ) +} + diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt new file mode 100644 index 00000000..2658e2f7 --- /dev/null +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt @@ -0,0 +1,267 @@ +package com.epages.restdocs.apispec + +import io.restassured.RestAssured +import io.restassured.builder.RequestSpecBuilder +import io.restassured.filter.Filter +import io.restassured.http.ContentType +import io.restassured.specification.RequestSpecification +import org.assertj.core.api.Assertions.assertThatCode +import org.assertj.core.api.BDDAssertions.then +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.hateoas.MediaTypes +import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.RestDocumentationExtension +import org.springframework.restdocs.headers.HeaderDocumentation.* +import org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel +import org.springframework.restdocs.hypermedia.HypermediaDocumentation.links +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor +import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.restdocs.request.RequestDocumentation.parameterWithName +import org.springframework.restdocs.request.RequestDocumentation.pathParameters +import org.springframework.restdocs.restassured3.RestAssuredRestDocumentation +import org.springframework.restdocs.restassured3.RestDocumentationFilter +import java.io.File + +@ExtendWith(RestDocumentationExtension::class) +class RestAssuredRestDocumentationWrapperIntegrationTest() { + val operationName = "test-${System.currentTimeMillis()}" + + private lateinit var app: ResourceSnippetIntegrationTest.TestApplication + private var serverPort: Int? = null + + private lateinit var spec: RequestSpecification + + @BeforeEach + fun setUp(restDocumentation: RestDocumentationContextProvider) { + startSpringApplication() + spec = RequestSpecBuilder() + .addFilter(RestAssuredRestDocumentation.documentationConfiguration(restDocumentation)) + .build() + } + + fun startSpringApplication() { + app = ResourceSnippetIntegrationTest.TestApplication() + app.main(arrayOf("--server.port=0")) + serverPort = app.applicaitonContext.environment.getProperty("local.server.port")?.toInt() + } + + @AfterEach + fun tearDown() { + app.applicaitonContext.close() + } + + fun givenEndpointInvoked(documentationFilter: Filter, flagValue: String = "true") { + RestAssured.given(spec) + .filter(documentationFilter) + .baseUri("http://localhost") + .port(requireNotNull(serverPort) { IllegalStateException("Server port is not available!") }) + .pathParam("someId", "id") + .pathParam("otherId", 1) + .contentType(ContentType.JSON) + .header("X-Custom-Header", "test") + .accept(MediaTypes.HAL_JSON_VALUE) + .body("""{ + "comment": "some", + "flag": $flagValue, + "count": 1 + }""".trimIndent()) + .`when`() + .post("/some/{someId}/other/{otherId}") + .then() + .statusCode(200) + } + + @Test + fun should_document_both_restdocs_and_resource() { + givenEndpointInvoked(whenDocumentedAsPrivateResource()) + thenSnippetFileExists() + } + + @Test + fun should_document_both_restdocs_and_resource_as_private_resource() { + givenEndpointInvoked(whenDocumentedAsPrivateResource()) + thenSnippetFileExists() + } + + @Test + fun should_document_using_the_passed_raml_snippet() { + givenEndpointInvoked(whenDocumentedWithRamlSnippet()) + thenSnippetFileExists() + } + + @Test + fun should_value_ignored_fields_and_links() { + assertThatCode { givenEndpointInvoked(this.whenDocumentedWithAllFieldsLinksIgnored()) }.doesNotThrowAnyException() + } + + @Test + fun should_document_restdocs_and_resource_snippet_details() { + givenEndpointInvoked(whenDocumentedWithResourceSnippetDetails()) + thenSnippetFileExists() + } + + @Test + fun should_document_request() { + givenEndpointInvoked(whenResourceSnippetDocumentedWithoutParameters()) + thenSnippetFileExists() + } + + @Test + fun should_document_request_with_description() { + givenEndpointInvoked(whenResourceSnippetDocumentedWithDescription()) + thenSnippetFileExists() + } + + @Test + fun should_document_request_with_fields() { + givenEndpointInvoked(whenResourceSnippetDocumentedWithRequestAndResponseFields()) + thenSnippetFileExists() + } + + @Test + fun should_document_request_with_null_field() { + assertThatCode { + givenEndpointInvoked(this.whenResourceSnippetDocumentedWithRequestAndResponseFields(), "null") + } + .doesNotThrowAnyException() + } + + private fun whenResourceSnippetDocumentedWithoutParameters(): RestDocumentationFilter { + return RestAssuredRestDocumentationWrapper.document(identifier = operationName, snippets = *arrayOf(ResourceDocumentation.resource())) + } + + private fun whenResourceSnippetDocumentedWithDescription(): RestDocumentationFilter { + return RestAssuredRestDocumentationWrapper.document(identifier = operationName, snippets = *arrayOf(ResourceDocumentation.resource("A description"))) + } + + private fun whenResourceSnippetDocumentedWithRequestAndResponseFields(): RestDocumentationFilter { + return RestAssuredRestDocumentationWrapper.document( + identifier = operationName, + snippets = *arrayOf(buildFullResourceSnippet()) + ) + } + + @Throws(Exception::class) + private fun whenDocumentedWithRestdocsAndResource(): RestDocumentationFilter { + return RestAssuredRestDocumentationWrapper.document( + identifier = operationName, + snippets = *arrayOf( + pathParameters( + parameterWithName("someId").description("someId"), + parameterWithName("otherId").description("otherId") + ), + requestFields(fieldDescriptors().fieldDescriptors), + requestHeaders( + headerWithName("X-Custom-Header").description("some custom header") + ), + responseFields( + fieldWithPath("comment").description("the comment"), + fieldWithPath("flag").description("the flag"), + fieldWithPath("count").description("the count"), + fieldWithPath("id").description("id"), + subsectionWithPath("_links").ignored() + ), + responseHeaders( + headerWithName("X-Custom-Header").description("some custom header") + ), + links( + linkWithRel("self").description("some"), + linkWithRel("multiple").description("multiple") + ) + ) + ) + } + + @Throws(Exception::class) + private fun whenDocumentedWithRamlSnippet(): RestDocumentationFilter { + return RestAssuredRestDocumentationWrapper.document( + identifier = operationName, + snippets = *arrayOf(buildFullResourceSnippet()) + ) + } + + @Throws(Exception::class) + private fun whenDocumentedWithAllFieldsLinksIgnored(): RestDocumentationFilter { + return RestAssuredRestDocumentationWrapper.document( + identifier = operationName, + snippets = *arrayOf( + requestFields(fieldDescriptors().fieldDescriptors), + responseFields( + fieldWithPath("comment").ignored(), + fieldWithPath("flag").ignored(), + fieldWithPath("count").ignored(), + fieldWithPath("id").ignored(), + subsectionWithPath("_links").ignored() + ), + links( + linkWithRel("self").optional().ignored(), + linkWithRel("multiple").optional().ignored() + ) + ) + ) + } + + @Throws(Exception::class) + private fun whenDocumentedAsPrivateResource(): RestDocumentationFilter { + val operationRequestPreprocessor = OperationRequestPreprocessor { r -> r } + return RestAssuredRestDocumentationWrapper.document( + identifier = operationName, + privateResource = true, + requestPreprocessor = operationRequestPreprocessor, + snippets = *arrayOf( + requestFields(fieldDescriptors().fieldDescriptors), + responseFields( + fieldWithPath("comment").description("the comment"), + fieldWithPath("flag").description("the flag"), + fieldWithPath("count").description("the count"), + fieldWithPath("id").description("id"), + subsectionWithPath("_links").ignored() + ), + links( + linkWithRel("self").description("some"), + linkWithRel("multiple").description("multiple") + ) + ) + ) + } + + @Throws(Exception::class) + private fun whenDocumentedWithResourceSnippetDetails(): RestDocumentationFilter { + val operationRequestPreprocessor = OperationRequestPreprocessor { r -> r } + return RestAssuredRestDocumentationWrapper.document( + identifier = operationName, + resourceDetails = MockMvcRestDocumentationWrapper.resourceDetails() + .description("The Resource") + .privateResource(true) + .tag("some-tag"), + requestPreprocessor = operationRequestPreprocessor, + snippets = *arrayOf( + requestFields(fieldDescriptors().fieldDescriptors), + responseFields( + fieldWithPath("comment").description("the comment"), + fieldWithPath("flag").description("the flag"), + fieldWithPath("count").description("the count"), + fieldWithPath("id").description("id"), + subsectionWithPath("_links").ignored() + ), + links( + linkWithRel("self").description("some"), + linkWithRel("multiple").description("multiple") + ) + ) + ) + } + + private fun thenSnippetFileExists() { + with(generatedSnippetFile()) { + then(this).exists() + val contents = readText() + then(contents).isNotEmpty() + } + } + + private fun generatedSnippetFile() = File("build/generated-snippets", "$operationName/resource.json") +} From ef2aa74e2f9972f992809ea2db8a72919de73f47 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Fri, 7 Dec 2018 10:04:48 +0100 Subject: [PATCH 02/10] update Readme.md RestAssured limitations removed --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5c97620c..4001816f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ This is why we came up with this project. - [Build configuration](#build-configuration) - [Gradle](#gradle) - [Maven](#maven) - - [Usage with Spring REST Docs](#usage-with-spring-rest-docs) + - [Usage with Spring REST Docs - MockMvc](#usage-with-spring-rest-docs---mockmvc) + - [Usage with Spring REST Docs - RestAssured](#usage-with-spring-rest-docs---rest-assured) - [Documenting Bean Validation constraints](#documenting-bean-validation-constraints) - [Migrate existing Spring REST Docs tests](#migrate-existing-spring-rest-docs-tests) - [Security Definitions in OpenAPI](#security-definitions-in-openapi) @@ -134,7 +135,7 @@ See the [build.gradle](samples/restdocs-api-spec-sample/build.gradle) for the se The root project does not provide a maven plugin. But you can find a plugin that works with `restdocs-api-spec` at [BerkleyTechnologyServices/restdocs-spec](https://github.com/BerkleyTechnologyServices/restdocs-spec). -### Usage with Spring REST Docs +### Usage with Spring REST Docs - MockMvc The class [ResourceDocumentation](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceDocumentation.kt) contains the entry point for using the [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt). @@ -219,6 +220,9 @@ This makes the `urlTemplate` available in the snippet and we can depend on the n mockMvc.perform(get("/carts/{id}", cartId) ``` +### Usage with Spring REST Docs - REST Assured +The usage for REST Assured is similar to MockMVC, except that [com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt) is used instead of [com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt). + ### Documenting Bean Validation constraints Similar to the way Spring REST Docs allows to use [bean validation constraints](https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-constraints) to enhance your documentation, you can also use the constraints from your model classes to let `restdocs-api-spec` enrich the generated JsonSchemas. @@ -438,8 +442,3 @@ See [openapi2raml.gradle](samples/restdocs-api-spec-sample/openapi2raml.gradle). ./gradlew -b samples/restdocs-api-spec-sample/openapi2raml.gradle openapi2raml ``` -## Limitations - -### Rest Assured - -Spring REST Docs also supports REST Assured to write tests that produce documentation. We currently have not tried REST Assured with our project. From bac2ffa4c9e616483d2f1ba3250febfd762739cd Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Fri, 7 Dec 2018 10:14:25 +0100 Subject: [PATCH 03/10] fix kotlin lint failures (code style, wildcard imports) --- .../apispec/ResourceSnippetIntegrationTest.kt | 8 ++------ ...ssuredRestDocumentationWrapperIntegrationTest.kt | 13 +++++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt index 68c3fabb..495b9e46 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -98,8 +98,6 @@ open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMv .andDo(document(operationName, buildFullResourceSnippet())) } - - protected fun givenEndpointInvoked(flagValue: String = "true") { resultActions = mockMvc.perform( post("/some/{someId}/other/{otherId}", "id", 1) @@ -127,9 +125,9 @@ open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMv @SpringBootApplication internal open class TestApplication { - lateinit var applicaitonContext : ConfigurableApplicationContext + lateinit var applicationContext: ConfigurableApplicationContext fun main(args: Array) { - applicaitonContext = SpringApplication.run(TestApplication::class.java, *args) + applicationContext = SpringApplication.run(TestApplication::class.java, *args) } @RestController @@ -166,7 +164,6 @@ open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMv ) } - fun fieldDescriptors(): FieldDescriptors { val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) return ResourceDocumentation.fields( @@ -204,4 +201,3 @@ fun buildFullResourceSnippet(): ResourceSnippet { .build() ) } - diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt index 2658e2f7..6f6dc946 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt @@ -14,11 +14,16 @@ import org.junit.jupiter.api.extension.ExtendWith import org.springframework.hateoas.MediaTypes import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.RestDocumentationExtension -import org.springframework.restdocs.headers.HeaderDocumentation.* +import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName +import org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders +import org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders import org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel import org.springframework.restdocs.hypermedia.HypermediaDocumentation.links import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor -import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.restdocs.payload.PayloadDocumentation.requestFields +import org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath import org.springframework.restdocs.request.RequestDocumentation.parameterWithName import org.springframework.restdocs.request.RequestDocumentation.pathParameters import org.springframework.restdocs.restassured3.RestAssuredRestDocumentation @@ -45,12 +50,12 @@ class RestAssuredRestDocumentationWrapperIntegrationTest() { fun startSpringApplication() { app = ResourceSnippetIntegrationTest.TestApplication() app.main(arrayOf("--server.port=0")) - serverPort = app.applicaitonContext.environment.getProperty("local.server.port")?.toInt() + serverPort = app.applicationContext.environment.getProperty("local.server.port")?.toInt() } @AfterEach fun tearDown() { - app.applicaitonContext.close() + app.applicationContext.close() } fun givenEndpointInvoked(documentationFilter: Filter, flagValue: String = "true") { From bd768d314fe2798cf8050e09bb958a2000fba511 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Mon, 10 Dec 2018 13:45:42 +0100 Subject: [PATCH 04/10] Move duplicated code in wrappers to common abstract base class --- .../MockMvcRestDocumentationWrapper.kt | 101 +---------------- .../RestAssuredRestDocumentationWrapper.kt | 101 +---------------- .../apispec/RestDocumentationWrapper.kt | 105 ++++++++++++++++++ 3 files changed, 107 insertions(+), 200 deletions(-) create mode 100644 restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt index d3eefb4f..26fe1b24 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt @@ -1,29 +1,17 @@ package com.epages.restdocs.apispec -import org.springframework.restdocs.headers.HeaderDescriptor -import org.springframework.restdocs.headers.RequestHeadersSnippet -import org.springframework.restdocs.headers.ResponseHeadersSnippet -import org.springframework.restdocs.hypermedia.LinkDescriptor -import org.springframework.restdocs.hypermedia.LinksSnippet import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor -import org.springframework.restdocs.payload.FieldDescriptor -import org.springframework.restdocs.payload.RequestFieldsSnippet -import org.springframework.restdocs.payload.ResponseFieldsSnippet -import org.springframework.restdocs.request.ParameterDescriptor -import org.springframework.restdocs.request.PathParametersSnippet -import org.springframework.restdocs.request.RequestParametersSnippet import org.springframework.restdocs.snippet.Snippet - import java.util.function.Function /** * Convenience class to migrate to restdocs-openapi in a non-invasive way. * It it a wrapper and replacement for MockMvcRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. */ -object MockMvcRestDocumentationWrapper { +object MockMvcRestDocumentationWrapper : RestDocumentationWrapper() { @JvmOverloads @JvmStatic fun document( @@ -107,91 +95,4 @@ object MockMvcRestDocumentationWrapper { fun resourceDetails(): ResourceSnippetDetails { return ResourceSnippetParametersBuilder() } - - internal fun enhanceSnippetsWithResourceSnippet( - resourceDetails: ResourceSnippetDetails, - snippetFilter: Function, List>, - vararg snippets: Snippet - ): Array { - - val enhancedSnippets = if (snippets.none { it is ResourceSnippet }) { // No ResourceSnippet, so we configure our own based on the info of the other snippets - val resourceParameters = createBuilder(resourceDetails) - .requestFields( - snippets.filter { it is RequestFieldsSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - ) - .responseFields( - snippets.filter { it is ResponseFieldsSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - ) - .links( - snippets.filter { it is LinksSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - ) - .requestParameters( - *snippets.filter { it is RequestParametersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .pathParameters( - *snippets.filter { it is PathParametersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .requestHeaders( - *snippets.filter { it is RequestHeadersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .responseHeaders( - *snippets.filter { it is ResponseHeadersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .build() - snippets.toList() + ResourceDocumentation.resource(resourceParameters) - } else snippets.toList() - - return snippetFilter.apply(enhancedSnippets).toTypedArray() - } - - internal fun createBuilder(resourceDetails: ResourceSnippetDetails): ResourceSnippetParametersBuilder { - return when (resourceDetails) { - is ResourceSnippetParametersBuilder -> resourceDetails - else -> ResourceSnippetParametersBuilder() - .description(resourceDetails.description) - .summary(resourceDetails.summary) - .privateResource(resourceDetails.privateResource) - .deprecated(resourceDetails.deprecated) - .tags(*resourceDetails.tags.toTypedArray()) - } - } } diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt index fa108e3a..e9c0a501 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt @@ -1,29 +1,17 @@ package com.epages.restdocs.apispec -import org.springframework.restdocs.headers.HeaderDescriptor -import org.springframework.restdocs.headers.RequestHeadersSnippet -import org.springframework.restdocs.headers.ResponseHeadersSnippet -import org.springframework.restdocs.hypermedia.LinkDescriptor -import org.springframework.restdocs.hypermedia.LinksSnippet import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor -import org.springframework.restdocs.payload.FieldDescriptor -import org.springframework.restdocs.payload.RequestFieldsSnippet -import org.springframework.restdocs.payload.ResponseFieldsSnippet -import org.springframework.restdocs.request.ParameterDescriptor -import org.springframework.restdocs.request.PathParametersSnippet -import org.springframework.restdocs.request.RequestParametersSnippet import org.springframework.restdocs.restassured3.RestAssuredRestDocumentation import org.springframework.restdocs.restassured3.RestDocumentationFilter import org.springframework.restdocs.snippet.Snippet - import java.util.function.Function /** * Convenience class to migrate to restdocs-openapi in a non-invasive way. * It it a wrapper and replacement for RestAssuredRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. */ -object RestAssuredRestDocumentationWrapper { +object RestAssuredRestDocumentationWrapper : RestDocumentationWrapper() { @JvmOverloads @JvmStatic fun document( @@ -107,91 +95,4 @@ object RestAssuredRestDocumentationWrapper { fun resourceDetails(): ResourceSnippetDetails { return ResourceSnippetParametersBuilder() } - - internal fun enhanceSnippetsWithResourceSnippet( - resourceDetails: ResourceSnippetDetails, - snippetFilter: Function, List>, - vararg snippets: Snippet - ): Array { - - val enhancedSnippets = if (snippets.none { it is ResourceSnippet }) { // No ResourceSnippet, so we configure our own based on the info of the other snippets - val resourceParameters = createBuilder(resourceDetails) - .requestFields( - snippets.filter { it is RequestFieldsSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - ) - .responseFields( - snippets.filter { it is ResponseFieldsSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - ) - .links( - snippets.filter { it is LinksSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - ) - .requestParameters( - *snippets.filter { it is RequestParametersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .pathParameters( - *snippets.filter { it is PathParametersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .requestHeaders( - *snippets.filter { it is RequestHeadersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .responseHeaders( - *snippets.filter { it is ResponseHeadersSnippet } - .flatMap { - DescriptorExtractor.extractDescriptorsFor( - it - ) - } - .toTypedArray() - ) - .build() - snippets.toList() + ResourceDocumentation.resource(resourceParameters) - } else snippets.toList() - - return snippetFilter.apply(enhancedSnippets).toTypedArray() - } - - internal fun createBuilder(resourceDetails: ResourceSnippetDetails): ResourceSnippetParametersBuilder { - return when (resourceDetails) { - is ResourceSnippetParametersBuilder -> resourceDetails - else -> ResourceSnippetParametersBuilder() - .description(resourceDetails.description) - .summary(resourceDetails.summary) - .privateResource(resourceDetails.privateResource) - .deprecated(resourceDetails.deprecated) - .tags(*resourceDetails.tags.toTypedArray()) - } - } } diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt new file mode 100644 index 00000000..7a79e12f --- /dev/null +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt @@ -0,0 +1,105 @@ +package com.epages.restdocs.apispec + +import org.springframework.restdocs.headers.HeaderDescriptor +import org.springframework.restdocs.headers.RequestHeadersSnippet +import org.springframework.restdocs.headers.ResponseHeadersSnippet +import org.springframework.restdocs.hypermedia.LinkDescriptor +import org.springframework.restdocs.hypermedia.LinksSnippet +import org.springframework.restdocs.payload.FieldDescriptor +import org.springframework.restdocs.payload.RequestFieldsSnippet +import org.springframework.restdocs.payload.ResponseFieldsSnippet +import org.springframework.restdocs.request.ParameterDescriptor +import org.springframework.restdocs.request.PathParametersSnippet +import org.springframework.restdocs.request.RequestParametersSnippet +import org.springframework.restdocs.snippet.Snippet +import java.util.function.Function + +abstract class RestDocumentationWrapper { + + internal fun enhanceSnippetsWithResourceSnippet( + resourceDetails: ResourceSnippetDetails, + snippetFilter: Function, List>, + vararg snippets: Snippet + ): Array { + + val enhancedSnippets = if (snippets.none { it is ResourceSnippet }) { // No ResourceSnippet, so we configure our own based on the info of the other snippets + val resourceParameters = createBuilder(resourceDetails) + .requestFields( + snippets.filter { it is RequestFieldsSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + ) + .responseFields( + snippets.filter { it is ResponseFieldsSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + ) + .links( + snippets.filter { it is LinksSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + ) + .requestParameters( + *snippets.filter { it is RequestParametersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .pathParameters( + *snippets.filter { it is PathParametersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .requestHeaders( + *snippets.filter { it is RequestHeadersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .responseHeaders( + *snippets.filter { it is ResponseHeadersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .build() + snippets.toList() + ResourceDocumentation.resource(resourceParameters) + } else snippets.toList() + + return snippetFilter.apply(enhancedSnippets).toTypedArray() + } + + internal fun createBuilder(resourceDetails: ResourceSnippetDetails): ResourceSnippetParametersBuilder { + return when (resourceDetails) { + is ResourceSnippetParametersBuilder -> resourceDetails + else -> ResourceSnippetParametersBuilder() + .description(resourceDetails.description) + .summary(resourceDetails.summary) + .privateResource(resourceDetails.privateResource) + .deprecated(resourceDetails.deprecated) + .tags(*resourceDetails.tags.toTypedArray()) + } + } +} \ No newline at end of file From 7bd14cb57258266075c7a6aa68bd4eb9db47fb25 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Thu, 20 Dec 2018 13:17:11 +0100 Subject: [PATCH 05/10] Create two new modules for mockmvc and restassured support Remove the dependency to a specific spring-restdocs client from the restdocs-api-spec module and create two new modules: - restdocs-api-spec-mockmvc Contains the wrapper for spring-restdocs-mockmvnc - restdocs-api-spec-restassured Contains the wrapper for spring-restdocs-restassured --- restdocs-api-spec-mockmvc/build.gradle.kts | 30 +++++++ .../MockMvcRestDocumentationWrapper.kt | 2 +- ...RestDocumentationWrapperIntegrationTest.kt | 85 +++++++++++++++++- .../build.gradle.kts | 29 ++++++ .../RestAssuredRestDocumentationWrapper.kt | 2 +- ...RestDocumentationWrapperIntegrationTest.kt | 5 +- restdocs-api-spec/build.gradle.kts | 12 ++- .../apispec/RestDocumentationWrapper.kt | 2 +- .../apispec/ResourceSnippetIntegrationTest.kt | 90 +------------------ settings.gradle | 2 + 10 files changed, 162 insertions(+), 97 deletions(-) create mode 100644 restdocs-api-spec-mockmvc/build.gradle.kts rename {restdocs-api-spec => restdocs-api-spec-mockmvc}/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt (98%) rename {restdocs-api-spec => restdocs-api-spec-mockmvc}/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt (74%) create mode 100644 restdocs-api-spec-restassured/build.gradle.kts rename {restdocs-api-spec => restdocs-api-spec-restassured}/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt (98%) rename {restdocs-api-spec => restdocs-api-spec-restassured}/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt (98%) diff --git a/restdocs-api-spec-mockmvc/build.gradle.kts b/restdocs-api-spec-mockmvc/build.gradle.kts new file mode 100644 index 00000000..3be15f6a --- /dev/null +++ b/restdocs-api-spec-mockmvc/build.gradle.kts @@ -0,0 +1,30 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() + maven { url = uri("https://jitpack.io") } +} + +val springBootVersion: String by extra +val springRestDocsVersion: String by extra +val junitVersion: String by extra + +dependencies { + compile(kotlin("stdlib-jdk8")) + + compile(project(":restdocs-api-spec")) + compile("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion") + + testCompile(project(":restdocs-api-spec", "testConf")) + testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { + exclude("junit") + } + testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testImplementation("org.junit-pioneer:junit-pioneer:0.2.2") + testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion") +} + diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt b/restdocs-api-spec-mockmvc/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt similarity index 98% rename from restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt rename to restdocs-api-spec-mockmvc/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt index 26fe1b24..71516b82 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt +++ b/restdocs-api-spec-mockmvc/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt @@ -9,7 +9,7 @@ import java.util.function.Function /** * Convenience class to migrate to restdocs-openapi in a non-invasive way. - * It it a wrapper and replacement for MockMvcRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. + * It is a wrapper and replacement for MockMvcRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. */ object MockMvcRestDocumentationWrapper : RestDocumentationWrapper() { diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt similarity index 74% rename from restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt rename to restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt index fba3811b..9ecbe5cc 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt @@ -1,10 +1,14 @@ package com.epages.restdocs.apispec +import com.epages.restdocs.apispec.ResourceDocumentation.resource import org.assertj.core.api.Assertions.assertThatCode +import org.assertj.core.api.BDDAssertions.then 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.web.servlet.WebMvcTest +import org.springframework.hateoas.MediaTypes.HAL_JSON +import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName import org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders import org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders @@ -20,10 +24,14 @@ import org.springframework.restdocs.request.RequestDocumentation.pathParameters import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.io.File @ExtendWith(SpringExtension::class) @WebMvcTest -class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired mockMvc: MockMvc) : ResourceSnippetIntegrationTest(mockMvc) { +class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mockMvc: MockMvc) : ResourceSnippetIntegrationTest() { @Test fun should_document_both_restdocs_and_resource() { @@ -68,6 +76,81 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired mockMvc: MockMvc thenSnippetFileExists() } + @Test + fun should_document_request() { + givenEndpointInvoked() + + whenResourceSnippetDocumentedWithoutParameters() + + thenSnippetFileExists() + } + + @Test + fun should_document_request_with_description() { + givenEndpointInvoked() + + whenResourceSnippetDocumentedWithDescription() + + thenSnippetFileExists() + } + + @Test + fun should_document_request_with_fields() { + givenEndpointInvoked() + + whenResourceSnippetDocumentedWithRequestAndResponseFields() + + thenSnippetFileExists() + } + + @Test + fun should_document_request_with_null_field() { + givenEndpointInvoked("null") + + assertThatCode { this.whenResourceSnippetDocumentedWithRequestAndResponseFields() } + .doesNotThrowAnyException() + } + + private fun whenResourceSnippetDocumentedWithoutParameters() { + resultActions + .andDo(document(operationName, resource())) + } + + private fun whenResourceSnippetDocumentedWithDescription() { + resultActions + .andDo(document(operationName, resource("A description"))) + } + + private fun whenResourceSnippetDocumentedWithRequestAndResponseFields() { + resultActions + .andDo(document(operationName, buildFullResourceSnippet())) + } + + private fun givenEndpointInvoked(flagValue: String = "true") { + resultActions = mockMvc.perform( + post("/some/{someId}/other/{otherId}", "id", 1) + .contentType(APPLICATION_JSON) + .header("X-Custom-Header", "test") + .accept(HAL_JSON) + .content("""{ + "comment": "some", + "flag": $flagValue, + "count": 1 + }""".trimIndent() + ) + ).andExpect(status().isOk) + } + + private fun thenSnippetFileExists() { + with(generatedSnippetFile()) { + then(this).exists() + val contents = readText() + then(contents).isNotEmpty() + } + } + + private fun generatedSnippetFile() = File("build/generated-snippets", "$operationName/resource.json") + @Throws(Exception::class) private fun whenDocumentedWithRestdocsAndResource() { resultActions diff --git a/restdocs-api-spec-restassured/build.gradle.kts b/restdocs-api-spec-restassured/build.gradle.kts new file mode 100644 index 00000000..666a72cc --- /dev/null +++ b/restdocs-api-spec-restassured/build.gradle.kts @@ -0,0 +1,29 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} +repositories { + mavenCentral() + maven { url = uri("https://jitpack.io") } +} + +val springBootVersion: String by extra +val springRestDocsVersion: String by extra +val junitVersion: String by extra + +dependencies { + compile(kotlin("stdlib-jdk8")) + + compile(project(":restdocs-api-spec")) + compile("org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion") + + testCompile(project(":restdocs-api-spec", "testConf")) + testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { + exclude("junit") + } + testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testImplementation("org.junit-pioneer:junit-pioneer:0.2.2") + testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion") +} + diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt b/restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt similarity index 98% rename from restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt rename to restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt index e9c0a501..3f08284e 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt +++ b/restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt @@ -9,7 +9,7 @@ import java.util.function.Function /** * Convenience class to migrate to restdocs-openapi in a non-invasive way. - * It it a wrapper and replacement for RestAssuredRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. + * It is a wrapper and replacement for RestAssuredRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets. */ object RestAssuredRestDocumentationWrapper : RestDocumentationWrapper() { diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt similarity index 98% rename from restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt rename to restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt index 6f6dc946..73b736f9 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt @@ -31,8 +31,7 @@ import org.springframework.restdocs.restassured3.RestDocumentationFilter import java.io.File @ExtendWith(RestDocumentationExtension::class) -class RestAssuredRestDocumentationWrapperIntegrationTest() { - val operationName = "test-${System.currentTimeMillis()}" +class RestAssuredRestDocumentationWrapperIntegrationTest : ResourceSnippetIntegrationTest() { private lateinit var app: ResourceSnippetIntegrationTest.TestApplication private var serverPort: Int? = null @@ -238,7 +237,7 @@ class RestAssuredRestDocumentationWrapperIntegrationTest() { val operationRequestPreprocessor = OperationRequestPreprocessor { r -> r } return RestAssuredRestDocumentationWrapper.document( identifier = operationName, - resourceDetails = MockMvcRestDocumentationWrapper.resourceDetails() + resourceDetails = RestAssuredRestDocumentationWrapper.resourceDetails() .description("The Resource") .privateResource(true) .tag("some-tag"), diff --git a/restdocs-api-spec/build.gradle.kts b/restdocs-api-spec/build.gradle.kts index 23ac269f..c3d91bde 100755 --- a/restdocs-api-spec/build.gradle.kts +++ b/restdocs-api-spec/build.gradle.kts @@ -17,8 +17,7 @@ dependencies { compile(kotlin("stdlib-jdk8")) compile(kotlin("reflect")) - compile("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion") - compile("org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion") + compile("org.springframework.restdocs:spring-restdocs-core:$springRestDocsVersion") compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") @@ -37,3 +36,12 @@ dependencies { } +// build testJar for test classes dependency of MockMvc & RestAssured modules +val testConf by configurations.creating { } +val testJar by tasks.creating(Jar::class) { + classifier = "testJar" + from(java.sourceSets["test"].output) +} +artifacts.add("testConf", testJar) + + diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt index 7a79e12f..92fefe58 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt @@ -16,7 +16,7 @@ import java.util.function.Function abstract class RestDocumentationWrapper { - internal fun enhanceSnippetsWithResourceSnippet( + protected fun enhanceSnippetsWithResourceSnippet( resourceDetails: ResourceSnippetDetails, snippetFilter: Function, List>, vararg snippets: Snippet diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt index 495b9e46..2e73b22a 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -2,129 +2,43 @@ package com.epages.restdocs.apispec import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource -import org.assertj.core.api.Assertions.assertThatCode -import org.assertj.core.api.BDDAssertions.then import org.hibernate.validator.constraints.Length -import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.ConfigurableApplicationContext import org.springframework.hateoas.Link -import org.springframework.hateoas.MediaTypes.HAL_JSON import org.springframework.hateoas.Resource import org.springframework.hateoas.mvc.BasicLinkBuilder import org.springframework.http.HttpHeaders.ACCEPT import org.springframework.http.HttpHeaders.CONTENT_TYPE -import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.http.ResponseEntity import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName import org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel -import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document -import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActions -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController -import java.io.File import java.util.UUID import javax.validation.constraints.NotEmpty @ExtendWith(SpringExtension::class) @WebMvcTest @AutoConfigureRestDocs -open class ResourceSnippetIntegrationTest(@Autowired private val mockMvc: MockMvc) { +open class ResourceSnippetIntegrationTest { val operationName = "test-${System.currentTimeMillis()}" lateinit var resultActions: ResultActions - @Test - fun should_document_request() { - givenEndpointInvoked() - - whenResourceSnippetDocumentedWithoutParameters() - - thenSnippetFileExists() - } - - @Test - fun should_document_request_with_description() { - givenEndpointInvoked() - - whenResourceSnippetDocumentedWithDescription() - - thenSnippetFileExists() - } - - @Test - fun should_document_request_with_fields() { - givenEndpointInvoked() - - whenResourceSnippetDocumentedWithRequestAndResponseFields() - - thenSnippetFileExists() - } - - @Test - fun should_document_request_with_null_field() { - givenEndpointInvoked("null") - - assertThatCode { this.whenResourceSnippetDocumentedWithRequestAndResponseFields() } - .doesNotThrowAnyException() - } - - private fun whenResourceSnippetDocumentedWithoutParameters() { - resultActions - .andDo(document(operationName, resource())) - } - - private fun whenResourceSnippetDocumentedWithDescription() { - resultActions - .andDo(document(operationName, resource("A description"))) - } - - private fun whenResourceSnippetDocumentedWithRequestAndResponseFields() { - resultActions - .andDo(document(operationName, buildFullResourceSnippet())) - } - - protected fun givenEndpointInvoked(flagValue: String = "true") { - resultActions = mockMvc.perform( - post("/some/{someId}/other/{otherId}", "id", 1) - .contentType(APPLICATION_JSON) - .header("X-Custom-Header", "test") - .accept(HAL_JSON) - .content("""{ - "comment": "some", - "flag": $flagValue, - "count": 1 - }""".trimIndent() - ) - ).andExpect(status().isOk) - } - - fun thenSnippetFileExists() { - with(generatedSnippetFile()) { - then(this).exists() - val contents = readText() - then(contents).isNotEmpty() - } - } - - fun generatedSnippetFile() = File("build/generated-snippets", "$operationName/resource.json") - @SpringBootApplication - internal open class TestApplication { + open class TestApplication { lateinit var applicationContext: ConfigurableApplicationContext fun main(args: Array) { applicationContext = SpringApplication.run(TestApplication::class.java, *args) diff --git a/settings.gradle b/settings.gradle index 223be33c..ed0bf275 100755 --- a/settings.gradle +++ b/settings.gradle @@ -7,3 +7,5 @@ include 'restdocs-api-spec-openapi3-generator' include 'restdocs-api-spec-gradle-plugin' include 'restdocs-api-spec-sample' project(':restdocs-api-spec-sample').projectDir = file('samples/restdocs-api-spec-sample') +include 'restdocs-api-spec-mockmvc' +include 'restdocs-api-spec-restassured' From 63c86f8cdaa8f68810a13958267775d5198e3e59 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Thu, 20 Dec 2018 14:37:51 +0100 Subject: [PATCH 06/10] Add dependency on new restdocs-api-spec-mockmvc module (now required for mockmvc support) --- samples/restdocs-api-spec-sample/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/restdocs-api-spec-sample/build.gradle b/samples/restdocs-api-spec-sample/build.gradle index d05f2da3..51cfa4e6 100755 --- a/samples/restdocs-api-spec-sample/build.gradle +++ b/samples/restdocs-api-spec-sample/build.gradle @@ -39,8 +39,8 @@ dependencies { testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('org.springframework.restdocs:spring-restdocs-mockmvc') - //testCompile('com.epages:restdocs-api-spec:0.5.0') - testCompile project(':restdocs-api-spec') //enable for depending on the submodule directly +// testCompile('com.epages:restdocs-api-spec-mockmvc:0.5.0') + testCompile project(':restdocs-api-spec-mockmvc') //enable for depending on the submodule directly testCompile('com.google.guava:guava:23.0') } From 6dd3ac8ab865d7bf14567d6a7e4ed8c4f66cb896 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Thu, 20 Dec 2018 14:39:33 +0100 Subject: [PATCH 07/10] Fix: add missing dependency on jaxb-api - caused spring ApplicationContext init failure --- samples/restdocs-api-spec-sample/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/restdocs-api-spec-sample/build.gradle b/samples/restdocs-api-spec-sample/build.gradle index 51cfa4e6..acbd9ae8 100755 --- a/samples/restdocs-api-spec-sample/build.gradle +++ b/samples/restdocs-api-spec-sample/build.gradle @@ -33,6 +33,7 @@ ext { dependencies { compile('org.springframework.boot:spring-boot-starter-data-jpa') compile('org.springframework.boot:spring-boot-starter-data-rest') + compile('javax.xml.bind:jaxb-api:2.3.1') runtime('com.h2database:h2') testCompile('org.junit.jupiter:junit-jupiter-engine:5.3.2') From 910b7c6226e84b19c9350d0c676b26d23b3ad7ed Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Thu, 20 Dec 2018 15:06:22 +0100 Subject: [PATCH 08/10] Fix: build adjustments for gradle5 --- restdocs-api-spec/build.gradle.kts | 2 +- samples/restdocs-api-spec-sample/build.gradle | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/restdocs-api-spec/build.gradle.kts b/restdocs-api-spec/build.gradle.kts index c3d91bde..f56c1d0d 100755 --- a/restdocs-api-spec/build.gradle.kts +++ b/restdocs-api-spec/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { val testConf by configurations.creating { } val testJar by tasks.creating(Jar::class) { classifier = "testJar" - from(java.sourceSets["test"].output) + from(sourceSets["test"].output) } artifacts.add("testConf", testJar) diff --git a/samples/restdocs-api-spec-sample/build.gradle b/samples/restdocs-api-spec-sample/build.gradle index acbd9ae8..51cfa4e6 100755 --- a/samples/restdocs-api-spec-sample/build.gradle +++ b/samples/restdocs-api-spec-sample/build.gradle @@ -33,7 +33,6 @@ ext { dependencies { compile('org.springframework.boot:spring-boot-starter-data-jpa') compile('org.springframework.boot:spring-boot-starter-data-rest') - compile('javax.xml.bind:jaxb-api:2.3.1') runtime('com.h2database:h2') testCompile('org.junit.jupiter:junit-jupiter-engine:5.3.2') From 6019c911cf1216f1895d1e41701905571fb940f9 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Fri, 21 Dec 2018 08:52:03 +0100 Subject: [PATCH 09/10] Remove dependency on test classes from restdoc-api-spec module --- restdocs-api-spec-mockmvc/build.gradle.kts | 1 - .../apispec/ResourceSnippetIntegrationTest.kt | 0 .../build.gradle.kts | 1 - .../apispec/ResourceSnippetIntegrationTest.kt | 135 ++++++++++++++++++ ...RestDocumentationWrapperIntegrationTest.kt | 20 +-- restdocs-api-spec/build.gradle.kts | 9 -- 6 files changed, 137 insertions(+), 29 deletions(-) rename {restdocs-api-spec => restdocs-api-spec-mockmvc}/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt (100%) create mode 100644 restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt diff --git a/restdocs-api-spec-mockmvc/build.gradle.kts b/restdocs-api-spec-mockmvc/build.gradle.kts index 3be15f6a..71958034 100644 --- a/restdocs-api-spec-mockmvc/build.gradle.kts +++ b/restdocs-api-spec-mockmvc/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { compile(project(":restdocs-api-spec")) compile("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion") - testCompile(project(":restdocs-api-spec", "testConf")) testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude("junit") } diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt similarity index 100% rename from restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt rename to restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt diff --git a/restdocs-api-spec-restassured/build.gradle.kts b/restdocs-api-spec-restassured/build.gradle.kts index 666a72cc..af5f8362 100644 --- a/restdocs-api-spec-restassured/build.gradle.kts +++ b/restdocs-api-spec-restassured/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { compile(project(":restdocs-api-spec")) compile("org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion") - testCompile(project(":restdocs-api-spec", "testConf")) testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude("junit") } diff --git a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt new file mode 100644 index 00000000..d582d5df --- /dev/null +++ b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -0,0 +1,135 @@ +package com.epages.restdocs.apispec + +import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName +import com.epages.restdocs.apispec.ResourceDocumentation.resource +import org.hibernate.validator.constraints.Length +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.hateoas.Link +import org.springframework.hateoas.Resource +import org.springframework.hateoas.mvc.BasicLinkBuilder +import org.springframework.http.HttpHeaders.ACCEPT +import org.springframework.http.HttpHeaders.CONTENT_TYPE +import org.springframework.http.ResponseEntity +import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName +import org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.servlet.ResultActions +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RestController +import java.util.UUID +import javax.validation.constraints.NotEmpty + +@ExtendWith(SpringExtension::class) +@WebMvcTest +@AutoConfigureRestDocs +open class ResourceSnippetIntegrationTest { + + val operationName = "test-${System.currentTimeMillis()}" + + lateinit var resultActions: ResultActions + + private lateinit var app: ResourceSnippetIntegrationTest.TestApplication + protected var serverPort: Int? = null + + @BeforeEach + fun setUp(restDocumentation: RestDocumentationContextProvider) { + app = ResourceSnippetIntegrationTest.TestApplication() + app.main(arrayOf("--server.port=0")) + serverPort = app.applicationContext.environment.getProperty("local.server.port")?.toInt() + } + + @AfterEach + fun tearDown() { + app.applicationContext.close() + } + + @SpringBootApplication + open class TestApplication { + lateinit var applicationContext: ConfigurableApplicationContext + fun main(args: Array) { + applicationContext = SpringApplication.run(TestApplication::class.java, *args) + } + + @RestController + internal open class TestController { + + @PostMapping(path = ["/some/{someId}/other/{otherId}"]) + fun doSomething( + @PathVariable someId: String, + @PathVariable otherId: Int?, + @RequestHeader("X-Custom-Header") customHeader: String, + @RequestBody testDataHolder: TestDataHolder + ): ResponseEntity> { + val resource = Resource(testDataHolder.copy(id = UUID.randomUUID().toString())) + val link = BasicLinkBuilder.linkToCurrentMapping().slash("some").slash(someId).slash("other").slash(otherId).toUri().toString() + resource.add(Link(link, Link.REL_SELF)) + resource.add(Link(link, "multiple")) + resource.add(Link(link, "multiple")) + + return ResponseEntity + .ok() + .header("X-Custom-Header", customHeader) + .body>(resource) + } + } + } + + internal data class TestDataHolder( + @field:Length(min = 1, max = 255) + val comment: String? = null, + val flag: Boolean = false, + val count: Int = 0, + @field:NotEmpty + val id: String? = null + ) +} + +fun fieldDescriptors(): FieldDescriptors { + val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) + return ResourceDocumentation.fields( + fields.withPath("comment").description("the comment").optional(), + fields.withPath("flag").description("the flag"), + fields.withMappedPath("count", "count").description("the count") + ) +} + +fun buildFullResourceSnippet(): ResourceSnippet { + return resource( + ResourceSnippetParameters.builder() + .description("description") + .summary("summary") + .deprecated(true) + .privateResource(true) + .requestFields(fieldDescriptors()) + .responseFields(fieldDescriptors().and(fieldWithPath("id").description("id"))) + .requestHeaders( + headerWithName("X-Custom-Header").description("A custom header"), + headerWithName(ACCEPT).description("Accept") + ) + .responseHeaders( + headerWithName("X-Custom-Header").description("A custom header"), + headerWithName(CONTENT_TYPE).description("ContentType") + ) + .pathParameters( + parameterWithName("someId").description("some id"), + parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) + ) + .links( + linkWithRel("self").description("some"), + linkWithRel("multiple").description("multiple") + ) + .build() + ) +} diff --git a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt index 73b736f9..472ea5d7 100644 --- a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt @@ -7,7 +7,6 @@ import io.restassured.http.ContentType import io.restassured.specification.RequestSpecification import org.assertj.core.api.Assertions.assertThatCode import org.assertj.core.api.BDDAssertions.then -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -33,31 +32,16 @@ import java.io.File @ExtendWith(RestDocumentationExtension::class) class RestAssuredRestDocumentationWrapperIntegrationTest : ResourceSnippetIntegrationTest() { - private lateinit var app: ResourceSnippetIntegrationTest.TestApplication - private var serverPort: Int? = null - private lateinit var spec: RequestSpecification @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - startSpringApplication() + fun setUpSpec(restDocumentation: RestDocumentationContextProvider) { spec = RequestSpecBuilder() .addFilter(RestAssuredRestDocumentation.documentationConfiguration(restDocumentation)) .build() } - fun startSpringApplication() { - app = ResourceSnippetIntegrationTest.TestApplication() - app.main(arrayOf("--server.port=0")) - serverPort = app.applicationContext.environment.getProperty("local.server.port")?.toInt() - } - - @AfterEach - fun tearDown() { - app.applicationContext.close() - } - - fun givenEndpointInvoked(documentationFilter: Filter, flagValue: String = "true") { + private fun givenEndpointInvoked(documentationFilter: Filter, flagValue: String = "true") { RestAssured.given(spec) .filter(documentationFilter) .baseUri("http://localhost") diff --git a/restdocs-api-spec/build.gradle.kts b/restdocs-api-spec/build.gradle.kts index f56c1d0d..1f317e44 100755 --- a/restdocs-api-spec/build.gradle.kts +++ b/restdocs-api-spec/build.gradle.kts @@ -36,12 +36,3 @@ dependencies { } -// build testJar for test classes dependency of MockMvc & RestAssured modules -val testConf by configurations.creating { } -val testJar by tasks.creating(Jar::class) { - classifier = "testJar" - from(sourceSets["test"].output) -} -artifacts.add("testConf", testJar) - - From 9df75e7509402d119d6473449f17206dfc945026 Mon Sep 17 00:00:00 2001 From: cnsgithub Date: Fri, 21 Dec 2018 09:24:40 +0100 Subject: [PATCH 10/10] Add example for RestAssuredRestDocumentationWrapper usage to README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 4001816f..c31f64e3 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,25 @@ mockMvc.perform(get("/carts/{id}", cartId) ### Usage with Spring REST Docs - REST Assured The usage for REST Assured is similar to MockMVC, except that [com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt) is used instead of [com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt). +To use the ``RestAssuredRestDocumentationWrapper``, you have to add a dependency to [restdocs-api-spec-restassured](restdocs-api-spec-restassured) to your build. +```java +RestAssured.given(this.spec) + .filter(RestAssuredRestDocumentationWrapper.document("{method-name}", + "The API description", + requestParameters( + parameterWithName("param").description("the param") + ), + responseFields( + fieldWithPath("doc.timestamp").description("Creation timestamp") + ) + )) + .when() + .queryParam("param", "foo") + .get("/restAssuredExample") + .then() + .statusCode(200); +``` + ### Documenting Bean Validation constraints Similar to the way Spring REST Docs allows to use [bean validation constraints](https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-constraints) to enhance your documentation, you can also use the constraints from your model classes to let `restdocs-api-spec` enrich the generated JsonSchemas.