From 272ed24de8587a82b850f6c422d340cbdf7b77dd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Sep 2024 10:34:32 +0100 Subject: [PATCH 01/60] Use getUriTemplate of MockHttpServletRequest Fixes gh-939 --- .../docs/asciidoc/documenting-your-api.adoc | 2 +- .../MockMvcRestDocumentationConfigurer.java | 27 +++++++++++++-- ...ckMvcRestDocumentationConfigurerTests.java | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 3f8dc5de2..ce3dc5909 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -802,7 +802,7 @@ Uses the static `parameterWithName` method on `org.springframework.restdocs.requ The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. -TIP: If you use MockMvc, to make the path parameters available for documentation, you must build the request by using one of the methods on `RestDocumentationRequestBuilders` rather than `MockMvcRequestBuilders`. +TIP: If you use MockMvc with Spring Framework 6.1 or earlier, to make the path parameters available for documentation, you must build the request by using one of the methods on `RestDocumentationRequestBuilders` rather than `MockMvcRequestBuilders`. When documenting path parameters, the test fails if an undocumented path parameter is used in the request. Similarly, the test also fails if a documented path parameter is not found in the request and the path parameter has not been marked as optional. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index 73eeb9997..9c495ebb3 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package org.springframework.restdocs.mockmvc; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentationContext; @@ -27,6 +29,7 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.util.ReflectionUtils; import org.springframework.web.context.WebApplicationContext; /** @@ -85,6 +88,26 @@ public MockMvcOperationPreprocessorsConfigurer operationPreprocessors() { private final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { + private static final Function urlTemplateExtractor; + + static { + Function fromRequestAttribute = ( + request) -> (String) request.getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); + Function extractor; + try { + Method accessorMethod = MockHttpServletRequest.class.getMethod("getUriTemplate"); + extractor = (request) -> { + String urlTemplate = fromRequestAttribute.apply(request); + return (urlTemplate != null) ? urlTemplate + : (String) ReflectionUtils.invokeMethod(accessorMethod, request); + }; + } + catch (Exception ex) { + extractor = fromRequestAttribute; + } + urlTemplateExtractor = extractor; + } + private final RestDocumentationContextProvider contextManager; private ConfigurerApplyingRequestPostProcessor(RestDocumentationContextProvider contextManager) { @@ -97,7 +120,7 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - request.getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE)); + urlTemplateExtractor.apply(request)); configuration.put(RestDocumentationContext.class.getName(), context); request.setAttribute(RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION, configuration); MockMvcRestDocumentationConfigurer.this.apply(configuration, context); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index a135125cf..a1e3e9a0c 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -16,12 +16,18 @@ package org.springframework.restdocs.mockmvc; +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.util.ReflectionUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -86,6 +92,33 @@ public void noContentLengthHeaderWhenRequestHasNotContent() { assertThat(this.request.getHeader("Content-Length")).isNull(); } + @Test + @SuppressWarnings("unchecked") + public void uriTemplateFromRequestAttribute() { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation) + .beforeMockMvcCreated(null, null); + this.request.setAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); + postProcessor.postProcessRequest(this.request); + Map configuration = (Map) this.request + .getAttribute(RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION); + assertThat(configuration).containsEntry(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); + } + + @Test + @SuppressWarnings("unchecked") + public void uriTemplateFromRequest() { + Method setUriTemplate = ReflectionUtils.findMethod(MockHttpServletRequest.class, "setUriTemplate", + String.class); + Assume.assumeNotNull(setUriTemplate); + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation) + .beforeMockMvcCreated(null, null); + ReflectionUtils.invokeMethod(setUriTemplate, this.request, "{a}/{b}"); + postProcessor.postProcessRequest(this.request); + Map configuration = (Map) this.request + .getAttribute(RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION); + assertThat(configuration).containsEntry(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); + } + private void assertUriConfiguration(String scheme, String host, int port) { assertThat(scheme).isEqualTo(this.request.getScheme()); assertThat(host).isEqualTo(this.request.getServerName()); From 2e1ddae0fa554371dbb002e52fa6bb6aff602b56 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Sep 2024 10:37:26 +0100 Subject: [PATCH 02/60] Update JDK version in .sdkmanrc --- .sdkmanrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sdkmanrc b/.sdkmanrc index e1f4b3591..55b7d1b70 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=8.0.333-librca +java=8.0.422-librca From 609bf21ef4f71ac236fb4c97f56a49162a3d720d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 10:06:42 +0100 Subject: [PATCH 03/60] Use Spring Framework 6.1.x by default Closes gh-941 --- gradle.properties | 2 +- spring-restdocs-core/build.gradle | 2 +- spring-restdocs-mockmvc/build.gradle | 2 +- spring-restdocs-webtestclient/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 14d7b3c43..c3aeca76b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ org.gradle.parallel=true javaFormatVersion=0.0.41 jmustacheVersion=1.15 -springFrameworkVersion=6.0.14 +springFrameworkVersion=6.1.13 diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 62f791244..18d4e7a35 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -86,6 +86,6 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesRuntime compatibilityTest { dependency("Spring Framework") { springFramework -> springFramework.groupId = "org.springframework" - springFramework.versions = ["6.1.+", "6.2.+"] + springFramework.versions = ["6.0.+", "6.2.+"] } } diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index e0797246a..dfe138138 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -26,6 +26,6 @@ dependencies { compatibilityTest { dependency("Spring Framework") { springFramework -> springFramework.groupId = "org.springframework" - springFramework.versions = ["6.1.+", "6.2.+"] + springFramework.versions = ["6.0.+", "6.2.+"] } } \ No newline at end of file diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index 027d19ac4..f011fc9fb 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -25,6 +25,6 @@ dependencies { compatibilityTest { dependency("Spring Framework") { springFramework -> springFramework.groupId = "org.springframework" - springFramework.versions = ["6.1.+", "6.2.+"] + springFramework.versions = ["6.0.+", "6.2.+"] } } From d58b32cef027089c946db37b2c68d99b080ae07d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:05:38 +0100 Subject: [PATCH 04/60] Upgrade to compatibility test plugin 0.0.3 Closes gh-935 --- spring-restdocs-core/build.gradle | 2 +- spring-restdocs-mockmvc/build.gradle | 2 +- spring-restdocs-webtestclient/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 18d4e7a35..a7cfb3a2e 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3-SNAPSHOT" + id "io.spring.compatibility-test" version "0.0.3" id "java-library" id "java-test-fixtures" id "maven-publish" diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index dfe138138..8a487c2ba 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3-SNAPSHOT" + id "io.spring.compatibility-test" version "0.0.3" id "java-library" id "maven-publish" id "optional-dependencies" diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index f011fc9fb..2e26c4031 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3-SNAPSHOT" + id "io.spring.compatibility-test" version "0.0.3" id "java-library" id "maven-publish" } From e66e1078de81b4aafc01c1aa6ce46676e4246751 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:07:43 +0100 Subject: [PATCH 05/60] Upgrade to Develocity Conventions 0.0.21 --- settings.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index a3dd419d3..8c1b1607f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,8 +14,7 @@ pluginManagement { } plugins { - id "com.gradle.develocity" version "3.17.5" - id "io.spring.develocity.conventions" version "0.0.19" + id "io.spring.develocity.conventions" version "0.0.21" } rootProject.name = "spring-restdocs" From 10ade82fc8a039ab726aaf0f72823f62ee03106b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:08:17 +0100 Subject: [PATCH 06/60] Remove code that writes build scan URI to a file It was used by Concourse but is no longer needed after the move to GitHub Actions. --- settings.gradle | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/settings.gradle b/settings.gradle index 8c1b1607f..81adc416a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,18 +19,6 @@ plugins { rootProject.name = "spring-restdocs" -settings.gradle.projectsLoaded { - develocity { - buildScan { - settings.gradle.rootProject.getBuildDir().mkdirs() - new File(settings.gradle.rootProject.getBuildDir(), "build-scan-uri.txt").text = "build scan not generated" - buildScanPublished { scan -> - new File(settings.gradle.rootProject.getBuildDir(), "build-scan-uri.txt").text = "<${scan.buildScanUri}|build scan>\n" - } - } - } -} - include "docs" include "spring-restdocs-asciidoctor" include "spring-restdocs-bom" From a3a3b4af12d15171cc9628f403b0343eaafcefd6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:11:17 +0100 Subject: [PATCH 07/60] Upgrade to gradle/actions v4.1.0 --- .github/actions/prepare-gradle-build/action.yml | 2 +- .github/workflows/build-pull-request.yml | 4 +--- .github/workflows/validate-gradle-wrapper.yml | 11 ----------- 3 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 .github/workflows/validate-gradle-wrapper.yml diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index b6e78b562..b18ccb775 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -27,7 +27,7 @@ runs: ${{ inputs.java-version }} ${{ inputs.java-toolchain == 'true' && '17' || '' }} - name: Set Up Gradle - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 with: cache-read-only: false develocity-access-key: ${{ inputs.develocity-access-key }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 9f0f44538..58c865b91 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -17,10 +17,8 @@ jobs: distribution: 'liberica' - name: Check Out uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 - name: Set Up Gradle - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 - name: Build env: run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build diff --git a/.github/workflows/validate-gradle-wrapper.yml b/.github/workflows/validate-gradle-wrapper.yml deleted file mode 100644 index 7a473b3af..000000000 --- a/.github/workflows/validate-gradle-wrapper.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: "Validate Gradle Wrapper" -on: [push, pull_request] -permissions: - contents: read -jobs: - validation: - name: "Validate Gradle Wrapper" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 From 8f2fee176255657131f9c71df618cd257a4db2f8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:13:00 +0100 Subject: [PATCH 08/60] Upgrade to jfrog/setup-cli v4.4.1 --- .github/actions/sync-to-maven-central/action.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml index b33f3882a..838da9bd8 100644 --- a/.github/actions/sync-to-maven-central/action.yml +++ b/.github/actions/sync-to-maven-central/action.yml @@ -20,7 +20,7 @@ runs: using: composite steps: - name: Set Up JFrog CLI - uses: jfrog/setup-jfrog-cli@8bab65dc312163b065ac5b03de6f6a5bdd1bec41 # v4.1.3 + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 env: JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} - name: Download Release Artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1c32801a..c47a49093 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up JFrog CLI - uses: jfrog/setup-jfrog-cli@8bab65dc312163b065ac5b03de6f6a5bdd1bec41 # v4.1.3 + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 env: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Promote build From 20fdf4a5e35ea5b310ad86ca211bf02c575a11cb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:15:31 +0100 Subject: [PATCH 09/60] Upgrade to actions/checkout v4.2.0 --- .github/workflows/build-and-deploy-snapshot.yml | 2 +- .github/workflows/build-pull-request.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index eb964bf4c..631664347 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -12,7 +12,7 @@ jobs: if: ${{ github.repository == 'spring-projects/spring-restdocs' }} steps: - name: Check Out Code - uses: actions/checkout@v4 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Build and Publish id: build-and-publish uses: ./.github/actions/build diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 58c865b91..05c82c42f 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -16,7 +16,7 @@ jobs: java-version: '17' distribution: 'liberica' - name: Check Out - uses: actions/checkout@v4 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set Up Gradle uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 - name: Build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2c30d294..69eb0a048 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: git config --global core.longPaths true Stop-Service -name Docker - name: Check Out Code - uses: actions/checkout@v4 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Build id: build uses: ./.github/actions/build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c47a49093..46e33b788 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: if: ${{ github.repository == 'spring-projects/spring-restdocs' }} steps: - name: Check Out Code - uses: actions/checkout@v4 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Build and Publish id: build-and-publish uses: ./.github/actions/build @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check Out Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Sync to Maven Central uses: ./.github/actions/sync-to-maven-central with: @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check Out Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Create GitHub Release uses: ./.github/actions/create-github-release with: From e40fa5e7235fde8bc749be8075da07f6d5f49d2f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:17:37 +0100 Subject: [PATCH 10/60] Upgrade to actions/setup-java v4.4.0 --- .github/actions/prepare-gradle-build/action.yml | 2 +- .github/workflows/build-pull-request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index b18ccb775..3715cc197 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -20,7 +20,7 @@ runs: using: composite steps: - name: Set Up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: distribution: ${{ inputs.java-distribution }} java-version: | diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 05c82c42f..cf50659f3 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -11,7 +11,7 @@ jobs: if: ${{ github.repository == 'spring-projects/spring-restdocs' }} steps: - name: Set Up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: java-version: '17' distribution: 'liberica' From 694f28e0303d964f141e6ee2663837fddc566cf6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:20:23 +0100 Subject: [PATCH 11/60] Upgrade to Spring Java Format 0.0.43 --- gradle.properties | 2 +- .../org/springframework/restdocs/payload/JsonFieldPath.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index c3aeca76b..ccf82ebaa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,6 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 org.gradle.parallel=true -javaFormatVersion=0.0.41 +javaFormatVersion=0.0.43 jmustacheVersion=1.15 springFrameworkVersion=6.1.13 diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index c1897084e..9e7610475 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -153,7 +153,7 @@ enum PathType { /** * The path identifies multiple items in the payload. */ - MULTI; + MULTI } From 27145dfd0a97011ffaf8ea65c2a190c188159152 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:25:13 +0100 Subject: [PATCH 12/60] Test compatibility with latest versions of REST Assured --- spring-restdocs-restassured/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 92a8a33bb..812ed5d1d 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,4 +1,5 @@ plugins { + id "io.spring.compatibility-test" version "0.0.3" id "java-library" id "maven-publish" } @@ -22,3 +23,10 @@ dependencies { testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.mockito:mockito-core") } + +compatibilityTest { + dependency("REST Assured") { restAssured -> + restAssured.groupId = "io.rest-assured" + restAssured.versions = ["5.3.+", "5.4.+", "5.5.+"] + } +} From d2461e737d4a54c4e8070fb64d0820d794c52066 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Sep 2024 11:27:37 +0100 Subject: [PATCH 13/60] Upgrade to Gradle 8.10.2 --- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +++++-- gradlew.bat | 22 ++++++++++++---------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 34592 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 delta 34555 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e0930..df97d72b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f1..9d21a2183 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail From 4e7b36bfd97c61c4f8ced9ca64fe7bf287f5e6bb Mon Sep 17 00:00:00 2001 From: Daniel Vergien Date: Tue, 1 Oct 2024 16:30:22 +0200 Subject: [PATCH 14/60] Omit the version in the Maven Resources plugin example See gh-943 --- docs/src/docs/asciidoc/getting-started.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 41f98542d..c7b1bf9f0 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -146,7 +146,6 @@ The following listings show how to do so in both Maven and Gradle: <2> maven-resources-plugin - 2.7 copy-resources From 7f6a0f6cbc28b3a4ce0285fdf43d641f055633fa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 1 Oct 2024 16:12:23 +0100 Subject: [PATCH 15/60] Polish "Omit the version in the Maven Resources plugin example" See gh-943 --- docs/src/docs/asciidoc/getting-started.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index c7b1bf9f0..81e24381b 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -171,6 +171,7 @@ The following listings show how to do so in both Maven and Gradle: ---- <1> The existing declaration for the Asciidoctor plugin. <2> The resource plugin must be declared after the Asciidoctor plugin as they are bound to the same phase (`prepare-package`) and the resource plugin must run after the Asciidoctor plugin to ensure that the documentation is generated before it's copied. +If you are not using Spring Boot and its plugin management, declare the plugin with an appropriate ``. <3> Copy the generated documentation into the build output's `static/docs` directory, from where it will be included in the jar file. [source,indent=0,role="secondary"] From 5d9b7ec575e8aad87914f5f2def559c4fb2b35cd Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 17 Oct 2024 21:16:36 +0900 Subject: [PATCH 16/60] Use single Cookie header in HttpRequestSnippet See https://www.rfc-editor.org/rfc/rfc6265#section-5.4 See gh-947 --- .../springframework/restdocs/http/HttpRequestSnippet.java | 6 +++++- .../restdocs/http/HttpRequestSnippetTests.java | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index ef7617208..403b83bec 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -103,8 +103,12 @@ private List> getHeaders(OperationRequest request) { } } + List cookies = new ArrayList<>(); for (RequestCookie cookie : request.getCookies()) { - headers.add(header(HttpHeaders.COOKIE, String.format("%s=%s", cookie.getName(), cookie.getValue()))); + cookies.add(String.format("%s=%s", cookie.getName(), cookie.getValue())); + } + if (!cookies.isEmpty()) { + headers.add(header(HttpHeaders.COOKIE, String.join("; ", cookies))); } if (requiresFormEncodingContentTypeHeader(request)) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 41cccdd1c..447e00c05 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -82,8 +82,7 @@ public void getRequestWithCookies() throws IOException { .build()); assertThat(this.generatedSnippets.httpRequest()) .is(httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, "localhost") - .header(HttpHeaders.COOKIE, "name1=value1") - .header(HttpHeaders.COOKIE, "name2=value2")); + .header(HttpHeaders.COOKIE, "name1=value1; name2=value2")); } @Test From ef12effe9b4a952d1055ab52f17ad22b452b0626 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 11:48:25 +0100 Subject: [PATCH 17/60] Align GitHub Actions config more closely with Spring Boot --- .../actions/await-http-resource/action.yml | 20 +++++++++ .github/actions/build/action.yml | 34 +++++++++------ .../actions/create-github-release/action.yml | 16 +++---- .../changelog-generator.yml | 1 - .../actions/prepare-gradle-build/action.yml | 43 +++++++++++++------ .../actions/print-jvm-thread-dumps/action.yml | 2 +- .github/actions/send-notification/action.yml | 28 +++++++----- .../actions/sync-to-maven-central/action.yml | 39 +++++++---------- .../workflows/build-and-deploy-snapshot.yml | 27 ++++++------ .github/workflows/build-pull-request.yml | 24 ++++------- .github/workflows/ci.yml | 23 ++++++---- .github/workflows/release.yml | 27 ++++++------ 12 files changed, 166 insertions(+), 118 deletions(-) create mode 100644 .github/actions/await-http-resource/action.yml diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml new file mode 100644 index 000000000..ba177fb75 --- /dev/null +++ b/.github/actions/await-http-resource/action.yml @@ -0,0 +1,20 @@ +name: Await HTTP Resource +description: 'Waits for an HTTP resource to be available (a HEAD request succeeds)' +inputs: + url: + description: 'URL of the resource to await' + required: true +runs: + using: composite + steps: + - name: Await HTTP resource + shell: bash + run: | + url=${{ inputs.url }} + echo "Waiting for $url" + until curl --fail --head --silent ${{ inputs.url }} > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 6328d227e..70b051310 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -1,31 +1,39 @@ -name: 'Build' +name: Build description: 'Builds the project, optionally publishing it to a local deployment repository' inputs: - java-version: + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' required: false - default: '17' - description: 'The Java version to compile and test with' + gradle-cache-read-only: + description: 'Whether Gradle''s cache should be read only' + required: false + default: 'true' java-distribution: + description: 'Java distribution to use' required: false default: 'liberica' - description: 'The Java distribution to use for the build' - java-toolchain: + java-early-access: + description: 'Whether the Java version is in early access' required: false default: 'false' + java-toolchain: description: 'Whether a Java toolchain should be used' - publish: required: false default: 'false' + java-version: + description: 'Java version to compile and test with' + required: false + default: '17' + publish: description: 'Whether to publish artifacts ready for deployment to Artifactory' - develocity-access-key: required: false - description: 'The access key for authentication with ge.spring.io' + default: 'false' outputs: build-scan-url: - description: 'The URL, if any, of the build scan produced by the build' + description: 'URL, if any, of the build scan produced by the build' value: ${{ (inputs.publish == 'true' && steps.publish.outputs.build-scan-url) || steps.build.outputs.build-scan-url }} version: - description: 'The version that was built' + description: 'Version that was built' value: ${{ steps.read-version.outputs.version }} runs: using: composite @@ -33,10 +41,12 @@ runs: - name: Prepare Gradle Build uses: ./.github/actions/prepare-gradle-build with: + cache-read-only: ${{ inputs.gradle-cache-read-only }} develocity-access-key: ${{ inputs.develocity-access-key }} - java-version: ${{ inputs.java-version }} java-distribution: ${{ inputs.java-distribution }} + java-early-access: ${{ inputs.java-early-access }} java-toolchain: ${{ inputs.java-toolchain }} + java-version: ${{ inputs.java-version }} - name: Build id: build if: ${{ inputs.publish == 'false' }} diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index d5cc67fb9..03452537a 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -1,27 +1,27 @@ name: Create GitHub Release -description: Create the release on GitHub with a changelog +description: 'Create the release on GitHub with a changelog' inputs: milestone: - description: Name of the GitHub milestone for which a release will be created - required: true - token: - description: Token to use for authentication with GitHub + description: 'Name of the GitHub milestone for which a release will be created' required: true pre-release: - description: Whether the release is a pre-release (a milestone or release candidate) + description: 'Whether the release is a pre-release (a milestone or release candidate)' required: false default: 'false' + token: + description: 'Token to use for authentication with GitHub' + required: true runs: using: composite steps: - name: Generate Changelog uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 with: + config-file: .github/actions/create-github-release/changelog-generator.yml milestone: ${{ inputs.milestone }} token: ${{ inputs.token }} - config-file: .github/actions/create-github-release/changelog-generator.yml - name: Create GitHub Release + shell: bash env: GITHUB_TOKEN: ${{ inputs.token }} - shell: bash run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || '' }} diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml index 9c7fa3589..0c1077fdf 100644 --- a/.github/actions/create-github-release/changelog-generator.yml +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -1,5 +1,4 @@ changelog: - repository: spring-projects/spring-restdocs sections: - title: ":star: New Features" labels: diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index 3715cc197..819c13d07 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -1,36 +1,55 @@ -name: 'Prepare Gradle Build' +name: Prepare Gradle Build description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' inputs: - java-version: + cache-read-only: + description: 'Whether Gradle''s cache should be read only' + required: false + default: 'true' + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' required: false - default: '17' - description: 'The Java version to use for the build' java-distribution: + description: 'Java distribution to use' required: false default: 'liberica' - description: 'The Java distribution to use for the build' - java-toolchain: + java-early-access: + description: 'Whether the Java version is in early access. When true, forces java-distribution to temurin' required: false default: 'false' + java-toolchain: description: 'Whether a Java toolchain should be used' - develocity-access-key: required: false - description: 'The access key for authentication with ge.spring.io' + default: 'false' + java-version: + description: 'Java version to use for the build' + required: false + default: '17' runs: using: composite steps: + - name: Free Disk Space + if: ${{ runner.os == 'Linux' }} + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + tool-cache: true + docker-images: false - name: Set Up Java - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@v4 with: - distribution: ${{ inputs.java-distribution }} + distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }} java-version: | - ${{ inputs.java-version }} + ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-toolchain == 'true' && '17' || '' }} - - name: Set Up Gradle + - name: Set Up Gradle With Read/Write Cache + if: ${{ inputs.cache-read-only == 'false' }} uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 with: cache-read-only: false develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + with: + develocity-access-key: ${{ inputs.develocity-access-key }} - name: Configure Gradle Properties shell: bash run: | diff --git a/.github/actions/print-jvm-thread-dumps/action.yml b/.github/actions/print-jvm-thread-dumps/action.yml index 9b0905b77..bcaebf367 100644 --- a/.github/actions/print-jvm-thread-dumps/action.yml +++ b/.github/actions/print-jvm-thread-dumps/action.yml @@ -1,5 +1,5 @@ name: Print JVM thread dumps -description: Prints a thread dump for all running JVMs +description: 'Prints a thread dump for all running JVMs' runs: using: composite steps: diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml index d13897763..b379e6789 100644 --- a/.github/actions/send-notification/action.yml +++ b/.github/actions/send-notification/action.yml @@ -1,33 +1,39 @@ name: Send Notification -description: Sends a Google Chat message as a notification of the job's outcome +description: 'Sends a Google Chat message as a notification of the job''s outcome' inputs: - webhook-url: - description: 'Google Chat Webhook URL' - required: true - status: - description: 'Status of the job' - required: true build-scan-url: description: 'URL of the build scan to include in the notification' + required: false run-name: description: 'Name of the run to include in the notification' + required: false default: ${{ format('{0} {1}', github.ref_name, github.job) }} + status: + description: 'Status of the job' + required: true + webhook-url: + description: 'Google Chat Webhook URL' + required: true runs: using: composite steps: - - shell: bash + - name: Prepare Variables + shell: bash run: | echo "BUILD_SCAN=${{ inputs.build-scan-url == '' && ' [build scan unavailable]' || format(' [<{0}|Build Scan>]', inputs.build-scan-url) }}" >> "$GITHUB_ENV" echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV" - - shell: bash + - name: Success Notification if: ${{ inputs.status == 'success' }} + shell: bash run: | curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was successful ${{ env.BUILD_SCAN }}"}' || true - - shell: bash + - name: Failure Notification if: ${{ inputs.status == 'failure' }} + shell: bash run: | curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: " *<${{ env.RUN_URL }}|${{ inputs.run-name }}> failed* ${{ env.BUILD_SCAN }}"}' || true - - shell: bash + - name: Cancel Notification if: ${{ inputs.status == 'cancelled' }} + shell: bash run: | curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml index 838da9bd8..ec3b20d36 100644 --- a/.github/actions/sync-to-maven-central/action.yml +++ b/.github/actions/sync-to-maven-central/action.yml @@ -1,20 +1,20 @@ name: Sync to Maven Central -description: Syncs a release to Maven Central and waits for it to be available for use +description: 'Syncs a release to Maven Central and waits for it to be available for use' inputs: jfrog-cli-config-token: description: 'Config token for the JFrog CLI' required: true - spring-restdocs-version: - description: 'The version of Spring REST Docs that is being synced to Central' - required: true - ossrh-s01-token-username: - description: 'Username for authentication with s01.oss.sonatype.org' + ossrh-s01-staging-profile: + description: 'Staging profile to use when syncing to Central' required: true ossrh-s01-token-password: description: 'Password for authentication with s01.oss.sonatype.org' required: true - ossrh-s01-staging-profile: - description: 'Staging profile to use when syncing to Central' + ossrh-s01-token-username: + description: 'Username for authentication with s01.oss.sonatype.org' + required: true + spring-restdocs-version: + description: 'Version of Spring REST Docs that is being synced to Central' required: true runs: using: composite @@ -29,22 +29,15 @@ runs: - name: Sync uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 with: - username: ${{ inputs.ossrh-s01-token-username }} + close: true + create: true + generate-checksums: true password: ${{ inputs.ossrh-s01-token-password }} + release: true staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} - create: true upload: true - close: true - release: true - generate-checksums: true + username: ${{ inputs.ossrh-s01-token-username }} - name: Await - shell: bash - run: | - url=${{ format('https://repo.maven.apache.org/maven2/org/springframework/restdocs/spring-restdocs-core/%7B0%7D/spring-restdocs-core-%7B0%7D.jar', inputs.spring-restdocs-version) }} - echo "Waiting for $url" - until curl --fail --head --silent $url > /dev/null - do - echo "." - sleep 60 - done - echo "$url is available" + uses: ./.github/actions/await-http-resource + with: + url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/restdocs/spring-restdocs-core/%7B0%7D/spring-restdocs-core-%7B0%7D.jar', inputs.spring-restdocs-version) }} diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 631664347..a77698bd7 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -8,37 +8,38 @@ concurrency: jobs: build-and-deploy-snapshot: name: Build and Deploy Snapshot - runs-on: ubuntu-latest if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} steps: - name: Check Out Code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@v4 - name: Build and Publish id: build-and-publish uses: ./.github/actions/build with: - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false publish: true - name: Deploy uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: - uri: 'https://repo.spring.io/' - username: ${{ secrets.ARTIFACTORY_USERNAME }} - password: ${{ secrets.ARTIFACTORY_PASSWORD }} - build-name: 'spring-restdocs-3.0.x' - repository: 'libs-snapshot-local' + artifact-properties: | + /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false + build-name: ${{ format('spring-restdocs-{0}', github.ref_name) }} folder: 'deployment-repository' + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository: ${{ 'libs-snapshot-local' }} signing-key: ${{ secrets.GPG_PRIVATE_KEY }} signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} - artifact-properties: | - /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false + uri: 'https://repo.spring.io/' + username: ${{ secrets.ARTIFACTORY_USERNAME }} - name: Send Notification - uses: ./.github/actions/send-notification if: always() + uses: ./.github/actions/send-notification with: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - status: ${{ job.status }} build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} outputs: version: ${{ steps.build-and-publish.outputs.version }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index cf50659f3..164c04dc2 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -1,30 +1,24 @@ name: Build Pull Request on: pull_request - permissions: contents: read - jobs: build: name: Build Pull Request - runs-on: ubuntu-latest if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} steps: - - name: Set Up JDK 17 - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 - with: - java-version: '17' - distribution: 'liberica' - - name: Check Out - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - name: Set Up Gradle - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + - name: Check Out Code + uses: actions/checkout@v4 - name: Build - env: - run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build + id: build + uses: ./.github/actions/build + - name: Print JVM Thread Dumps When Cancelled + if: cancelled() + uses: ./.github/actions/print-jvm-thread-dumps - name: Upload Build Reports - uses: actions/upload-artifact@v4 if: failure() + uses: actions/upload-artifact@v4 with: name: build-reports path: '**/build/reports/' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69eb0a048..c6b216c28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,13 @@ concurrency: jobs: ci: name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' - runs-on: ${{ matrix.os.id }} if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ matrix.os.id }} strategy: + fail-fast: false matrix: os: - - id: ubuntu-latest + - id: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name: Linux - id: windows-latest name: Windows @@ -24,6 +25,8 @@ jobs: toolchain: true - version: 22 toolchain: true + - version: 23 + toolchain: true exclude: - os: name: Linux @@ -37,20 +40,22 @@ jobs: git config --global core.longPaths true Stop-Service -name Docker - name: Check Out Code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@v4 - name: Build id: build uses: ./.github/actions/build with: - java-version: ${{ matrix.java.version }} - java-distribution: ${{ matrix.java.distribution || 'liberica' }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false + java-early-access: ${{ matrix.java.early-access || 'false' }} + java-distribution: ${{ matrix.java.distribution }} java-toolchain: ${{ matrix.java.toolchain }} - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + java-version: ${{ matrix.java.version }} - name: Send Notification - uses: ./.github/actions/send-notification if: always() + uses: ./.github/actions/send-notification with: - webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} - status: ${{ job.status }} build-scan-url: ${{ steps.build.outputs.build-scan-url }} run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46e33b788..b1aa842f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,28 +8,29 @@ concurrency: jobs: build-and-stage-release: name: Build and Stage Release - runs-on: ubuntu-latest if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ vars.UBUNTU_MEDIUIM || 'ubuntu-latest' }} steps: - name: Check Out Code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@v4 - name: Build and Publish id: build-and-publish uses: ./.github/actions/build with: - develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false publish: true - name: Stage Release uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: - uri: 'https://repo.spring.io/' - username: ${{ secrets.ARTIFACTORY_USERNAME }} + build-name: ${{ format('spring-restdocs-{0}', github.ref_name) }} + folder: 'deployment-repository' password: ${{ secrets.ARTIFACTORY_PASSWORD }} - build-name: ${{ format('spring-restdocs-{0}', steps.build-and-publish.outputs.version)}} repository: 'libs-staging-local' - folder: 'deployment-repository' signing-key: ${{ secrets.GPG_PRIVATE_KEY }} signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: 'https://repo.spring.io/' + username: ${{ secrets.ARTIFACTORY_USERNAME }} outputs: version: ${{ steps.build-and-publish.outputs.version }} sync-to-maven-central: @@ -37,10 +38,10 @@ jobs: needs: - build-and-stage-release - verify - runs-on: ubuntu-latest + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} steps: - name: Check Out Code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Sync to Maven Central uses: ./.github/actions/sync-to-maven-central with: @@ -54,23 +55,23 @@ jobs: needs: - build-and-stage-release - sync-to-maven-central - runs-on: ubuntu-latest + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} steps: - name: Set up JFrog CLI uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 env: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - name: Promote build + - name: Promote Open Source Build run: jfrog rt build-promote ${{ format('spring-restdocs-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local create-github-release: name: Create GitHub Release needs: - build-and-stage-release - promote-release - runs-on: ubuntu-latest + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} steps: - name: Check Out Code - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Create GitHub Release uses: ./.github/actions/create-github-release with: From 12c93577b3c3a12639f7ec67a9686447dd6888fa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 11:52:06 +0100 Subject: [PATCH 18/60] Don't free disk space on GitHub Actions as it isn't necessary --- .github/actions/prepare-gradle-build/action.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index 819c13d07..f9969cf31 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -27,12 +27,6 @@ inputs: runs: using: composite steps: - - name: Free Disk Space - if: ${{ runner.os == 'Linux' }} - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 - with: - tool-cache: true - docker-images: false - name: Set Up Java uses: actions/setup-java@v4 with: From 92e027bf266e1243bcca27cb21c40b1bca9849bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 12:01:15 +0100 Subject: [PATCH 19/60] Correct needs in release workflow --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1aa842f4..0c19cae6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,6 @@ jobs: name: Sync to Maven Central needs: - build-and-stage-release - - verify runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} steps: - name: Check Out Code From 156cf11e355dd8d5cb9560e5f69009ebf49fca65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 12:55:54 +0100 Subject: [PATCH 20/60] Allow milestone dependencies for compatibility testing of a release --- build.gradle | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8a912885a..42e043b3e 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,13 @@ allprojects { group = "org.springframework.restdocs" repositories { mavenCentral() - if (version.contains('-')) { - maven { url "https://repo.spring.io/milestone" } + maven { + url "https://repo.spring.io/milestone" + content { + includeGroup "io.micrometer" + includeGroup "io.projectreactor" + includeGroup "org.springframework" + } } if (version.endsWith('-SNAPSHOT')) { maven { url "https://repo.spring.io/snapshot" } From 03043693ce1eac48c208a5d44d657142efef8010 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 13:15:39 +0100 Subject: [PATCH 21/60] Restore previous build-name config for snapshots and releases --- .github/workflows/build-and-deploy-snapshot.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index a77698bd7..bbc755b23 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -25,7 +25,7 @@ jobs: with: artifact-properties: | /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false - build-name: ${{ format('spring-restdocs-{0}', github.ref_name) }} + build-name: 'spring-restdocs-3.0.x' folder: 'deployment-repository' password: ${{ secrets.ARTIFACTORY_PASSWORD }} repository: ${{ 'libs-snapshot-local' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c19cae6b..73b4e72cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Stage Release uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: - build-name: ${{ format('spring-restdocs-{0}', github.ref_name) }} + build-name: ${{ format('spring-restdocs-{0}', steps.build-and-publish.outputs.version) }} folder: 'deployment-repository' password: ${{ secrets.ARTIFACTORY_PASSWORD }} repository: 'libs-staging-local' From c61516d4ab4f3ddf9152532102d8dab860492819 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 13:27:32 +0100 Subject: [PATCH 22/60] Add workflow to delete a staged release --- .github/workflows/delete-staged-release.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/delete-staged-release.yml diff --git a/.github/workflows/delete-staged-release.yml b/.github/workflows/delete-staged-release.yml new file mode 100644 index 000000000..98db31725 --- /dev/null +++ b/.github/workflows/delete-staged-release.yml @@ -0,0 +1,17 @@ +name: Delete Staged Release +on: + workflow_dispatch: + inputs: + build-version: + description: 'Version of the build to delete' + required: true +jobs: + delete-staged-release: + name: Delete Staged Release + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Delete Build + run: jfrog rt delete --build spring-restdocs-${{ github.event.inputs.build-version }} From d33965465c451744b4b8a547acff4f24b537e0fb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 13:29:23 +0100 Subject: [PATCH 23/60] Polish workflow --- .github/workflows/delete-staged-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/delete-staged-release.yml b/.github/workflows/delete-staged-release.yml index 98db31725..56aff2b40 100644 --- a/.github/workflows/delete-staged-release.yml +++ b/.github/workflows/delete-staged-release.yml @@ -8,6 +8,7 @@ on: jobs: delete-staged-release: name: Delete Staged Release + runs-on: ubuntu-latest steps: - name: Set up JFrog CLI uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 From ab8f6b7bf21541b7f7fea2e09a458c9445657662 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 14:37:25 +0100 Subject: [PATCH 24/60] Next development version (v3.0.3-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ccf82ebaa..5f6bea503 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.2-SNAPSHOT +version=3.0.3-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 From 840e05561adf2eb9ac272e9023ebaf5acb0e3678 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Oct 2024 14:43:20 +0100 Subject: [PATCH 25/60] Set artifact properties for docs publishing in release workflow --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73b4e72cb..ef92b5cb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,8 @@ jobs: - name: Stage Release uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: + artifact-properties: | + /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false build-name: ${{ format('spring-restdocs-{0}', steps.build-and-publish.outputs.version) }} folder: 'deployment-repository' password: ${{ secrets.ARTIFACTORY_PASSWORD }} From aa94fb1d9cbe572131015ca9185820e239c5b71a Mon Sep 17 00:00:00 2001 From: SJLEE Date: Wed, 23 Oct 2024 16:53:44 +0900 Subject: [PATCH 26/60] Correct typo from 'Explict' to 'Explicit' See gh-948 --- .../restdocs/payload/RequestFieldsSnippetTests.java | 5 +++-- .../restdocs/payload/ResponseFieldsSnippetTests.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 4d8867e34..105a7ef9c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -44,6 +44,7 @@ * Tests for {@link RequestFieldsSnippet}. * * @author Andy Wilkinson + * @author Sungjun Lee */ public class RequestFieldsSnippetTests extends AbstractSnippetTests { @@ -239,7 +240,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { } @Test - public void fieldWithExplictExactlyMatchingType() throws IOException { + public void fieldWithExplicitExactlyMatchingType() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5 }").build()); assertThat(this.generatedSnippets.requestFields()) @@ -247,7 +248,7 @@ public void fieldWithExplictExactlyMatchingType() throws IOException { } @Test - public void fieldWithExplictVariesType() throws IOException { + public void fieldWithExplicitVariesType() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5 }").build()); assertThat(this.generatedSnippets.requestFields()) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index d8545ea48..3aad29271 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -43,6 +43,7 @@ * Tests for {@link ResponseFieldsSnippet}. * * @author Andy Wilkinson + * @author Sungjun Lee */ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { @@ -224,7 +225,7 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { } @Test - public void fieldWithExplictExactlyMatchingType() throws IOException { + public void fieldWithExplicitExactlyMatchingType() throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) .document(this.operationBuilder.response().content("{\"a\": 5 }").build()); assertThat(this.generatedSnippets.responseFields()) @@ -232,7 +233,7 @@ public void fieldWithExplictExactlyMatchingType() throws IOException { } @Test - public void fieldWithExplictVariesType() throws IOException { + public void fieldWithExplicitVariesType() throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) .document(this.operationBuilder.response().content("{\"a\": 5 }").build()); assertThat(this.generatedSnippets.responseFields()) From ba89d685a4348bc07fdff53c2297acfd839b2ed9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 23 Oct 2024 11:37:09 +0100 Subject: [PATCH 27/60] Polish "Correct typo from 'Explict' to 'Explicit'" See gh-948 --- .../restdocs/payload/RequestFieldsSnippetTests.java | 2 +- .../restdocs/payload/ResponseFieldsSnippetTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 105a7ef9c..bb6ff7b42 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 3aad29271..8372ae5e7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 0abc76307e3a4bdcab376ba068a8122f7b39b328 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 31 Oct 2024 19:49:27 +0000 Subject: [PATCH 28/60] Fix incompatibility with AsciidoctorJ 3.0 AsciidoctorJ 3.0 contains a breaking change to the signature of Preprocessor#process. This commit avoids this incompatibility by rewriting the preprocessor extension in Ruby. Fixes gh-949 --- spring-restdocs-asciidoctor/build.gradle | 9 +++ .../RestDocsExtensionRegistry.java | 6 +- .../SnippetsDirectoryResolver.java | 11 ++- .../extensions/default_attributes.rb | 14 ++++ .../DefaultAttributesPreprocessorTests.java | 67 ------------------- 5 files changed, 35 insertions(+), 72 deletions(-) create mode 100644 spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb delete mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index ceb5202f0..717744ea5 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -1,6 +1,7 @@ plugins { id "java-library" id "maven-publish" + id "io.spring.compatibility-test" version "0.0.3" } description = "Spring REST Docs Asciidoctor Extension" @@ -19,3 +20,11 @@ dependencies { testRuntimeOnly("org.asciidoctor:asciidoctorj-pdf") } + +compatibilityTest { + dependency("AsciidoctorJ") { asciidoctorj -> + asciidoctorj.groupId = "org.asciidoctor" + asciidoctorj.artifactId = "asciidoctorj" + asciidoctorj.versions = ["3.0.0"] + } +} \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java index 0a58a7cf9..5432b23c0 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,9 @@ public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); + asciidoctor.rubyExtensionRegistry() + .loadClass(RestDocsExtensionRegistry.class.getResourceAsStream("/extensions/default_attributes.rb")) + .preprocessor("DefaultAttributes"); asciidoctor.rubyExtensionRegistry() .loadClass(RestDocsExtensionRegistry.class.getResourceAsStream("/extensions/operation_block_macro.rb")) .blockMacro("operation", "OperationBlockMacro"); diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java index af2e94822..2ca3ea5f5 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,9 +30,14 @@ * * @author Andy Wilkinson */ -class SnippetsDirectoryResolver { +public class SnippetsDirectoryResolver { - File getSnippetsDirectory(Map attributes) { + /** + * Returns the snippets directory derived from the given {@code attributes}. + * @param attributes the attributes + * @return the snippets directory + */ + public File getSnippetsDirectory(Map attributes) { if (System.getProperty("maven.home") != null) { return getMavenSnippetsDirectory(attributes); } diff --git a/spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb b/spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb new file mode 100644 index 000000000..a4060d5b3 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb @@ -0,0 +1,14 @@ +require 'asciidoctor/extensions' +require 'java' + +class DefaultAttributes < Asciidoctor::Extensions::Preprocessor + + def process(document, reader) + resolver = org.springframework.restdocs.asciidoctor.SnippetsDirectoryResolver.new() + attributes = document.attributes + attributes["snippets"] = resolver.getSnippetsDirectory(attributes) unless attributes.has_key?("snippets") + false + end + +end + \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java deleted file mode 100644 index 154cd73f6..000000000 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2014-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.asciidoctor; - -import java.io.File; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.Attributes; -import org.asciidoctor.Options; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link DefaultAttributesPreprocessor}. - * - * @author Andy Wilkinson - */ -public class DefaultAttributesPreprocessorTests { - - @Test - public void snippetsAttributeIsSet() { - String converted = createAsciidoctor().convert("{snippets}", createOptions("projectdir=../../..")); - assertThat(converted).contains("build" + File.separatorChar + "generated-snippets"); - } - - @Test - public void snippetsAttributeFromConvertArgumentIsNotOverridden() { - String converted = createAsciidoctor().convert("{snippets}", - createOptions("snippets=custom projectdir=../../..")); - assertThat(converted).contains("custom"); - } - - @Test - public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { - String converted = createAsciidoctor().convert(":snippets: custom\n{snippets}", - createOptions("projectdir=../../..")); - assertThat(converted).contains("custom"); - } - - private Options createOptions(String attributes) { - Options options = Options.builder().build(); - options.setAttributes(Attributes.builder().arguments(attributes).build()); - return options; - } - - private Asciidoctor createAsciidoctor() { - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); - return asciidoctor; - } - -} From 7f0e6dde7436765e7c99159a295ab2e99cbef69e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Nov 2024 09:38:16 +0000 Subject: [PATCH 29/60] Next development version (v3.0.4-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5f6bea503..64a74adbb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.3-SNAPSHOT +version=3.0.4-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 From 2d838a1a347d07a46888e8919469be1cdd254a04 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Nov 2024 11:26:58 +0000 Subject: [PATCH 30/60] Ensure that default value for snippets attribute is an absolute path Previously, the snippets attribute was absolute when using Gradle and relative to the docdir when using Maven. The relative path that was used with Maven only worked as long as the working directory when invoking Asciidoctor was the same as the docdir. This was the case until 3.1.0 of the Asciidoctor Maven Plugin when the working directory and docdir diverged at which point the snippets could no longer be found. This commit updates the snippets directory resolver to return an absolute file when using Maven, just has it already does when using Gradle. The resolution for Gradle has also been updated to explicity make the File absolute rather than relying on the gradle-projectdir or projectdir attributes having an absolute value. Fixes gh-950 --- .../asciidoctor/SnippetsDirectoryResolver.java | 8 ++++---- .../SnippetsDirectoryResolverTests.java | 17 ++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java index 2ca3ea5f5..521c08329 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -25,8 +25,7 @@ /** * Resolves the directory from which snippets can be read for inclusion in an Asciidoctor - * document. The resolved directory is relative to the {@code docdir} of the Asciidoctor - * document that it being rendered. + * document. The resolved directory is absolute. * * @author Andy Wilkinson */ @@ -46,7 +45,7 @@ public File getSnippetsDirectory(Map attributes) { private File getMavenSnippetsDirectory(Map attributes) { Path docdir = Paths.get(getRequiredAttribute(attributes, "docdir")); - return new File(docdir.relativize(findPom(docdir).getParent()).toFile(), "target/generated-snippets"); + return new File(findPom(docdir).getParent().toFile(), "target/generated-snippets").getAbsoluteFile(); } private Path findPom(Path docdir) { @@ -63,7 +62,8 @@ private Path findPom(Path docdir) { private File getGradleSnippetsDirectory(Map attributes) { return new File(getRequiredAttribute(attributes, "gradle-projectdir", - () -> getRequiredAttribute(attributes, "projectdir")), "build/generated-snippets"); + () -> getRequiredAttribute(attributes, "projectdir")), "build/generated-snippets") + .getAbsoluteFile(); } private String getRequiredAttribute(Map attributes, String name) { diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java index b6cc1b246..2bede597c 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,13 +39,13 @@ public class SnippetsDirectoryResolverTests { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocdir() throws IOException { + public void mavenProjectsUseTargetGeneratedSnippets() throws IOException { this.temporaryFolder.newFile("pom.xml"); Map attributes = new HashMap<>(); attributes.put("docdir", new File(this.temporaryFolder.getRoot(), "src/main/asciidoc").getAbsolutePath()); File snippetsDirectory = getMavenSnippetsDirectory(attributes); - assertThat(snippetsDirectory).isRelative(); - assertThat(snippetsDirectory).isEqualTo(new File("../../../target/generated-snippets")); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File(this.temporaryFolder.getRoot(), "target/generated-snippets")); } @Test @@ -69,7 +69,8 @@ public void gradleProjectsUseBuildGeneratedSnippetsBeneathGradleProjectdir() { Map attributes = new HashMap<>(); attributes.put("gradle-projectdir", "project/dir"); File snippetsDirectory = new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); - assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets")); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets").getAbsoluteFile()); } @Test @@ -78,7 +79,8 @@ public void gradleProjectsUseBuildGeneratedSnippetsBeneathGradleProjectdirWhenBo attributes.put("gradle-projectdir", "project/dir"); attributes.put("projectdir", "fallback/dir"); File snippetsDirectory = new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); - assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets")); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets").getAbsoluteFile()); } @Test @@ -86,7 +88,8 @@ public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectdirWhenGradlePr Map attributes = new HashMap<>(); attributes.put("projectdir", "project/dir"); File snippetsDirectory = new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); - assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets")); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets").getAbsoluteFile()); } @Test From 5419cd202f2dada11ab7f36695a0c7f25609e03f Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 7 Dec 2024 22:33:00 +0900 Subject: [PATCH 31/60] Remove DefaultAttributesPreprocessor See gh-951 --- .../DefaultAttributesPreprocessor.java | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java deleted file mode 100644 index 1d1be53b9..000000000 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.asciidoctor; - -import org.asciidoctor.ast.Document; -import org.asciidoctor.extension.Preprocessor; -import org.asciidoctor.extension.PreprocessorReader; - -/** - * {@link Preprocessor} that sets defaults for REST Docs-related {@link Document} - * attributes. - * - * @author Andy Wilkinson - */ -final class DefaultAttributesPreprocessor extends Preprocessor { - - private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); - - @Override - public void process(Document document, PreprocessorReader reader) { - document.setAttribute("snippets", this.snippetsDirectoryResolver.getSnippetsDirectory(document.getAttributes()), - false); - } - -} From cc13994ccfed272b272a2f7504c040888d4ddfea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 15 Jan 2025 09:18:16 +0000 Subject: [PATCH 32/60] Switch from CLA to DCO --- .github/dco.yml | 2 ++ CONTRIBUTING.md | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 .github/dco.yml diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 000000000..0c4b142e9 --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fa8a9075..24e7b4254 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,11 +8,10 @@ If you would like to contribute something, or simply want to work with the code, This project adheres to the Contributor Covenant [code of conduct][1]. By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. -## Sign the contributor license agreement - -Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's license agreement (CLA)][2]. -Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. +## Include a Signed-off-by Trailer +All commits must include a _Signed-off-by_ trailer at the end of each commit message to indicate that the contributor agrees to the [Developer Certificate of Origin (DCO)](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin). +For additional details, please refer to the ["Hello DCO, Goodbye CLA: Simplifying Contributions to Spring"](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring) blog post. ## Code conventions and housekeeping From 7b7a1f858f6911d9833de4dd22d5d01136414ff9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Jan 2025 13:41:06 +0000 Subject: [PATCH 33/60] Polish README --- README.md | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index da2a40449..e3dec714d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring REST Docs [![Build status][1]][2] [![Revved up by Develocity][24]][25] +# Spring REST Docs [![Build status][1]][2] [![Revved up by Develocity][23]][24] ## Overview @@ -29,23 +29,22 @@ There are several that you can contribute to Spring REST Docs: - Open a [pull request][12]. Please see the [contributor guidelines][13] for details - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag - - Chat with fellow users [on Gitter][16] ## Third-party extensions | Name | Description | | ---- | ----------- | -| [restdocs-wiremock][17] | Auto-generate WireMock stubs as part of documenting your RESTful API | -| [restdocsext-jersey][18] | Enables Spring REST Docs to be used with [Jersey's test framework][19] | -| [spring-auto-restdocs][20] | Uses introspection and Javadoc to automatically document request and response parameters | -| [restdocs-api-spec][21] | A Spring REST Docs extension that adds API specification support. It currently supports [OpenAPI 2][22] and [OpenAPI 3][23] | +| [restdocs-wiremock][16] | Auto-generate WireMock stubs as part of documenting your RESTful API | +| [restdocsext-jersey][17] | Enables Spring REST Docs to be used with [Jersey's test framework][18] | +| [spring-auto-restdocs][19] | Uses introspection and Javadoc to automatically document request and response parameters | +| [restdocs-api-spec][20] | A Spring REST Docs extension that adds API specification support. It currently supports [OpenAPI 2][21] and [OpenAPI 3][22] | ## Licence Spring REST Docs is open source software released under the [Apache 2.0 license][14]. -[1]: https://ci.spring.io/api/v1/teams/spring-restdocs/pipelines/spring-restdocs-2.0.x/jobs/build/badge (Build status) -[2]: https://ci.spring.io/teams/spring-restdocs/pipelines/spring-restdocs-2.0.x?groups=build +[1]: https://github.com/spring-projects/spring-restdocs/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=main (Build status) +[2]: https://github.com/spring-projects/spring-restdocs/actions/workflows/build-and-deploy-snapshot.yml [3]: https://asciidoctor.org [4]: https://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/htmlsingle/#spring-mvc-test-framework [5]: https://developer.github.com/v3/ @@ -59,13 +58,12 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [13]: CONTRIBUTING.md [14]: https://www.apache.org/licenses/LICENSE-2.0.html [15]: https://stackoverflow.com/tags/spring-restdocs -[16]: https://gitter.im/spring-projects/spring-restdocs -[17]: https://github.com/ePages-de/restdocs-wiremock -[18]: https://github.com/RESTDocsEXT/restdocsext-jersey -[19]: https://jersey.java.net/documentation/latest/test-framework.html -[20]: https://github.com/ScaCap/spring-auto-restdocs -[21]: https://github.com/ePages-de/restdocs-api-spec -[22]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md -[23]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md -[24]: https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A -[25]: https://ge.spring.io/scans?search.rootProjectNames=spring-restdocs +[16]: https://github.com/ePages-de/restdocs-wiremock +[17]: https://github.com/RESTDocsEXT/restdocsext-jersey +[18]: https://jersey.java.net/documentation/latest/test-framework.html +[19]: https://github.com/ScaCap/spring-auto-restdocs +[20]: https://github.com/ePages-de/restdocs-api-spec +[21]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md +[22]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md +[23]: https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A +[24]: https://ge.spring.io/scans?search.rootProjectNames=spring-restdocs From 5cfd3dad59170510e60b56039548fc2b493cab30 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jan 2025 15:05:19 +0000 Subject: [PATCH 34/60] Prepare 3.0.x branch --- .github/workflows/build-and-deploy-snapshot.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index bbc755b23..39c8755bf 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -2,7 +2,7 @@ name: Build and Deploy Snapshot on: push: branches: - - main + - 3.0.x concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6b216c28..4b966e5d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: branches: - - main + - '3.0.x' concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: From db16f279c268939cdd8e60e685867291cbef15d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jan 2025 15:06:43 +0000 Subject: [PATCH 35/60] Begin work on 4.0 --- .github/workflows/build-and-deploy-snapshot.yml | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index bbc755b23..8850cec14 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -25,7 +25,7 @@ jobs: with: artifact-properties: | /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false - build-name: 'spring-restdocs-3.0.x' + build-name: 'spring-restdocs-4.0.x' folder: 'deployment-repository' password: ${{ secrets.ARTIFACTORY_PASSWORD }} repository: ${{ 'libs-snapshot-local' }} diff --git a/gradle.properties b/gradle.properties index 64a74adbb..17c12f9fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.4-SNAPSHOT +version=4.0.0-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 From c72e32d3cb6f2f6b08101b3f3481588409941e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 13 Jan 2025 13:33:40 +0100 Subject: [PATCH 36/60] Raise the minimum support version of Spring Framework to 7.0 See gh-955 --- gradle.properties | 2 +- spring-restdocs-core/build.gradle | 7 -- .../restdocs/cli/CliOperationRequest.java | 4 +- .../restdocs/cli/CurlRequestSnippet.java | 4 +- .../restdocs/cli/HttpieRequestSnippet.java | 4 +- .../headers/RequestHeadersSnippet.java | 4 +- .../headers/ResponseHeadersSnippet.java | 4 +- .../restdocs/http/HttpRequestSnippet.java | 4 +- .../restdocs/http/HttpResponseSnippet.java | 4 +- ...HeadersModifyingOperationPreprocessor.java | 4 +- .../UriModifyingOperationPreprocessor.java | 4 +- .../RestDocumentationConfigurerTests.java | 6 +- ...rsModifyingOperationPreprocessorTests.java | 83 +++++++++++-------- spring-restdocs-mockmvc/build.gradle | 7 -- .../mockmvc/MockMvcResponseConverter.java | 4 +- .../mockmvc/MockMvcRequestConverterTests.java | 7 +- .../MockMvcResponseConverterTests.java | 8 +- .../RestAssuredRequestConverterTests.java | 20 ++--- spring-restdocs-webtestclient/build.gradle | 7 -- .../WebTestClientResponseConverter.java | 4 +- .../WebTestClientRequestConverterTests.java | 11 +-- .../WebTestClientResponseConverterTests.java | 8 +- 22 files changed, 99 insertions(+), 111 deletions(-) diff --git a/gradle.properties b/gradle.properties index 17c12f9fb..4c5eda762 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ org.gradle.parallel=true javaFormatVersion=0.0.43 jmustacheVersion=1.15 -springFrameworkVersion=6.1.13 +springFrameworkVersion=7.0.0-SNAPSHOT diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index a7cfb3a2e..5face83b4 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -82,10 +82,3 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesApiElem components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } - -compatibilityTest { - dependency("Spring Framework") { springFramework -> - springFramework.groupId = "org.springframework" - springFramework.versions = ["6.0.+", "6.2.+"] - } -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index a955fcafb..efc21ba4e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,7 +76,7 @@ public String getContentAsString() { @Override public HttpHeaders getHeaders() { HttpHeaders filteredHeaders = new HttpHeaders(); - for (Entry> header : this.delegate.getHeaders().entrySet()) { + for (Entry> header : this.delegate.getHeaders().headerSet()) { if (allowedHeader(header)) { filteredHeaders.put(header.getKey(), header.getValue()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 358db25ad..469973aee 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -132,7 +132,7 @@ private void writeHttpMethod(OperationRequest request, StringBuilder builder) { } private void writeHeaders(CliOperationRequest request, List lines) { - for (Entry> entry : request.getHeaders().entrySet()) { + for (Entry> entry : request.getHeaders().headerSet()) { for (String header : entry.getValue()) { if (StringUtils.hasText(request.getContentAsString()) && HttpHeaders.CONTENT_TYPE.equals(entry.getKey()) && MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 18b337797..8b92fa0bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,7 +160,7 @@ private void writeFormDataIfNecessary(OperationRequest request, List lin private void writeHeaders(OperationRequest request, List lines) { HttpHeaders headers = request.getHeaders(); - for (Entry> entry : headers.entrySet()) { + for (Entry> entry : headers.headerSet()) { if (entry.getKey().equals(HttpHeaders.CONTENT_TYPE) && headers.getContentType().isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) { continue; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index e64e7c08b..79d587928 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ protected RequestHeadersSnippet(List descriptors, Map extractActualHeaders(Operation operation) { - return operation.getRequest().getHeaders().keySet(); + return operation.getRequest().getHeaders().headerNames(); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index e1acb6812..ea9687e52 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ protected ResponseHeadersSnippet(List descriptors, Map extractActualHeaders(Operation operation) { - return operation.getResponse().getHeaders().keySet(); + return operation.getResponse().getHeaders().headerNames(); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 403b83bec..87087b315 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,7 +91,7 @@ private boolean includeParametersInUri(OperationRequest request) { private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); - for (Entry> header : request.getHeaders().entrySet()) { + for (Entry> header : request.getHeaders().headerSet()) { for (String value : header.getValue()) { if (HttpHeaders.CONTENT_TYPE.equals(header.getKey()) && !request.getParts().isEmpty()) { headers.add(header(header.getKey(), String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index ec56d3977..919d312f9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ private String responseBody(OperationResponse response) { private List> headers(OperationResponse response) { List> headers = new ArrayList<>(); - for (Entry> header : response.getHeaders().entrySet()) { + for (Entry> header : response.getHeaders().headerSet()) { List values = header.getValue(); for (String value : values) { headers.add(header(header.getKey(), value)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java index 9f0d43724..001f7789d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -210,7 +210,7 @@ private RemoveHeadersByNamePatternModification(Pattern namePattern) { @Override public void applyTo(HttpHeaders headers) { - headers.keySet().removeIf((name) -> this.namePattern.matcher(name).matches()); + headers.headerNames().removeIf((name) -> this.namePattern.matcher(name).matches()); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java index b36d97150..6e15b74b9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,7 +147,7 @@ public OperationResponse preprocess(OperationResponse response) { private HttpHeaders modify(HttpHeaders headers) { HttpHeaders modified = new HttpHeaders(); - for (Entry> header : headers.entrySet()) { + for (Entry> header : headers.headerSet()) { for (String value : header.getValue()) { modified.add(header.getKey(), this.contentModifier.modify(value)); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 67f590544..07fae9e5a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -210,7 +210,7 @@ public void customDefaultOperationRequestPreprocessor() { headers.add("Foo", "value"); OperationRequest request = new OperationRequestFactory().create(URI.create("http://localhost:8080/"), HttpMethod.GET, null, headers, null, Collections.emptyList()); - assertThat(preprocessor.preprocess(request).getHeaders()).doesNotContainKey("Foo"); + assertThat(preprocessor.preprocess(request).getHeaders().headerNames()).doesNotContain("Foo"); } @Test @@ -224,7 +224,7 @@ public void customDefaultOperationResponsePreprocessor() { HttpHeaders headers = new HttpHeaders(); headers.add("Foo", "value"); OperationResponse response = new OperationResponseFactory().create(HttpStatus.OK, headers, null); - assertThat(preprocessor.preprocess(response).getHeaders()).doesNotContainKey("Foo"); + assertThat(preprocessor.preprocess(response).getHeaders().headerNames()).doesNotContain("Foo"); } private RestDocumentationContext createContext() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java index 9091da49f..5d3e77ac3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link HeadersModifyingOperationPreprocessor}. @@ -47,60 +48,66 @@ public class HeadersModifyingOperationPreprocessorTests { @Test public void addNewHeader() { this.preprocessor.add("a", "alpha"); - assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).containsEntry("a", - Arrays.asList("alpha")); - assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).containsEntry("a", - Arrays.asList("alpha")); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().get("a")) + .isEqualTo(Arrays.asList("alpha")); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().get("a")) + .isEqualTo(Arrays.asList("alpha")); } @Test public void addValueToExistingHeader() { this.preprocessor.add("a", "alpha"); - assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) - .containsEntry("a", Arrays.asList("apple", "alpha")); - assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) - .containsEntry("a", Arrays.asList("apple", "alpha")); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("apple", "alpha"))); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("apple", "alpha"))); } @Test public void setNewHeader() { this.preprocessor.set("a", "alpha", "avocado"); - assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).containsEntry("a", - Arrays.asList("alpha", "avocado")); - assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).containsEntry("a", - Arrays.asList("alpha", "avocado")); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerSet()) + .contains(entry("a", Arrays.asList("alpha", "avocado"))); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerSet()) + .contains(entry("a", Arrays.asList("alpha", "avocado"))); } @Test public void setExistingHeader() { this.preprocessor.set("a", "alpha", "avocado"); - assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) - .containsEntry("a", Arrays.asList("alpha", "avocado")); - assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) - .containsEntry("a", Arrays.asList("alpha", "avocado")); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("alpha", "avocado"))); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("alpha", "avocado"))); } @Test public void removeNonExistentHeader() { this.preprocessor.remove("a"); - assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).doesNotContainKey("a"); - assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerNames()).doesNotContain("a"); } @Test public void removeHeader() { this.preprocessor.remove("a"); - assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) - .doesNotContainKey("a"); - assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) - .doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); } @Test public void removeHeaderValueForNonExistentHeader() { this.preprocessor.remove("a", "apple"); - assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).doesNotContainKey("a"); - assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerNames()).doesNotContain("a"); } @Test @@ -108,20 +115,24 @@ public void removeHeaderValueWithMultipleValues() { this.preprocessor.remove("a", "apple"); assertThat( this.preprocessor.preprocess(createRequest((headers) -> headers.addAll("a", List.of("apple", "alpha")))) - .getHeaders()) - .containsEntry("a", Arrays.asList("alpha")); + .getHeaders() + .headerSet()) + .contains(entry("a", Arrays.asList("alpha"))); assertThat(this.preprocessor .preprocess(createResponse((headers) -> headers.addAll("a", List.of("apple", "alpha")))) - .getHeaders()).containsEntry("a", Arrays.asList("alpha")); + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("alpha"))); } @Test public void removeHeaderValueWithSingleValueRemovesEntryEntirely() { this.preprocessor.remove("a", "apple"); - assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) - .doesNotContainKey("a"); - assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) - .doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); } @Test @@ -133,10 +144,10 @@ public void removeHeadersByNamePattern() { headers.add("bravo", "bravo"); }; this.preprocessor.removeMatching("^a.*"); - assertThat(this.preprocessor.preprocess(createRequest(headersCustomizer)).getHeaders()).containsOnlyKeys("Host", - "bravo"); - assertThat(this.preprocessor.preprocess(createResponse(headersCustomizer)).getHeaders()) - .containsOnlyKeys("bravo"); + assertThat(this.preprocessor.preprocess(createRequest(headersCustomizer)).getHeaders().headerNames()) + .containsOnly("Host", "bravo"); + assertThat(this.preprocessor.preprocess(createResponse(headersCustomizer)).getHeaders().headerNames()) + .containsOnly("bravo"); } private OperationRequest createRequest() { diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index 8a487c2ba..1f4ed7d78 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -22,10 +22,3 @@ dependencies { testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.mockito:mockito-core") } - -compatibilityTest { - dependency("Spring Framework") { springFramework -> - springFramework.groupId = "org.springframework" - springFramework.versions = ["6.0.+", "6.2.+"] - } -} \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java index e3caca603..701d86998 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ private HttpHeaders extractHeaders(MockHttpServletResponse response) { } } - if (response.getCookies() != null && !headers.containsKey(HttpHeaders.SET_COOKIE)) { + if (response.getCookies() != null && !headers.containsHeader(HttpHeaders.SET_COOKIE)) { for (Cookie cookie : response.getCookies()) { headers.add(HttpHeaders.SET_COOKIE, generateSetCookieHeader(cookie)); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 397b7ed71..5122510a2 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -78,8 +79,8 @@ public void requestWithHeaders() { MockMvcRequestBuilders.get("/foo").header("a", "alpha", "apple").header("b", "bravo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); - assertThat(request.getHeaders()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getHeaders()).containsEntry("b", Arrays.asList("bravo")); + assertThat(request.getHeaders().headerSet()).contains(entry("a", Arrays.asList("alpha", "apple")), + entry("b", Arrays.asList("bravo"))); } @Test diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java index 1d208b439..300649a2f 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.restdocs.operation.ResponseCookie; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link MockMvcResponseConverter}. @@ -57,9 +58,8 @@ public void responseWithCookie() { cookie.setHttpOnly(true); response.addCookie(cookie); OperationResponse operationResponse = this.factory.convert(response); - assertThat(operationResponse.getHeaders()).hasSize(1); - assertThat(operationResponse.getHeaders()).containsEntry(HttpHeaders.SET_COOKIE, - Collections.singletonList("name=value; Domain=localhost; HttpOnly")); + assertThat(operationResponse.getHeaders().headerSet()).containsOnly( + entry(HttpHeaders.SET_COOKIE, Collections.singletonList("name=value; Domain=localhost; HttpOnly"))); assertThat(operationResponse.getCookies()).hasSize(1); assertThat(operationResponse.getCookies()).first().extracting(ResponseCookie::getName).isEqualTo("name"); assertThat(operationResponse.getCookies()).first().extracting(ResponseCookie::getValue).isEqualTo("value"); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 0307e20b2..e7abf05d3 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link RestAssuredRequestConverter}. @@ -98,10 +99,8 @@ public void headers() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).header("Foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getHeaders()).hasSize(2); - assertThat(request.getHeaders()).containsEntry("Foo", Collections.singletonList("bar")); - assertThat(request.getHeaders()).containsEntry("Host", - Collections.singletonList("localhost:" + tomcat.getPort())); + assertThat(request.getHeaders().headerSet()).containsOnly(entry("Foo", Collections.singletonList("bar")), + entry("Host", Collections.singletonList("localhost:" + tomcat.getPort()))); } @Test @@ -112,11 +111,9 @@ public void headersWithCustomAccept() { .accept("application/json"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getHeaders()).hasSize(3); - assertThat(request.getHeaders()).containsEntry("Foo", Collections.singletonList("bar")); - assertThat(request.getHeaders()).containsEntry("Accept", Collections.singletonList("application/json")); - assertThat(request.getHeaders()).containsEntry("Host", - Collections.singletonList("localhost:" + tomcat.getPort())); + assertThat(request.getHeaders().headerSet()).containsOnly(entry("Foo", Collections.singletonList("bar")), + entry("Accept", Collections.singletonList("application/json")), + entry("Host", Collections.singletonList("localhost:" + tomcat.getPort()))); } @Test @@ -153,8 +150,7 @@ public void multipart() { assertThat(parts).extracting("name").containsExactly("a", "b"); assertThat(parts).extracting("submittedFileName").containsExactly("a.txt", "file"); assertThat(parts).extracting("contentAsString").containsExactly("alpha", "{\"foo\":\"bar\"}"); - assertThat(parts).extracting("headers") - .extracting(HttpHeaders.CONTENT_TYPE) + assertThat(parts).map((part) -> part.getHeaders().get(HttpHeaders.CONTENT_TYPE)) .containsExactly(Collections.singletonList(MediaType.TEXT_PLAIN_VALUE), Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); } diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index 2e26c4031..843d8995c 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -21,10 +21,3 @@ dependencies { testRuntimeOnly("org.springframework:spring-context") } - -compatibilityTest { - dependency("Spring Framework") { springFramework -> - springFramework.groupId = "org.springframework" - springFramework.versions = ["6.0.+", "6.2.+"] - } -} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java index 4aef283ac..5a8416bd4 100644 --- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public OperationResponse convert(ExchangeResult result) { private HttpHeaders extractHeaders(ExchangeResult result) { HttpHeaders headers = result.getResponseHeaders(); - if (result.getResponseCookies().isEmpty() || headers.containsKey(HttpHeaders.SET_COOKIE)) { + if (result.getResponseCookies().isEmpty() || headers.containsHeader(HttpHeaders.SET_COOKIE)) { return headers; } result.getResponseCookies() diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java index edc578ba4..5c8782e3d 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.POST; @@ -99,8 +100,8 @@ public void requestWithHeaders() { OperationRequest request = this.converter.convert(result); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); - assertThat(request.getHeaders()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getHeaders()).containsEntry("b", Arrays.asList("bravo")); + assertThat(request.getHeaders().headerSet()).contains(entry("a", Arrays.asList("alpha", "apple")), + entry("b", Arrays.asList("bravo"))); } @Test @@ -266,7 +267,7 @@ public void multipartUpload() { OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName()).isEqualTo("file"); assertThat(part.getSubmittedFileName()).isNull(); - assertThat(part.getHeaders()).hasSize(2); + assertThat(part.getHeaders().size()).isEqualTo(2); assertThat(part.getHeaders().getContentLength()).isEqualTo(4L); assertThat(part.getHeaders().getContentDisposition().getName()).isEqualTo("file"); assertThat(part.getContent()).containsExactly(1, 2, 3, 4); @@ -303,7 +304,7 @@ public String getFilename() { OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName()).isEqualTo("file"); assertThat(part.getSubmittedFileName()).isEqualTo("image.png"); - assertThat(part.getHeaders()).hasSize(3); + assertThat(part.getHeaders().size()).isEqualTo(3); assertThat(part.getHeaders().getContentLength()).isEqualTo(4); ContentDisposition contentDisposition = part.getHeaders().getContentDisposition(); assertThat(contentDisposition.getName()).isEqualTo("file"); diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java index e397975d3..474c6feba 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; /** @@ -83,9 +84,8 @@ public void responseWithCookie() { .expectBody() .returnResult(); OperationResponse response = this.converter.convert(result); - assertThat(response.getHeaders()).hasSize(1); - assertThat(response.getHeaders()).containsEntry(HttpHeaders.SET_COOKIE, - Collections.singletonList("name=value; Domain=localhost; HttpOnly")); + assertThat(response.getHeaders().headerSet()).containsOnly( + entry(HttpHeaders.SET_COOKIE, Collections.singletonList("name=value; Domain=localhost; HttpOnly"))); assertThat(response.getCookies()).hasSize(1); assertThat(response.getCookies()).first().extracting(ResponseCookie::getName).isEqualTo("name"); assertThat(response.getCookies()).first().extracting(ResponseCookie::getValue).isEqualTo("value"); From 34c93666e1b44e4c5554ef4c65d6e09b8db0fcba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jan 2025 15:18:02 +0000 Subject: [PATCH 37/60] Polish "Raise the minimum support version of Spring Framework to 7.0" See gh-955 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 2 -- docs/src/docs/asciidoc/getting-started.adoc | 2 +- gradle.properties | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index ce3dc5909..55fa120e4 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -802,8 +802,6 @@ Uses the static `parameterWithName` method on `org.springframework.restdocs.requ The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. -TIP: If you use MockMvc with Spring Framework 6.1 or earlier, to make the path parameters available for documentation, you must build the request by using one of the methods on `RestDocumentationRequestBuilders` rather than `MockMvcRequestBuilders`. - When documenting path parameters, the test fails if an undocumented path parameter is used in the request. Similarly, the test also fails if a documented path parameter is not found in the request and the path parameter has not been marked as optional. diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 81e24381b..3e4e2c4f4 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -18,7 +18,7 @@ If you want to jump straight in, a number of https://github.com/spring-projects/ Spring REST Docs has the following minimum requirements: * Java 17 -* Spring Framework 6 +* Spring Framework 7 Additionally, the `spring-restdocs-restassured` module requires REST Assured 5.2. diff --git a/gradle.properties b/gradle.properties index 4c5eda762..5cc794136 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ org.gradle.parallel=true javaFormatVersion=0.0.43 jmustacheVersion=1.15 -springFrameworkVersion=7.0.0-SNAPSHOT +springFrameworkVersion=7.0.0-M1 From 5e6530a601485348267776ba20272423208601a6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jan 2025 16:06:01 +0000 Subject: [PATCH 38/60] Raise minimum versions of validation dependencies Closes gh-956 --- build.gradle | 4 +- .../docs/asciidoc/documenting-your-api.adoc | 2 +- ...ceBundleConstraintDescriptionResolver.java | 10 ++-- .../DefaultConstraintDescriptions.properties | 3 -- ...dleConstraintDescriptionResolverTests.java | 54 ++++++++++--------- spring-restdocs-platform/build.gradle | 4 +- 6 files changed, 39 insertions(+), 38 deletions(-) diff --git a/build.gradle b/build.gradle index 42e043b3e..b58d80134 100644 --- a/build.gradle +++ b/build.gradle @@ -35,8 +35,8 @@ nohttp { ext { javadocLinks = [ "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", - "https://docs.jboss.org/hibernate/validator/7.0/api/", - "https://jakarta.ee/specifications/bean-validation/3.0/apidocs/" + "https://docs.jboss.org/hibernate/validator/9.0/api/", + "https://jakarta.ee/specifications/bean-validation/3.1/apidocs/" ] as String[] } diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 55fa120e4..cb9ce82a3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -1146,7 +1146,7 @@ To take complete control of constraint resolution, you can use your own implemen [[documenting-your-api-constraints-describing]] ==== Describing Constraints -Default descriptions are provided for all of Bean Validation 3.0's constraints: +Default descriptions are provided for all of Bean Validation 3.1's constraints: * `AssertFalse` * `AssertTrue` diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 1c409a6f8..59788d061 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +64,7 @@ * {@code jakarta.validation.constraints.NotNull} is * {@code jakarta.validation.constraints.NotNull.description}. *

    - * Default descriptions are provided for Bean Validation 2.0's constraints: + * Default descriptions are provided for all of Bean Validation 3.1's constraints: * *

      *
    • {@link AssertFalse} @@ -92,20 +92,18 @@ *
    * *

    - * Default descriptions are also provided for Hibernate Validator's constraints: + * Default descriptions are also provided for the following Hibernate Validator + * constraints: * *

      *
    • {@link CodePointLength} *
    • {@link CreditCardNumber} *
    • {@link Currency} *
    • {@link EAN} - *
    • {@link org.hibernate.validator.constraints.Email} *
    • {@link Length} *
    • {@link LuhnCheck} *
    • {@link Mod10Check} *
    • {@link Mod11Check} - *
    • {@link org.hibernate.validator.constraints.NotBlank} - *
    • {@link org.hibernate.validator.constraints.NotEmpty} *
    • {@link Range} *
    • {@link URL} *
    diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index 248cb2382..699b900e2 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -24,12 +24,9 @@ org.hibernate.validator.constraints.CodePointLength.description=Code point lengt org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number org.hibernate.validator.constraints.Currency.description=Must be in an accepted currency unit (${value}) org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number -org.hibernate.validator.constraints.Email.description=Must be a well-formed email address org.hibernate.validator.constraints.Length.description=Length must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.LuhnCheck.description=Must pass the Luhn Modulo 10 checksum algorithm org.hibernate.validator.constraints.Mod10Check.description=Must pass the Mod10 checksum algorithm org.hibernate.validator.constraints.Mod11Check.description=Must pass the Mod11 checksum algorithm -org.hibernate.validator.constraints.NotBlank.description=Must not be blank -org.hibernate.validator.constraints.NotEmpty.description=Must not be empty org.hibernate.validator.constraints.Range.description=Must be at least ${min} and at most ${max} org.hibernate.validator.constraints.URL.description=Must be a well-formed URL \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 4598cd1ab..f4b5e2af8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,11 @@ import java.net.URL; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.ListResourceBundle; import java.util.ResourceBundle; +import java.util.Set; import javax.money.MonetaryAmount; @@ -61,7 +63,11 @@ import org.junit.Test; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -180,12 +186,6 @@ public void defaultMessageEmail() { assertThat(constraintDescriptionForField("email")).isEqualTo("Must be a well-formed email address"); } - @Test - public void defaultMessageEmailHibernateValidator() { - assertThat(constraintDescriptionForField("emailHibernateValidator")) - .isEqualTo("Must be a well-formed email address"); - } - @Test public void defaultMessageLength() { assertThat(constraintDescriptionForField("length")).isEqualTo("Length must be between 2 and 10 inclusive"); @@ -222,11 +222,6 @@ public void defaultMessageNotBlank() { assertThat(constraintDescriptionForField("notBlank")).isEqualTo("Must not be blank"); } - @Test - public void defaultMessageNotBlankHibernateValidator() { - assertThat(constraintDescriptionForField("notBlankHibernateValidator")).isEqualTo("Must not be blank"); - } - @Test public void defaultMessageNotEmpty() { assertThat(constraintDescriptionForField("notEmpty")).isEqualTo("Must not be empty"); @@ -298,6 +293,29 @@ protected Object[][] getContents() { assertThat(description).isEqualTo("Not null"); } + @Test + public void allBeanValidationConstraintsAreTested() throws Exception { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("jakarta/validation/constraints/*.class"); + Set> beanValidationConstraints = new HashSet<>(); + for (Resource resource : resources) { + String className = ClassUtils.convertResourcePathToClassName(((ClassPathResource) resource).getPath()); + if (className.endsWith(".class")) { + className = className.substring(0, className.length() - 6); + } + Class type = Class.forName(className); + if (type.isAnnotation() && type.isAnnotationPresent(jakarta.validation.Constraint.class)) { + beanValidationConstraints.add(type); + } + } + ReflectionUtils.doWithFields(Constrained.class, (field) -> { + for (Annotation annotation : field.getAnnotations()) { + beanValidationConstraints.remove(annotation.annotationType()); + } + }); + assertThat(beanValidationConstraints).isEmpty(); + } + private String constraintDescriptionForField(String name) { return this.resolver.resolveDescription(getConstraintFromField(name)); } @@ -372,10 +390,6 @@ private static final class Constrained { @Email private String email; - @SuppressWarnings("deprecation") - @org.hibernate.validator.constraints.Email - private String emailHibernateValidator; - @Length(min = 2, max = 10) private String length; @@ -397,17 +411,9 @@ private static final class Constrained { @NotBlank private String notBlank; - @SuppressWarnings("deprecation") - @org.hibernate.validator.constraints.NotBlank - private String notBlankHibernateValidator; - @NotEmpty private String notEmpty; - @SuppressWarnings("deprecation") - @org.hibernate.validator.constraints.NotEmpty - private String notEmptyHibernateValidator; - @Positive private int positive; diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 973cf7323..4b2637cf6 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -10,7 +10,7 @@ dependencies { constraints { api("com.samskivert:jmustache:$jmustacheVersion") api("jakarta.servlet:jakarta.servlet-api:6.0.0") - api("jakarta.validation:jakarta.validation-api:3.0.0") + api("jakarta.validation:jakarta.validation-api:3.1.0") api("junit:junit:4.13.1") api("org.apache.pdfbox:pdfbox:2.0.27") api("org.apache.tomcat.embed:tomcat-embed-core:10.1.1") @@ -20,7 +20,7 @@ dependencies { api("org.assertj:assertj-core:3.23.1") api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") - api("org.hibernate.validator:hibernate-validator:8.0.0.Final") + api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") api("org.javamoney:moneta:1.4.2") api("org.junit.jupiter:junit-jupiter-api:5.0.0") } From 85ebc4eb862b7a26be48a4ea0731950a21cd7c22 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jan 2025 16:06:12 +0000 Subject: [PATCH 39/60] Align Servlet-related dependencies with Framework 7 baseline See gh-955 --- spring-restdocs-platform/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 4b2637cf6..c3d3dcbfd 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -9,12 +9,12 @@ javaPlatform { dependencies { constraints { api("com.samskivert:jmustache:$jmustacheVersion") - api("jakarta.servlet:jakarta.servlet-api:6.0.0") + api("jakarta.servlet:jakarta.servlet-api:6.1.0") api("jakarta.validation:jakarta.validation-api:3.1.0") api("junit:junit:4.13.1") api("org.apache.pdfbox:pdfbox:2.0.27") - api("org.apache.tomcat.embed:tomcat-embed-core:10.1.1") - api("org.apache.tomcat.embed:tomcat-embed-el:10.1.1") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.2") + api("org.apache.tomcat.embed:tomcat-embed-el:11.0.2") api("org.asciidoctor:asciidoctorj:2.5.7") api("org.asciidoctor:asciidoctorj-pdf:2.3.3") api("org.assertj:assertj-core:3.23.1") From 0b103fdc0e747cc935f1e7fecf6f19bfadda2ef2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Jan 2025 16:28:44 +0000 Subject: [PATCH 40/60] Raise the minimum supported version of AsciidoctorJ to 3.0 Closes gh-957 --- docs/src/docs/asciidoc/getting-started.adoc | 2 + .../asciidoc/working-with-asciidoctor.adoc | 1 + spring-restdocs-asciidoctor/build.gradle | 9 --- .../DefaultAttributesPreprocessor.java | 41 ++++++++++++ .../RestDocsExtensionRegistry.java | 6 +- .../SnippetsDirectoryResolver.java | 11 +-- .../extensions/default_attributes.rb | 14 ---- .../DefaultAttributesPreprocessorTests.java | 67 +++++++++++++++++++ spring-restdocs-platform/build.gradle | 4 +- 9 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java delete mode 100644 spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb create mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 3e4e2c4f4..be21ed4ff 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -78,6 +78,7 @@ If you want to use `WebTestClient` or REST Assured rather than MockMvc, add a de <4> Add `spring-restdocs-asciidoctor` as a dependency of the Asciidoctor plugin. This will automatically configure the `snippets` attribute for use in your `.adoc` files to point to `target/generated-snippets`. It will also allow you to use the `operation` block macro. +It requires AsciidoctorJ 3.0. [source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle @@ -114,6 +115,7 @@ It will also allow you to use the `operation` block macro. <3> Add a dependency on `spring-restdocs-asciidoctor` in the `asciidoctorExt` configuration. This will automatically configure the `snippets` attribute for use in your `.adoc` files to point to `build/generated-snippets`. It will also allow you to use the `operation` block macro. +It requires AsciidoctorJ 3.0. <4> Add a dependency on `spring-restdocs-mockmvc` in the `testImplementation` configuration. If you want to use `WebTestClient` or REST Assured rather than MockMvc, add a dependency on `spring-restdocs-webtestclient` or `spring-restdocs-restassured` respectively instead. <5> Configure a `snippetsDir` property that defines the output location for generated snippets. diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index 068590384..698d5e460 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -28,6 +28,7 @@ This section covers how to include Asciidoc snippets. You can use a macro named `operation` to import all or some of the snippets that have been generated for a specific operation. It is made available by including `spring-restdocs-asciidoctor` in your project's <>. +`spring-restdocs-asciidoctor` requires AsciidoctorJ 3.0. The target of the macro is the name of the operation. In its simplest form, you can use the macro to include all of the snippets for an operation, as shown in the following example: diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index 717744ea5..ceb5202f0 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -1,7 +1,6 @@ plugins { id "java-library" id "maven-publish" - id "io.spring.compatibility-test" version "0.0.3" } description = "Spring REST Docs Asciidoctor Extension" @@ -20,11 +19,3 @@ dependencies { testRuntimeOnly("org.asciidoctor:asciidoctorj-pdf") } - -compatibilityTest { - dependency("AsciidoctorJ") { asciidoctorj -> - asciidoctorj.groupId = "org.asciidoctor" - asciidoctorj.artifactId = "asciidoctorj" - asciidoctorj.versions = ["3.0.0"] - } -} \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java new file mode 100644 index 000000000..7fa6ce5df --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.asciidoctor; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.Preprocessor; +import org.asciidoctor.extension.PreprocessorReader; +import org.asciidoctor.extension.Reader; + +/** + * {@link Preprocessor} that sets defaults for REST Docs-related {@link Document} + * attributes. + * + * @author Andy Wilkinson + */ +final class DefaultAttributesPreprocessor extends Preprocessor { + + private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); + + @Override + public Reader process(Document document, PreprocessorReader reader) { + document.setAttribute("snippets", this.snippetsDirectoryResolver.getSnippetsDirectory(document.getAttributes()), + false); + return reader; + } + +} diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java index 5432b23c0..0a58a7cf9 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,7 @@ public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - asciidoctor.rubyExtensionRegistry() - .loadClass(RestDocsExtensionRegistry.class.getResourceAsStream("/extensions/default_attributes.rb")) - .preprocessor("DefaultAttributes"); + asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); asciidoctor.rubyExtensionRegistry() .loadClass(RestDocsExtensionRegistry.class.getResourceAsStream("/extensions/operation_block_macro.rb")) .blockMacro("operation", "OperationBlockMacro"); diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java index 521c08329..dfbb957a8 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,14 +29,9 @@ * * @author Andy Wilkinson */ -public class SnippetsDirectoryResolver { +class SnippetsDirectoryResolver { - /** - * Returns the snippets directory derived from the given {@code attributes}. - * @param attributes the attributes - * @return the snippets directory - */ - public File getSnippetsDirectory(Map attributes) { + File getSnippetsDirectory(Map attributes) { if (System.getProperty("maven.home") != null) { return getMavenSnippetsDirectory(attributes); } diff --git a/spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb b/spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb deleted file mode 100644 index a4060d5b3..000000000 --- a/spring-restdocs-asciidoctor/src/main/resources/extensions/default_attributes.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'asciidoctor/extensions' -require 'java' - -class DefaultAttributes < Asciidoctor::Extensions::Preprocessor - - def process(document, reader) - resolver = org.springframework.restdocs.asciidoctor.SnippetsDirectoryResolver.new() - attributes = document.attributes - attributes["snippets"] = resolver.getSnippetsDirectory(attributes) unless attributes.has_key?("snippets") - false - end - -end - \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java new file mode 100644 index 000000000..78ba919cf --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Attributes; +import org.asciidoctor.Options; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultAttributesPreprocessor}. + * + * @author Andy Wilkinson + */ +public class DefaultAttributesPreprocessorTests { + + @Test + public void snippetsAttributeIsSet() { + String converted = createAsciidoctor().convert("{snippets}", createOptions("projectdir=../../..")); + assertThat(converted).contains("build" + File.separatorChar + "generated-snippets"); + } + + @Test + public void snippetsAttributeFromConvertArgumentIsNotOverridden() { + String converted = createAsciidoctor().convert("{snippets}", + createOptions("snippets=custom projectdir=../../..")); + assertThat(converted).contains("custom"); + } + + @Test + public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { + String converted = createAsciidoctor().convert(":snippets: custom\n{snippets}", + createOptions("projectdir=../../..")); + assertThat(converted).contains("custom"); + } + + private Options createOptions(String attributes) { + Options options = Options.builder().build(); + options.setAttributes(Attributes.builder().arguments(attributes).build()); + return options; + } + + private Asciidoctor createAsciidoctor() { + Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); + return asciidoctor; + } + +} diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index c3d3dcbfd..28f64f144 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -15,8 +15,8 @@ dependencies { api("org.apache.pdfbox:pdfbox:2.0.27") api("org.apache.tomcat.embed:tomcat-embed-core:11.0.2") api("org.apache.tomcat.embed:tomcat-embed-el:11.0.2") - api("org.asciidoctor:asciidoctorj:2.5.7") - api("org.asciidoctor:asciidoctorj-pdf:2.3.3") + api("org.asciidoctor:asciidoctorj:3.0.0") + api("org.asciidoctor:asciidoctorj-pdf:2.3.19") api("org.assertj:assertj-core:3.23.1") api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") From 8fd536c637e0c783a7411b545a766526ca24a7e2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 3 Feb 2025 10:00:28 +0000 Subject: [PATCH 41/60] Remove commons-logging excludes The spring-jcl module has been removed in Spring Framework 7 in favor of Commons Logging 1.3. This commit removes the excludes for commons-logging:commons-logging that werew required to ensure that spring-jcl was used instead but that are no longer needed. See gh-955 --- spring-restdocs-asciidoctor/build.gradle | 4 +--- spring-restdocs-restassured/build.gradle | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index ceb5202f0..149c81e2b 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -11,9 +11,7 @@ dependencies { internal(platform(project(":spring-restdocs-platform"))) testImplementation("junit:junit") - testImplementation("org.apache.pdfbox:pdfbox") { - exclude group: "commons-logging", module: "commons-logging" - } + testImplementation("org.apache.pdfbox:pdfbox") testImplementation("org.assertj:assertj-core") testImplementation("org.springframework:spring-core") diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 812ed5d1d..5c2dc4c91 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -8,9 +8,7 @@ description = "Spring REST Docs REST Assured" dependencies { api(project(":spring-restdocs-core")) - api("io.rest-assured:rest-assured") { - exclude group: "commons-logging", module: "commons-logging" - } + api("io.rest-assured:rest-assured") implementation("org.springframework:spring-web") internal(platform(project(":spring-restdocs-platform"))) From ec951e123c182c70fe19a5a2f04049bbc1f4573d Mon Sep 17 00:00:00 2001 From: Seonghun Jeong Date: Mon, 24 Mar 2025 14:53:59 +0900 Subject: [PATCH 42/60] Add Spring REST Docs icon for IntelliJ IDEA - Add an SVG project icon converted from the official PNG logo (source: https://spring.io/img/projects/spring-restdocs.png) - Update .gitignore to exclude all .idea files except icon.svg Signed-off-by: Seonghun Jeong See gh-961 --- .gitignore | 3 ++- .idea/icon.svg | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .idea/icon.svg diff --git a/.gitignore b/.gitignore index c508c0c30..c01583a59 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ bin build !buildSrc/src/main/groovy/org/springframework/restdocs/build/ target -.idea +.idea/* +!.idea/icon.svg *.iml \ No newline at end of file diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 000000000..5e6592046 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file From 64494bec84122111ec25f947d12ddf65d1fe7b9f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 May 2025 10:07:18 +0100 Subject: [PATCH 43/60] Add Git hooks for forward merges --- git/hooks/forward-merge | 17 ++++++++++++++--- git/hooks/prepare-forward-merge | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge index acbb60082..a042bb460 100755 --- a/git/hooks/forward-merge +++ b/git/hooks/forward-merge @@ -18,7 +18,14 @@ class ForwardMerge end def find_forward_merges(message_file) + $log.debug "Searching for forward merge" + branch=`git rev-parse -q --abbrev-ref HEAD`.strip + $log.debug "Found #{branch} from git rev-parse --abbrev-ref" + if( branch == "docs-build") then + $log.debug "Skipping docs build" + return nil + end rev=`git rev-parse -q --verify MERGE_HEAD`.strip $log.debug "Found #{rev} from git rev-parse" return nil unless rev @@ -65,7 +72,7 @@ def find_milestone(username, password, repository, title) prefix = title.delete_suffix('.x') $log.debug "Finding nearest milestone from candidates starting with #{prefix}" titles = milestones.map { |milestone| milestone['title'] } - titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x')} + titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x') || (title.count('.') > 2)} titles = titles.sort_by { |v| Gem::Version.new(v) } $log.debug "Considering candidates #{titles}" if(titles.empty?) @@ -112,12 +119,16 @@ message_file=ARGV[0] forward_merges = find_forward_merges(message_file) exit 0 unless forward_merges -$log.debug "Loading config from ~/.spring-restdocs/forward_merge.yml" +$log.debug "Loading config from ~/.spring-restdocs/forward-merge.yml" config = YAML.load_file(File.join(Dir.home, '.spring-restdocs', 'forward-merge.yml')) username = config['github']['credentials']['username'] password = config['github']['credentials']['password'] dry_run = config['dry_run'] -repository = 'spring-projects/spring-restdocs' + +gradleProperties = IO.read('gradle.properties') +springBuildType = gradleProperties.match(/^spring\.build-type\s?=\s?(.*)$/) +repository = (springBuildType && springBuildType[1] != 'oss') ? "spring-projects/spring-restdocs-#{springBuildType[1]}" : "spring-projects/spring-restdocs"; +$log.debug "Targeting repository #{repository}" forward_merges.each do |forward_merge| existing_issue = get_issue(username, password, repository, forward_merge.issue) diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge index 2b049790d..fbdb1e194 100755 --- a/git/hooks/prepare-forward-merge +++ b/git/hooks/prepare-forward-merge @@ -4,7 +4,7 @@ require 'net/http' require 'yaml' require 'logger' -$main_branch = "3.0.x" +$main_branch = "4.0.x" $log = Logger.new(STDOUT) $log.level = Logger::WARN From af35dbd874a3459b8c39cafd25fdb48fc6b9f558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Teglhus=20M=C3=B8ller?= Date: Mon, 12 May 2025 16:24:27 +0200 Subject: [PATCH 44/60] Align bootJar example with changes to Asciidoctor's Gradle plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jens Teglhus Møller See gh-962 --- docs/src/docs/asciidoc/getting-started.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 81e24381b..6cd9933bf 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -179,7 +179,7 @@ If you are not using Spring Boot and its plugin management, declare the plugin w ---- bootJar { dependsOn asciidoctor <1> - from ("${asciidoctor.outputDir}/html5") { <2> + from ("${asciidoctor.outputDir}") { <2> into 'static/docs' } } From c1266abe9fc98adf78a02008e6526752dc7ea3a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 11:13:22 +0100 Subject: [PATCH 45/60] Upgrade to Compatibility Test Plugin 0.0.4 --- spring-restdocs-asciidoctor/build.gradle | 2 +- spring-restdocs-core/build.gradle | 2 +- spring-restdocs-mockmvc/build.gradle | 2 +- spring-restdocs-restassured/build.gradle | 2 +- spring-restdocs-webtestclient/build.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index 717744ea5..2ab2e6534 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -1,7 +1,7 @@ plugins { id "java-library" id "maven-publish" - id "io.spring.compatibility-test" version "0.0.3" + id "io.spring.compatibility-test" version "0.0.4" } description = "Spring REST Docs Asciidoctor Extension" diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index a7cfb3a2e..f27643a4e 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3" + id "io.spring.compatibility-test" version "0.0.4" id "java-library" id "java-test-fixtures" id "maven-publish" diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index 8a487c2ba..077a03f23 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3" + id "io.spring.compatibility-test" version "0.0.4" id "java-library" id "maven-publish" id "optional-dependencies" diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 812ed5d1d..55431184d 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3" + id "io.spring.compatibility-test" version "0.0.4" id "java-library" id "maven-publish" } diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index 2e26c4031..1a5f848b3 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -1,5 +1,5 @@ plugins { - id "io.spring.compatibility-test" version "0.0.3" + id "io.spring.compatibility-test" version "0.0.4" id "java-library" id "maven-publish" } From 20cff4d77be4fb6087377489f35d0515168c3b74 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 11:13:35 +0100 Subject: [PATCH 46/60] Upgrade to Gradle 8.14.1 and minimize deprecation warnings --- build.gradle | 6 +++--- docs/build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 9 ++++----- gradlew.bat | 4 ++-- settings.gradle | 2 +- spring-restdocs-core/build.gradle | 2 +- 8 files changed, 13 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 42e043b3e..83c3c29b9 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ allprojects { repositories { mavenCentral() maven { - url "https://repo.spring.io/milestone" + url = "https://repo.spring.io/milestone" content { includeGroup "io.micrometer" includeGroup "io.projectreactor" @@ -18,7 +18,7 @@ allprojects { } } if (version.endsWith('-SNAPSHOT')) { - maven { url "https://repo.spring.io/snapshot" } + maven { url = "https://repo.spring.io/snapshot" } } } } @@ -70,7 +70,7 @@ subprojects { subproject -> test { testLogging { - exceptionFormat "full" + exceptionFormat = "full" } } diff --git a/docs/build.gradle b/docs/build.gradle index 5ee2e4171..8c08f1800 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.asciidoctor.jvm.convert" version "3.3.2" + id "org.asciidoctor.jvm.convert" version "4.0.4" id "java-library" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch delta 34943 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

    Gn4F+B~IivciU9^t0j-Mgao3 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 DDw4oP diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8..002b867c4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6..23d15a936 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a2183..db3a6ac20 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle b/settings.gradle index 81adc416a..33995e502 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,7 @@ pluginManagement { repositories { mavenCentral() gradlePluginPortal() - maven { url 'https://repo.spring.io/snapshot' } + maven { url = 'https://repo.spring.io/snapshot' } } resolutionStrategy { eachPlugin { diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index f27643a4e..c8a193b31 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -19,7 +19,7 @@ task jmustacheRepackJar(type: Jar) { repackJar -> repackJar.archiveVersion = jmustacheVersion doLast() { - project.ant { + ant { taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", classpath: configurations.jarjar.asPath jarjar(destfile: repackJar.archiveFile.get()) { From 5a7a1412d0408615fd3e99a648b55b904736b9b3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 13:14:57 +0100 Subject: [PATCH 47/60] Upgrade Java version in .sdkmanrc --- .sdkmanrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sdkmanrc b/.sdkmanrc index 828308d27..bea2d5156 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17.0.12-librca +java=17.0.15-librca From ddbfacb6ebf9f6df18a24aa5f265c55bdf7a5d65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 13:15:34 +0100 Subject: [PATCH 48/60] Upgrade to Develocity Conventions 0.0.23 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 33995e502..fd285ac1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id "io.spring.develocity.conventions" version "0.0.21" + id "io.spring.develocity.conventions" version "0.0.23" } rootProject.name = "spring-restdocs" From 3501200098ccb2c4d529782904e0f146bebaf41d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 13:24:09 +0100 Subject: [PATCH 49/60] Use Spring Framework 6.2.x by default Closes gh-966 --- gradle.properties | 2 +- spring-restdocs-core/build.gradle | 2 +- spring-restdocs-mockmvc/build.gradle | 2 +- spring-restdocs-webtestclient/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 64a74adbb..fbd1b3322 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ org.gradle.parallel=true javaFormatVersion=0.0.43 jmustacheVersion=1.15 -springFrameworkVersion=6.1.13 +springFrameworkVersion=6.2.7 diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index c8a193b31..61bc9d256 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -86,6 +86,6 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesRuntime compatibilityTest { dependency("Spring Framework") { springFramework -> springFramework.groupId = "org.springframework" - springFramework.versions = ["6.0.+", "6.2.+"] + springFramework.versions = ["6.0.+", "6.1.+"] } } diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index 077a03f23..a1cc2111e 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -26,6 +26,6 @@ dependencies { compatibilityTest { dependency("Spring Framework") { springFramework -> springFramework.groupId = "org.springframework" - springFramework.versions = ["6.0.+", "6.2.+"] + springFramework.versions = ["6.0.+", "6.1.+"] } } \ No newline at end of file diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index 1a5f848b3..d09bd8199 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -25,6 +25,6 @@ dependencies { compatibilityTest { dependency("Spring Framework") { springFramework -> springFramework.groupId = "org.springframework" - springFramework.versions = ["6.0.+", "6.2.+"] + springFramework.versions = ["6.0.+", "6.1.+"] } } From c7bde714d6c68fefe7c19c6dd841ac644cdf9207 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 16:47:20 +0100 Subject: [PATCH 50/60] Migrate tests to JUnit 5 Closes gh-959 --- spring-restdocs-asciidoctor/build.gradle | 7 +- .../AbstractOperationBlockMacroTests.java | 65 +- .../DefaultAttributesPreprocessorTests.java | 10 +- .../GradleOperationBlockMacroTests.java | 25 +- .../MavenOperationBlockMacroTests.java | 20 +- .../SnippetsDirectoryResolverTests.java | 19 +- spring-restdocs-core/build.gradle | 20 +- .../restdocs/snippet/TemplatedSnippet.java | 6 +- .../restdocs/AbstractSnippetTests.java | 34 +- .../RestDocumentationGeneratorTests.java | 16 +- .../ConcatenatingCommandFormatterTests.java | 14 +- .../restdocs/cli/CurlRequestSnippetTests.java | 279 +++---- .../cli/HttpieRequestSnippetTests.java | 291 ++++---- .../RestDocumentationConfigurerTests.java | 26 +- .../ConstraintDescriptionsTests.java | 10 +- ...dleConstraintDescriptionResolverTests.java | 76 +- .../ValidatorConstraintResolverTests.java | 14 +- .../RequestCookiesSnippetFailureTests.java | 59 -- .../cookies/RequestCookiesSnippetTests.java | 152 ++-- .../ResponseCookiesSnippetFailureTests.java | 60 -- .../cookies/ResponseCookiesSnippetTests.java | 152 ++-- .../RequestHeadersSnippetFailureTests.java | 59 -- .../headers/RequestHeadersSnippetTests.java | 160 ++--- .../ResponseHeadersSnippetFailureTests.java | 60 -- .../headers/ResponseHeadersSnippetTests.java | 152 ++-- .../http/HttpRequestSnippetTests.java | 228 +++--- .../http/HttpResponseSnippetTests.java | 95 ++- .../ContentTypeLinkExtractorTests.java | 12 +- .../LinkExtractorsPayloadTests.java | 32 +- .../hypermedia/LinksSnippetFailureTests.java | 80 --- .../hypermedia/LinksSnippetTests.java | 176 +++-- ...ntModifyingOperationPreprocessorTests.java | 12 +- ...tingOperationRequestPreprocessorTests.java | 8 +- ...ingOperationResponsePreprocessorTests.java | 8 +- ...rsModifyingOperationPreprocessorTests.java | 24 +- .../LinkMaskingContentModifierTests.java | 18 +- .../PatternReplacingContentModifierTests.java | 14 +- .../PrettyPrintingContentModifierTests.java | 31 +- ...riModifyingOperationPreprocessorTests.java | 68 +- .../AsciidoctorRequestFieldsSnippetTests.java | 58 +- ...ldPathPayloadSubsectionExtractorTests.java | 36 +- .../payload/FieldTypeResolverTests.java | 4 +- .../payload/JsonContentHandlerTests.java | 44 +- .../restdocs/payload/JsonFieldPathTests.java | 52 +- .../restdocs/payload/JsonFieldPathsTests.java | 24 +- .../payload/JsonFieldProcessorTests.java | 98 +-- .../JsonFieldTypesDiscovererTests.java | 56 +- .../restdocs/payload/JsonFieldTypesTests.java | 16 +- .../payload/PayloadDocumentationTests.java | 18 +- .../payload/RequestBodyPartSnippetTests.java | 116 ++- .../payload/RequestBodySnippetTests.java | 106 ++- .../RequestFieldsSnippetFailureTests.java | 196 ----- .../payload/RequestFieldsSnippetTests.java | 550 ++++++++------ .../RequestPartFieldsSnippetFailureTests.java | 70 -- .../RequestPartFieldsSnippetTests.java | 124 ++-- .../payload/ResponseBodySnippetTests.java | 112 ++- .../ResponseFieldsSnippetFailureTests.java | 175 ----- .../payload/ResponseFieldsSnippetTests.java | 557 ++++++++------ .../payload/XmlContentHandlerTests.java | 4 +- .../FormParametersSnippetFailureTests.java | 68 -- .../request/FormParametersSnippetTests.java | 208 +++--- .../PathParametersSnippetFailureTests.java | 73 -- .../request/PathParametersSnippetTests.java | 246 ++++--- .../QueryParametersSnippetFailureTests.java | 68 -- .../request/QueryParametersSnippetTests.java | 203 +++--- .../RequestPartsSnippetFailureTests.java | 68 -- .../request/RequestPartsSnippetTests.java | 174 ++--- ...tationContextPlaceholderResolverTests.java | 32 +- .../snippet/StandardWriterResolverTests.java | 27 +- .../snippet/TemplatedSnippetTests.java | 38 +- ...StandardTemplateResourceResolverTests.java | 14 +- ...sciidoctorTableCellContentLambdaTests.java | 10 +- .../request-fields-with-title.snippet | 1 + .../testfixtures/GeneratedSnippets.java | 144 ---- .../testfixtures/OperationTestRule.java | 52 -- .../testfixtures/OutputCaptureRule.java | 86 --- .../jupiter/AssertableSnippets.java | 679 ++++++++++++++++++ .../{ => jupiter}/CapturedOutput.java | 11 +- .../{ => jupiter}/OperationBuilder.java | 37 +- .../{ => jupiter}/OutputCapture.java | 18 +- .../jupiter/OutputCaptureExtension.java | 112 +++ .../jupiter/RenderedSnippetTest.java | 78 ++ .../jupiter/RenderedSnippetTestExtension.java | 155 ++++ .../testfixtures/jupiter/SnippetTemplate.java | 46 ++ .../testfixtures/jupiter/SnippetTest.java | 47 ++ .../jupiter/SnippetTestExtension.java | 66 ++ spring-restdocs-mockmvc/build.gradle | 8 +- .../mockmvc/MockMvcRequestConverterTests.java | 34 +- .../MockMvcResponseConverterTests.java | 10 +- ...ckMvcRestDocumentationConfigurerTests.java | 47 +- ...kMvcRestDocumentationIntegrationTests.java | 103 +-- ...RestDocumentationRequestBuildersTests.java | 42 +- spring-restdocs-platform/build.gradle | 3 +- spring-restdocs-restassured/build.gradle | 9 +- .../RestAssuredParameterBehaviorTests.java | 52 +- .../RestAssuredRequestConverterTests.java | 50 +- .../RestAssuredResponseConverterTests.java | 8 +- ...suredRestDocumentationConfigurerTests.java | 28 +- ...uredRestDocumentationIntegrationTests.java | 105 ++- .../restdocs/restassured/TomcatServer.java | 63 +- spring-restdocs-webtestclient/build.gradle | 8 +- .../WebTestClientRequestConverterTests.java | 30 +- .../WebTestClientResponseConverterTests.java | 10 +- ...lientRestDocumentationConfigurerTests.java | 29 +- ...ientRestDocumentationIntegrationTests.java | 39 +- 105 files changed, 4297 insertions(+), 4150 deletions(-) delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java delete mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java delete mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationTestRule.java delete mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCaptureRule.java create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java rename spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/{ => jupiter}/CapturedOutput.java (77%) rename spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/{ => jupiter}/OperationBuilder.java (93%) rename spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/{ => jupiter}/OutputCapture.java (94%) create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java create mode 100644 spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index 149c81e2b..f9d1f685d 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -10,10 +10,15 @@ dependencies { internal(platform(project(":spring-restdocs-platform"))) - testImplementation("junit:junit") testImplementation("org.apache.pdfbox:pdfbox") testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.springframework:spring-core") testRuntimeOnly("org.asciidoctor:asciidoctorj-pdf") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.named("test") { + useJUnitPlatform() } diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java index cfc43b553..d6df5f917 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,11 +35,10 @@ import org.asciidoctor.Attributes; import org.asciidoctor.Options; import org.asciidoctor.SafeMode; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.util.FileSystemUtils; @@ -51,42 +50,42 @@ * @author Gerrit Meier * @author Andy Wilkinson */ -public abstract class AbstractOperationBlockMacroTests { +abstract class AbstractOperationBlockMacroTests { - @Rule - public TemporaryFolder temp = new TemporaryFolder(); + private final Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - private Options options; + @TempDir + protected File temp; - private final Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + private Options options; - @Before - public void setUp() throws IOException { + @BeforeEach + void setUp() throws IOException { prepareOperationSnippets(getBuildOutputLocation()); this.options = Options.builder().safe(SafeMode.UNSAFE).baseDir(getSourceLocation()).build(); this.options.setAttributes(getAttributes()); CapturingLogHandler.clear(); } - @After - public void verifyLogging() { + @AfterEach + void verifyLogging() { assertThat(CapturingLogHandler.getLogRecords()).isEmpty(); } - public void prepareOperationSnippets(File buildOutputLocation) throws IOException { + private void prepareOperationSnippets(File buildOutputLocation) throws IOException { File destination = new File(buildOutputLocation, "generated-snippets/some-operation"); destination.mkdirs(); FileSystemUtils.copyRecursively(new File("src/test/resources/some-operation"), destination); } @Test - public void codeBlockSnippetInclude() throws Exception { + void codeBlockSnippetInclude() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[snippets='curl-request']", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-simple")); } @Test - public void operationWithParameterizedName() throws Exception { + void operationWithParameterizedName() throws Exception { Attributes attributes = getAttributes(); attributes.setAttribute("name", "some"); this.options.setAttributes(attributes); @@ -95,20 +94,20 @@ public void operationWithParameterizedName() throws Exception { } @Test - public void codeBlockSnippetIncludeWithPdfBackend() throws Exception { + void codeBlockSnippetIncludeWithPdfBackend() throws Exception { File output = configurePdfOutput(); this.asciidoctor.convert("operation::some-operation[snippets='curl-request']", this.options); assertThat(extractStrings(output)).containsExactly("Curl request", "$ curl 'http://localhost:8080/' -i", "1"); } @Test - public void tableSnippetInclude() throws Exception { + void tableSnippetInclude() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[snippets='response-fields']", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-table")); } @Test - public void tableSnippetIncludeWithPdfBackend() throws Exception { + void tableSnippetIncludeWithPdfBackend() throws Exception { File output = configurePdfOutput(); this.asciidoctor.convert("operation::some-operation[snippets='response-fields']", this.options); assertThat(extractStrings(output)).containsExactly("Response fields", "Path", "Type", "Description", "a", @@ -116,14 +115,14 @@ public void tableSnippetIncludeWithPdfBackend() throws Exception { } @Test - public void includeSnippetInSection() throws Exception { + void includeSnippetInSection() throws Exception { String result = this.asciidoctor.convert("= A\n:doctype: book\n:sectnums:\n\nAlpha\n\n== B\n\nBravo\n\n" + "operation::some-operation[snippets='curl-request']\n\n== C\n", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-in-section")); } @Test - public void includeSnippetInSectionWithAbsoluteLevelOffset() throws Exception { + void includeSnippetInSectionWithAbsoluteLevelOffset() throws Exception { String result = this.asciidoctor .convert("= A\n:doctype: book\n:sectnums:\n:leveloffset: 1\n\nAlpha\n\n= B\n\nBravo\n\n" + "operation::some-operation[snippets='curl-request']\n\n= C\n", this.options); @@ -131,7 +130,7 @@ public void includeSnippetInSectionWithAbsoluteLevelOffset() throws Exception { } @Test - public void includeSnippetInSectionWithRelativeLevelOffset() throws Exception { + void includeSnippetInSectionWithRelativeLevelOffset() throws Exception { String result = this.asciidoctor .convert("= A\n:doctype: book\n:sectnums:\n:leveloffset: +1\n\nAlpha\n\n= B\n\nBravo\n\n" + "operation::some-operation[snippets='curl-request']\n\n= C\n", this.options); @@ -139,7 +138,7 @@ public void includeSnippetInSectionWithRelativeLevelOffset() throws Exception { } @Test - public void includeSnippetInSectionWithPdfBackend() throws Exception { + void includeSnippetInSectionWithPdfBackend() throws Exception { File output = configurePdfOutput(); this.asciidoctor.convert("== Section\n" + "operation::some-operation[snippets='curl-request']", this.options); assertThat(extractStrings(output)).containsExactly("Section", "Curl request", @@ -147,26 +146,26 @@ public void includeSnippetInSectionWithPdfBackend() throws Exception { } @Test - public void includeMultipleSnippets() throws Exception { + void includeMultipleSnippets() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[snippets='curl-request,http-request']", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("multiple-snippets")); } @Test - public void useMacroWithoutSnippetAttributeAddsAllSnippets() throws Exception { + void useMacroWithoutSnippetAttributeAddsAllSnippets() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[]", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("all-snippets")); } @Test - public void useMacroWithEmptySnippetAttributeAddsAllSnippets() throws Exception { + void useMacroWithEmptySnippetAttributeAddsAllSnippets() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[snippets=]", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("all-snippets")); } @Test - public void includingMissingSnippetAddsWarning() throws Exception { + void includingMissingSnippetAddsWarning() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[snippets='missing-snippet']", this.options); assertThat(result).startsWith(getExpectedContentFromFile("missing-snippet")); assertThat(CapturingLogHandler.getLogRecords()).hasSize(1); @@ -177,13 +176,13 @@ public void includingMissingSnippetAddsWarning() throws Exception { } @Test - public void defaultTitleIsProvidedForCustomSnippet() throws Exception { + void defaultTitleIsProvidedForCustomSnippet() throws Exception { String result = this.asciidoctor.convert("operation::some-operation[snippets='custom-snippet']", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("custom-snippet-default-title")); } @Test - public void missingOperationIsHandledGracefully() throws Exception { + void missingOperationIsHandledGracefully() throws Exception { String result = this.asciidoctor.convert("operation::missing-operation[]", this.options); assertThat(result).startsWith(getExpectedContentFromFile("missing-operation")); assertThat(CapturingLogHandler.getLogRecords()).hasSize(1); @@ -194,14 +193,14 @@ public void missingOperationIsHandledGracefully() throws Exception { } @Test - public void titleOfBuiltInSnippetCanBeCustomizedUsingDocumentAttribute() throws URISyntaxException, IOException { + void titleOfBuiltInSnippetCanBeCustomizedUsingDocumentAttribute() throws URISyntaxException, IOException { String result = this.asciidoctor.convert(":operation-curl-request-title: Example request\n" + "operation::some-operation[snippets='curl-request']", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("built-in-snippet-custom-title")); } @Test - public void titleOfCustomSnippetCanBeCustomizedUsingDocumentAttribute() throws Exception { + void titleOfCustomSnippetCanBeCustomizedUsingDocumentAttribute() throws Exception { String result = this.asciidoctor.convert(":operation-custom-snippet-title: Customized title\n" + "operation::some-operation[snippets='custom-snippet']", this.options); assertThat(result).isEqualTo(getExpectedContentFromFile("custom-snippet-custom-title")); diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java index 78ba919cf..b5921dbaa 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -21,7 +21,7 @@ import org.asciidoctor.Asciidoctor; import org.asciidoctor.Attributes; import org.asciidoctor.Options; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -30,23 +30,23 @@ * * @author Andy Wilkinson */ -public class DefaultAttributesPreprocessorTests { +class DefaultAttributesPreprocessorTests { @Test - public void snippetsAttributeIsSet() { + void snippetsAttributeIsSet() { String converted = createAsciidoctor().convert("{snippets}", createOptions("projectdir=../../..")); assertThat(converted).contains("build" + File.separatorChar + "generated-snippets"); } @Test - public void snippetsAttributeFromConvertArgumentIsNotOverridden() { + void snippetsAttributeFromConvertArgumentIsNotOverridden() { String converted = createAsciidoctor().convert("{snippets}", createOptions("snippets=custom projectdir=../../..")); assertThat(converted).contains("custom"); } @Test - public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { + void snippetsAttributeFromDocumentPreambleIsNotOverridden() { String converted = createAsciidoctor().convert(":snippets: custom\n{snippets}", createOptions("projectdir=../../..")); assertThat(converted).contains("custom"); diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java index 8e8185b37..1ed21adb9 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,47 +19,42 @@ import java.io.File; import org.asciidoctor.Attributes; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests for Ruby operation block macro when used in a Gradle build. * * @author Andy Wilkinson */ -@RunWith(Parameterized.class) -public class GradleOperationBlockMacroTests extends AbstractOperationBlockMacroTests { +@ParameterizedClass +@ValueSource(strings = { "projectdir", "gradle-projectdir" }) +class GradleOperationBlockMacroTests extends AbstractOperationBlockMacroTests { private final String attributeName; - public GradleOperationBlockMacroTests(String attributeName) { + GradleOperationBlockMacroTests(String attributeName) { this.attributeName = attributeName; } - @Parameters(name = "{0}") - public static Object[] parameters() { - return new Object[] { "projectdir", "gradle-projectdir" }; - } - @Override protected Attributes getAttributes() { Attributes attributes = Attributes.builder() - .attribute(this.attributeName, new File(this.temp.getRoot(), "gradle-project").getAbsolutePath()) + .attribute(this.attributeName, new File(this.temp, "gradle-project").getAbsolutePath()) .build(); return attributes; } @Override protected File getBuildOutputLocation() { - File outputLocation = new File(this.temp.getRoot(), "gradle-project/build"); + File outputLocation = new File(this.temp, "gradle-project/build"); outputLocation.mkdirs(); return outputLocation; } @Override protected File getSourceLocation() { - File sourceLocation = new File(this.temp.getRoot(), "gradle-project/src/docs/asciidoc"); + File sourceLocation = new File(this.temp, "gradle-project/src/docs/asciidoc"); if (!sourceLocation.exists()) { sourceLocation.mkdirs(); } diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java index 702ed06b7..f02f326f9 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,23 +20,23 @@ import java.io.IOException; import org.asciidoctor.Attributes; -import org.junit.After; -import org.junit.Before; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; /** * Tests for Ruby operation block macro when used in a Maven build. * * @author Andy Wilkinson */ -public class MavenOperationBlockMacroTests extends AbstractOperationBlockMacroTests { +class MavenOperationBlockMacroTests extends AbstractOperationBlockMacroTests { - @Before - public void setMavenHome() { + @BeforeEach + void setMavenHome() { System.setProperty("maven.home", "maven-home"); } - @After - public void clearMavenHome() { + @AfterEach + void clearMavenHome() { System.clearProperty("maven.home"); } @@ -55,14 +55,14 @@ protected Attributes getAttributes() { @Override protected File getBuildOutputLocation() { - File outputLocation = new File(this.temp.getRoot(), "maven-project/target"); + File outputLocation = new File(this.temp, "maven-project/target"); outputLocation.mkdirs(); return outputLocation; } @Override protected File getSourceLocation() { - File sourceLocation = new File(this.temp.getRoot(), "maven-project/src/main/asciidoc"); + File sourceLocation = new File(this.temp, "maven-project/src/main/asciidoc"); if (!sourceLocation.exists()) { sourceLocation.mkdirs(); } diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java index 2bede597c..9ca213b93 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,8 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -35,23 +34,23 @@ */ public class SnippetsDirectoryResolverTests { - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @TempDir + File temp; @Test public void mavenProjectsUseTargetGeneratedSnippets() throws IOException { - this.temporaryFolder.newFile("pom.xml"); + new File(this.temp, "pom.xml").createNewFile(); Map attributes = new HashMap<>(); - attributes.put("docdir", new File(this.temporaryFolder.getRoot(), "src/main/asciidoc").getAbsolutePath()); + attributes.put("docdir", new File(this.temp, "src/main/asciidoc").getAbsolutePath()); File snippetsDirectory = getMavenSnippetsDirectory(attributes); assertThat(snippetsDirectory).isAbsolute(); - assertThat(snippetsDirectory).isEqualTo(new File(this.temporaryFolder.getRoot(), "target/generated-snippets")); + assertThat(snippetsDirectory).isEqualTo(new File(this.temp, "target/generated-snippets")); } @Test public void illegalStateExceptionWhenMavenPomCannotBeFound() { Map attributes = new HashMap<>(); - String docdir = new File(this.temporaryFolder.getRoot(), "src/main/asciidoc").getAbsolutePath(); + String docdir = new File(this.temp, "src/main/asciidoc").getAbsolutePath(); attributes.put("docdir", docdir); assertThatIllegalStateException().isThrownBy(() -> getMavenSnippetsDirectory(attributes)) .withMessage("pom.xml not found in '" + docdir + "' or above"); diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index ded501e45..6832649b4 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -32,6 +32,8 @@ task jmustacheRepackJar(type: Jar) { repackJar -> } dependencies { + compileOnly("org.apiguardian:apiguardian-api") + implementation("com.fasterxml.jackson.core:jackson-databind") implementation("org.springframework:spring-web") implementation(files(jmustacheRepackJar)) @@ -50,21 +52,29 @@ dependencies { optional("org.junit.jupiter:junit-jupiter-api") testFixturesApi(platform(project(":spring-restdocs-platform"))) - testFixturesApi("junit:junit") testFixturesApi("org.assertj:assertj-core") - testFixturesApi("org.hamcrest:hamcrest-core") + testFixturesApi("org.junit.jupiter:junit-jupiter") + testFixturesApi("org.mockito:mockito-core") + + testFixturesCompileOnly("org.apiguardian:apiguardian-api") + testFixturesImplementation(files(jmustacheRepackJar)) testFixturesImplementation("org.hamcrest:hamcrest-library") + testFixturesImplementation("org.mockito:mockito-core") testFixturesImplementation("org.springframework:spring-core") testFixturesImplementation("org.springframework:spring-web") + + testFixturesRuntimeOnly("org.junit.platform:junit-platform-launcher") + + testCompileOnly("org.apiguardian:apiguardian-api") - testImplementation("junit:junit") testImplementation("org.assertj:assertj-core") testImplementation("org.javamoney:moneta") testImplementation("org.mockito:mockito-core") testImplementation("org.springframework:spring-test") testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el") + testRuntimeOnly("org.junit.platform:junit-platform-engine") } jar { @@ -81,3 +91,7 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesApiElem components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 95fbb3cce..fb89916b4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,9 +74,9 @@ public void document(Operation operation) throws IOException { RestDocumentationContext context = (RestDocumentationContext) operation.getAttributes() .get(RestDocumentationContext.class.getName()); WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get(WriterResolver.class.getName()); + Map model = createModel(operation); + model.putAll(this.attributes); try (Writer writer = writerResolver.resolve(operation.getName(), this.snippetName, context)) { - Map model = createModel(operation); - model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() .get(TemplateEngine.class.getName()); writer.append(templateEngine.compileTemplate(this.templateName).render(model)); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index de1a65431..bc0a5d09e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,16 @@ package org.springframework.restdocs; -import java.util.Arrays; -import java.util.List; - -import org.junit.Rule; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.GeneratedSnippets; -import org.springframework.restdocs.testfixtures.OperationBuilder; import org.springframework.restdocs.testfixtures.SnippetConditions; import org.springframework.restdocs.testfixtures.SnippetConditions.CodeBlockCondition; import org.springframework.restdocs.testfixtures.SnippetConditions.HttpRequestCondition; import org.springframework.restdocs.testfixtures.SnippetConditions.HttpResponseCondition; import org.springframework.restdocs.testfixtures.SnippetConditions.TableCondition; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; import org.springframework.web.bind.annotation.RequestMethod; /** @@ -42,28 +33,11 @@ * * @author Andy Wilkinson */ -@RunWith(Parameterized.class) public abstract class AbstractSnippetTests { - protected final TemplateFormat templateFormat; - - @Rule - public GeneratedSnippets generatedSnippets; - - @Rule - public OperationBuilder operationBuilder; + protected final TemplateFormat templateFormat = TemplateFormats.asciidoctor(); - @Parameters(name = "{0}") - public static List parameters() { - return Arrays.asList(new Object[] { "Asciidoctor", TemplateFormats.asciidoctor() }, - new Object[] { "Markdown", TemplateFormats.markdown() }); - } - - protected AbstractSnippetTests(String name, TemplateFormat templateFormat) { - this.generatedSnippets = new GeneratedSnippets(templateFormat); - this.templateFormat = templateFormat; - this.operationBuilder = new OperationBuilder(this.templateFormat); - } + protected AssertableSnippets snippets; public CodeBlockCondition codeBlock(String language) { return this.codeBlock(language, null); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index 733c02647..3b41392a1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mockito; @@ -55,7 +55,7 @@ * @author Andy Wilkinson * @author Filip Hrisafov */ -public class RestDocumentationGeneratorTests { +class RestDocumentationGeneratorTests { @SuppressWarnings("unchecked") private final RequestConverter requestConverter = mock(RequestConverter.class); @@ -80,7 +80,7 @@ public class RestDocumentationGeneratorTests { private final OperationPreprocessor responsePreprocessor = mock(OperationPreprocessor.class); @Test - public void basicHandling() throws IOException { + void basicHandling() throws IOException { given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); @@ -90,7 +90,7 @@ public void basicHandling() throws IOException { } @Test - public void defaultSnippetsAreCalled() throws IOException { + void defaultSnippetsAreCalled() throws IOException { given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); @@ -107,7 +107,7 @@ public void defaultSnippetsAreCalled() throws IOException { } @Test - public void defaultOperationRequestPreprocessorsAreCalled() throws IOException { + void defaultOperationRequestPreprocessorsAreCalled() throws IOException { given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); @@ -128,7 +128,7 @@ public void defaultOperationRequestPreprocessorsAreCalled() throws IOException { } @Test - public void defaultOperationResponsePreprocessorsAreCalled() throws IOException { + void defaultOperationResponsePreprocessorsAreCalled() throws IOException { given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); @@ -149,7 +149,7 @@ public void defaultOperationResponsePreprocessorsAreCalled() throws IOException } @Test - public void newGeneratorOnlyCallsItsSnippets() throws IOException { + void newGeneratorOnlyCallsItsSnippets() throws IOException { OperationRequestPreprocessor requestPreprocessor = mock(OperationRequestPreprocessor.class); OperationResponsePreprocessor responsePreprocessor = mock(OperationResponsePreprocessor.class); given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java index 54c7cd44d..c69c879bb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -29,27 +29,27 @@ * @author Tomasz Kopczynski * @author Andy Wilkinson */ -public class ConcatenatingCommandFormatterTests { +class ConcatenatingCommandFormatterTests { private CommandFormatter singleLineFormat = new ConcatenatingCommandFormatter(" "); @Test - public void formattingAnEmptyListProducesAnEmptyString() { + void formattingAnEmptyListProducesAnEmptyString() { assertThat(this.singleLineFormat.format(Collections.emptyList())).isEqualTo(""); } @Test - public void formattingNullProducesAnEmptyString() { + void formattingNullProducesAnEmptyString() { assertThat(this.singleLineFormat.format(null)).isEqualTo(""); } @Test - public void formattingASingleElement() { + void formattingASingleElement() { assertThat(this.singleLineFormat.format(Collections.singletonList("alpha"))).isEqualTo(" alpha"); } @Test - public void formattingMultipleElements() { + void formattingMultipleElements() { assertThat(this.singleLineFormat.format(Arrays.asList("alpha", "bravo"))).isEqualTo(" alpha bravo"); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 2348adc46..e0136a1dd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,11 @@ import java.io.IOException; import java.util.Base64; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; import static org.assertj.core.api.Assertions.assertThat; @@ -40,200 +37,209 @@ * @author Paul-Christian Volkmer * @author Tomasz Kopczynski */ -@RunWith(Parameterized.class) -public class CurlRequestSnippetTests extends AbstractSnippetTests { +class CurlRequestSnippetTests { private CommandFormatter commandFormatter = CliDocumentation.singleLineFormat(); - public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void getRequest() throws IOException { + @RenderedSnippetTest + void getRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X GET")); + .document(operationBuilder.request("http://localhost/foo").build()); + assertThat(snippets.curlRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ curl 'http://localhost/foo' -i -X GET")); } - @Test - public void nonGetRequest() throws IOException { + @RenderedSnippetTest + void nonGetRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X POST")); + .document(operationBuilder.request("http://localhost/foo").method("POST").build()); + assertThat(snippets.curlRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ curl 'http://localhost/foo' -i -X POST")); } - @Test - public void requestWithContent() throws IOException { + @RenderedSnippetTest + void requestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").content("content").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X GET -d 'content'")); + .document(operationBuilder.request("http://localhost/foo").content("content").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET -d 'content'")); } - @Test - public void getRequestWithQueryString() throws IOException { + @RenderedSnippetTest + void getRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param=value").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?param=value' -i -X GET")); + .document(operationBuilder.request("http://localhost/foo?param=value").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X GET")); } - @Test - public void getRequestWithQueryStringWithNoValue() throws IOException { + @RenderedSnippetTest + void getRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?param' -i -X GET")); + .document(operationBuilder.request("http://localhost/foo?param").build()); + assertThat(snippets.curlRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ curl 'http://localhost/foo?param' -i -X GET")); } - @Test - public void postRequestWithQueryString() throws IOException { + @RenderedSnippetTest + void postRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param=value").method("POST").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?param=value' -i -X POST")); + .document(operationBuilder.request("http://localhost/foo?param=value").method("POST").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); } - @Test - public void postRequestWithQueryStringWithNoValue() throws IOException { + @RenderedSnippetTest + void postRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param").method("POST").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?param' -i -X POST")); + .document(operationBuilder.request("http://localhost/foo?param").method("POST").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?param' -i -X POST")); } - @Test - public void postRequestWithOneParameter() throws IOException { + @RenderedSnippetTest + void postRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").content("k1=v1").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + .document(operationBuilder.request("http://localhost/foo").method("POST").content("k1=v1").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); } - @Test - public void postRequestWithOneParameterAndExplicitContentType() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithOneParameterAndExplicitContentType(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .method("POST") .content("k1=v1") .build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); } - @Test - public void postRequestWithOneParameterWithNoValue() throws IOException { + @RenderedSnippetTest + void postRequestWithOneParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").content("k1=").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); + .document(operationBuilder.request("http://localhost/foo").method("POST").content("k1=").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); } - @Test - public void postRequestWithMultipleParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("POST") .content("k1=v1&k1=v1-bis&k2=v2") .build()); - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash") - .withContent("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); } - @Test - public void postRequestWithUrlEncodedParameter() throws IOException { + @RenderedSnippetTest + void postRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").content("k1=a%26b").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + .document(operationBuilder.request("http://localhost/foo").method("POST").content("k1=a%26b").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); } - @Test - public void postRequestWithJsonData() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithJsonData(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .content("{\"a\":\"alpha\"}") .build()); - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent( - "$ curl 'http://localhost/foo' -i -X POST -H 'Content-Type: application/json' -d '{\"a\":\"alpha\"}'")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content( + "$ curl 'http://localhost/foo' -i -X POST -H 'Content-Type: application/json' -d '{\"a\":\"alpha\"}'")); } - @Test - public void putRequestWithOneParameter() throws IOException { + @RenderedSnippetTest + void putRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("PUT").content("k1=v1").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); + .document(operationBuilder.request("http://localhost/foo").method("PUT").content("k1=v1").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); } - @Test - public void putRequestWithMultipleParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void putRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("PUT") .content("k1=v1&k1=v1-bis&k2=v2") .build()); - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash") - .withContent("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); } - @Test - public void putRequestWithUrlEncodedParameter() throws IOException { + @RenderedSnippetTest + void putRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("PUT").content("k1=a%26b").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); + .document(operationBuilder.request("http://localhost/foo").method("PUT").content("k1=a%26b").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); } - @Test - public void requestWithHeaders() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void requestWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent( - "$ curl 'http://localhost/foo' -i -X GET" + " -H 'Content-Type: application/json' -H 'a: alpha'")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET" + " -H 'Content-Type: application/json' -H 'a: alpha'")); } - @Test - public void requestWithHeadersMultiline() throws IOException { + @RenderedSnippetTest + void requestWithHeadersMultiline(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new CurlRequestSnippet(CliDocumentation.multiLineFormat()) - .document(this.operationBuilder.request("http://localhost/foo") + .document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent(String.format("$ curl 'http://localhost/foo' -i -X GET \\%n" + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(String.format("$ curl 'http://localhost/foo' -i -X GET \\%n" + " -H 'Content-Type: application/json' \\%n" + " -H 'a: alpha'"))); } - @Test - public void requestWithCookies() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void requestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .cookie("name1", "value1") .cookie("name2", "value2") .build()); - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash") - .withContent("$ curl 'http://localhost/foo' -i -X GET" + " --cookie 'name1=value1;name2=value2'")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET" + " --cookie 'name1=value1;name2=value2'")); } - @Test - public void multipartPostWithNoSubmittedFileName() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPostWithNoSubmittedFileName(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()) .build()); String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); + assertThat(snippets.curlRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPostWithContentType() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPostWithContentType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -242,12 +248,13 @@ public void multipartPostWithContentType() throws IOException { .build()); String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); + assertThat(snippets.curlRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPost() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -255,36 +262,38 @@ public void multipartPost() throws IOException { .build()); String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); + assertThat(snippets.curlRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void basicAuthCredentialsAreSuppliedUsingUserOption(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("user:secret".getBytes())) .build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -u 'user:secret' -X GET")); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -u 'user:secret' -X GET")); } - @Test - public void customAttributes() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void customAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X GET -H 'Host: api.example.com'" + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET -H 'Host: api.example.com'" + " -H 'Content-Type: application/json' -H 'a: alpha'")); } - @Test - public void deleteWithQueryString() throws IOException { + @RenderedSnippetTest + void deleteWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X DELETE")); + .document(operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X DELETE")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 3d4f9ff84..89500ef06 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,11 @@ import java.io.IOException; import java.util.Base64; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; import static org.assertj.core.api.Assertions.assertThat; @@ -41,249 +38,257 @@ * @author Raman Gupta * @author Tomasz Kopczynski */ -@RunWith(Parameterized.class) -public class HttpieRequestSnippetTests extends AbstractSnippetTests { +class HttpieRequestSnippetTests { private CommandFormatter commandFormatter = CliDocumentation.singleLineFormat(); - public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void getRequest() throws IOException { + @RenderedSnippetTest + void getRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo'")); + .document(operationBuilder.request("http://localhost/foo").build()); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content("$ http GET 'http://localhost/foo'")); } - @Test - public void nonGetRequest() throws IOException { + @RenderedSnippetTest + void nonGetRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http POST 'http://localhost/foo'")); + .document(operationBuilder.request("http://localhost/foo").method("POST").build()); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content("$ http POST 'http://localhost/foo'")); } - @Test - public void requestWithContent() throws IOException { + @RenderedSnippetTest + void requestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").content("content").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ echo 'content' | http GET 'http://localhost/foo'")); + .document(operationBuilder.request("http://localhost/foo").content("content").build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); } - @Test - public void getRequestWithQueryString() throws IOException { + @RenderedSnippetTest + void getRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param=value").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?param=value'")); + .document(operationBuilder.request("http://localhost/foo?param=value").build()); + assertThat(snippets.httpieRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ http GET 'http://localhost/foo?param=value'")); } - @Test - public void getRequestWithQueryStringWithNoValue() throws IOException { + @RenderedSnippetTest + void getRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?param'")); + .document(operationBuilder.request("http://localhost/foo?param").build()); + assertThat(snippets.httpieRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ http GET 'http://localhost/foo?param'")); } - @Test - public void postRequestWithQueryString() throws IOException { + @RenderedSnippetTest + void postRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param=value").method("POST").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http POST 'http://localhost/foo?param=value'")); + .document(operationBuilder.request("http://localhost/foo?param=value").method("POST").build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); } - @Test - public void postRequestWithQueryStringWithNoValue() throws IOException { + @RenderedSnippetTest + void postRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?param").method("POST").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http POST 'http://localhost/foo?param'")); + .document(operationBuilder.request("http://localhost/foo?param").method("POST").build()); + assertThat(snippets.httpieRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ http POST 'http://localhost/foo?param'")); } - @Test - public void postRequestWithOneParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1=v1") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form POST 'http://localhost/foo' 'k1=v1'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); } - @Test - public void postRequestWithOneParameterWithNoValue() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithOneParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form POST 'http://localhost/foo' 'k1='")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); } - @Test - public void postRequestWithMultipleParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1=v1&k1=v1-bis&k2=v2") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form POST 'http://localhost/foo' 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1' 'k1=v1-bis' 'k2=v2'")); } - @Test - public void postRequestWithUrlEncodedParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void postRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1=a%26b") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); } - @Test - public void putRequestWithOneParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void putRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("PUT") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1=v1") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); } - @Test - public void putRequestWithMultipleParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void putRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("PUT") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1=v1&k1=v1-bis&k2=v2") .build()); - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash") - .withContent("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); } - @Test - public void putRequestWithUrlEncodedParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void putRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .method("PUT") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .content("k1=a%26b") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); } - @Test - public void requestWithHeaders() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void requestWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash") - .withContent("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'"))); } - @Test - public void requestWithHeadersMultiline() throws IOException { + @RenderedSnippetTest + void requestWithHeadersMultiline(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new HttpieRequestSnippet(CliDocumentation.multiLineFormat()) - .document(this.operationBuilder.request("http://localhost/foo") + .document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(String.format( - "$ http GET 'http://localhost/foo' \\%n" + " 'Content-Type:application/json' \\%n 'a:alpha'"))); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(String.format("$ http GET 'http://localhost/foo' \\%n" + + " 'Content-Type:application/json' \\%n 'a:alpha'"))); } - @Test - public void requestWithCookies() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void requestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .cookie("name1", "value1") .cookie("name2", "value2") .build()); - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash") - .withContent("$ http GET 'http://localhost/foo'" + " 'Cookie:name1=value1' 'Cookie:name2=value2'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(("$ http GET 'http://localhost/foo'" + " 'Cookie:name1=value1' 'Cookie:name2=value2'"))); } - @Test - public void multipartPostWithNoSubmittedFileName() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + @RenderedSnippetTest + void multipartPostWithNoSubmittedFileName(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); String expectedContent = "$ http --multipart POST 'http://localhost/upload'" + " 'metadata'='{\"description\": \"foo\"}'"; - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPostWithContentType() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png") - .build()); + @RenderedSnippetTest + void multipartPostWithContentType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png") + .build()); // httpie does not yet support manually set content type by part String expectedContent = "$ http --multipart POST 'http://localhost/upload'" + " 'image'@'documents/images/example.png'"; - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPost() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png") - .build()); + @RenderedSnippetTest + void multipartPost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png") + .build()); String expectedContent = "$ http --multipart POST 'http://localhost/upload'" + " 'image'@'documents/images/example.png'"; - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void basicAuthCredentialsAreSuppliedUsingAuthOption(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("user:secret".getBytes())) .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --auth 'user:secret' GET 'http://localhost/foo'")); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); } - @Test - public void customAttributes() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void customAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo' 'Host:api.example.com'" + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http GET 'http://localhost/foo' 'Host:api.example.com'" + " 'Content-Type:application/json' 'a:alpha'")); } - @Test - public void deleteWithQueryString() throws IOException { + @RenderedSnippetTest + void deleteWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http DELETE 'http://localhost/foo?a=alpha&b=bravo'")); + .document(operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http DELETE 'http://localhost/foo?a=alpha&b=bravo'")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 07fae9e5a..aa6c68141 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -63,13 +63,13 @@ * @author Andy Wilkinson * @author Filip Hrisafov */ -public class RestDocumentationConfigurerTests { +class RestDocumentationConfigurerTests { private final TestRestDocumentationConfigurer configurer = new TestRestDocumentationConfigurer(); @SuppressWarnings("unchecked") @Test - public void defaultConfiguration() { + void defaultConfiguration() { Map configuration = new HashMap<>(); this.configurer.apply(configuration, createContext()); assertThat(configuration).containsKey(TemplateEngine.class.getName()); @@ -102,7 +102,7 @@ public void defaultConfiguration() { } @Test - public void customTemplateEngine() { + void customTemplateEngine() { Map configuration = new HashMap<>(); TemplateEngine templateEngine = mock(TemplateEngine.class); this.configurer.templateEngine(templateEngine).apply(configuration, createContext()); @@ -110,7 +110,7 @@ public void customTemplateEngine() { } @Test - public void customWriterResolver() { + void customWriterResolver() { Map configuration = new HashMap<>(); WriterResolver writerResolver = mock(WriterResolver.class); this.configurer.writerResolver(writerResolver).apply(configuration, createContext()); @@ -118,7 +118,7 @@ public void customWriterResolver() { } @Test - public void customDefaultSnippets() { + void customDefaultSnippets() { Map configuration = new HashMap<>(); this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()).apply(configuration, createContext()); assertThat(configuration).containsKey(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); @@ -133,7 +133,7 @@ public void customDefaultSnippets() { @SuppressWarnings("unchecked") @Test - public void additionalDefaultSnippets() { + void additionalDefaultSnippets() { Map configuration = new HashMap<>(); Snippet snippet = mock(Snippet.class); this.configurer.snippets().withAdditionalDefaults(snippet).apply(configuration, createContext()); @@ -148,7 +148,7 @@ public void additionalDefaultSnippets() { } @Test - public void customSnippetEncoding() { + void customSnippetEncoding() { Map configuration = new HashMap<>(); this.configurer.snippets().withEncoding("ISO-8859-1"); this.configurer.apply(configuration, createContext()); @@ -162,7 +162,7 @@ public void customSnippetEncoding() { } @Test - public void customTemplateFormat() { + void customTemplateFormat() { Map configuration = new HashMap<>(); this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()).apply(configuration, createContext()); assertThat(configuration).containsKey(SnippetConfiguration.class.getName()); @@ -174,7 +174,7 @@ public void customTemplateFormat() { @SuppressWarnings("unchecked") @Test - public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { + void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { Map configuration = new HashMap<>(); this.configurer.apply(configuration, createContext()); TemplateEngine templateEngine = (TemplateEngine) configuration.get(TemplateEngine.class.getName()); @@ -187,7 +187,7 @@ public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTempl @SuppressWarnings("unchecked") @Test - public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { + void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { Map configuration = new HashMap<>(); this.configurer.snippetConfigurer.withTemplateFormat(TemplateFormats.markdown()); this.configurer.apply(configuration, createContext()); @@ -199,7 +199,7 @@ public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidocto } @Test - public void customDefaultOperationRequestPreprocessor() { + void customDefaultOperationRequestPreprocessor() { Map configuration = new HashMap<>(); this.configurer.operationPreprocessors() .withRequestDefaults(Preprocessors.prettyPrint(), Preprocessors.modifyHeaders().remove("Foo")) @@ -214,7 +214,7 @@ public void customDefaultOperationRequestPreprocessor() { } @Test - public void customDefaultOperationResponsePreprocessor() { + void customDefaultOperationResponsePreprocessor() { Map configuration = new HashMap<>(); this.configurer.operationPreprocessors() .withResponseDefaults(Preprocessors.prettyPrint(), Preprocessors.modifyHeaders().remove("Foo")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 243dcf00b..c5425c38c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -30,7 +30,7 @@ * * @author Andy Wilkinson */ -public class ConstraintDescriptionsTests { +class ConstraintDescriptionsTests { private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class); @@ -41,7 +41,7 @@ public class ConstraintDescriptionsTests { this.constraintResolver, this.constraintDescriptionResolver); @Test - public void descriptionsForConstraints() { + void descriptionsForConstraints() { Constraint constraint1 = new Constraint("constraint1", Collections.emptyMap()); Constraint constraint2 = new Constraint("constraint2", Collections.emptyMap()); given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) @@ -52,7 +52,7 @@ public void descriptionsForConstraints() { } @Test - public void emptyListOfDescriptionsWhenThereAreNoConstraints() { + void emptyListOfDescriptionsWhenThereAreNoConstraints() { given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) .willReturn(Collections.emptyList()); assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size()).isEqualTo(0); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index f4b5e2af8..7a977a9a3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -60,7 +60,7 @@ import org.hibernate.validator.constraints.Mod10Check; import org.hibernate.validator.constraints.Mod11Check; import org.hibernate.validator.constraints.Range; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.ClassPathResource; @@ -77,183 +77,183 @@ * * @author Andy Wilkinson */ -public class ResourceBundleConstraintDescriptionResolverTests { +class ResourceBundleConstraintDescriptionResolverTests { private final ResourceBundleConstraintDescriptionResolver resolver = new ResourceBundleConstraintDescriptionResolver(); @Test - public void defaultMessageAssertFalse() { + void defaultMessageAssertFalse() { assertThat(constraintDescriptionForField("assertFalse")).isEqualTo("Must be false"); } @Test - public void defaultMessageAssertTrue() { + void defaultMessageAssertTrue() { assertThat(constraintDescriptionForField("assertTrue")).isEqualTo("Must be true"); } @Test - public void defaultMessageCodePointLength() { + void defaultMessageCodePointLength() { assertThat(constraintDescriptionForField("codePointLength")) .isEqualTo("Code point length must be between 2 and 5 inclusive"); } @Test - public void defaultMessageCurrency() { + void defaultMessageCurrency() { assertThat(constraintDescriptionForField("currency")) .isEqualTo("Must be in an accepted currency unit (GBP, USD)"); } @Test - public void defaultMessageDecimalMax() { + void defaultMessageDecimalMax() { assertThat(constraintDescriptionForField("decimalMax")).isEqualTo("Must be at most 9.875"); } @Test - public void defaultMessageDecimalMin() { + void defaultMessageDecimalMin() { assertThat(constraintDescriptionForField("decimalMin")).isEqualTo("Must be at least 1.5"); } @Test - public void defaultMessageDigits() { + void defaultMessageDigits() { assertThat(constraintDescriptionForField("digits")) .isEqualTo("Must have at most 2 integral digits and 5 fractional digits"); } @Test - public void defaultMessageFuture() { + void defaultMessageFuture() { assertThat(constraintDescriptionForField("future")).isEqualTo("Must be in the future"); } @Test - public void defaultMessageFutureOrPresent() { + void defaultMessageFutureOrPresent() { assertThat(constraintDescriptionForField("futureOrPresent")).isEqualTo("Must be in the future or the present"); } @Test - public void defaultMessageMax() { + void defaultMessageMax() { assertThat(constraintDescriptionForField("max")).isEqualTo("Must be at most 10"); } @Test - public void defaultMessageMin() { + void defaultMessageMin() { assertThat(constraintDescriptionForField("min")).isEqualTo("Must be at least 10"); } @Test - public void defaultMessageNotNull() { + void defaultMessageNotNull() { assertThat(constraintDescriptionForField("notNull")).isEqualTo("Must not be null"); } @Test - public void defaultMessageNull() { + void defaultMessageNull() { assertThat(constraintDescriptionForField("nul")).isEqualTo("Must be null"); } @Test - public void defaultMessagePast() { + void defaultMessagePast() { assertThat(constraintDescriptionForField("past")).isEqualTo("Must be in the past"); } @Test - public void defaultMessagePastOrPresent() { + void defaultMessagePastOrPresent() { assertThat(constraintDescriptionForField("pastOrPresent")).isEqualTo("Must be in the past or the present"); } @Test - public void defaultMessagePattern() { + void defaultMessagePattern() { assertThat(constraintDescriptionForField("pattern")) .isEqualTo("Must match the regular expression `[A-Z][a-z]+`"); } @Test - public void defaultMessageSize() { + void defaultMessageSize() { assertThat(constraintDescriptionForField("size")).isEqualTo("Size must be between 2 and 10 inclusive"); } @Test - public void defaultMessageCreditCardNumber() { + void defaultMessageCreditCardNumber() { assertThat(constraintDescriptionForField("creditCardNumber")) .isEqualTo("Must be a well-formed credit card number"); } @Test - public void defaultMessageEan() { + void defaultMessageEan() { assertThat(constraintDescriptionForField("ean")).isEqualTo("Must be a well-formed EAN13 number"); } @Test - public void defaultMessageEmail() { + void defaultMessageEmail() { assertThat(constraintDescriptionForField("email")).isEqualTo("Must be a well-formed email address"); } @Test - public void defaultMessageLength() { + void defaultMessageLength() { assertThat(constraintDescriptionForField("length")).isEqualTo("Length must be between 2 and 10 inclusive"); } @Test - public void defaultMessageLuhnCheck() { + void defaultMessageLuhnCheck() { assertThat(constraintDescriptionForField("luhnCheck")) .isEqualTo("Must pass the Luhn Modulo 10 checksum algorithm"); } @Test - public void defaultMessageMod10Check() { + void defaultMessageMod10Check() { assertThat(constraintDescriptionForField("mod10Check")).isEqualTo("Must pass the Mod10 checksum algorithm"); } @Test - public void defaultMessageMod11Check() { + void defaultMessageMod11Check() { assertThat(constraintDescriptionForField("mod11Check")).isEqualTo("Must pass the Mod11 checksum algorithm"); } @Test - public void defaultMessageNegative() { + void defaultMessageNegative() { assertThat(constraintDescriptionForField("negative")).isEqualTo("Must be negative"); } @Test - public void defaultMessageNegativeOrZero() { + void defaultMessageNegativeOrZero() { assertThat(constraintDescriptionForField("negativeOrZero")).isEqualTo("Must be negative or zero"); } @Test - public void defaultMessageNotBlank() { + void defaultMessageNotBlank() { assertThat(constraintDescriptionForField("notBlank")).isEqualTo("Must not be blank"); } @Test - public void defaultMessageNotEmpty() { + void defaultMessageNotEmpty() { assertThat(constraintDescriptionForField("notEmpty")).isEqualTo("Must not be empty"); } @Test - public void defaultMessageNotEmptyHibernateValidator() { + void defaultMessageNotEmptyHibernateValidator() { assertThat(constraintDescriptionForField("notEmpty")).isEqualTo("Must not be empty"); } @Test - public void defaultMessagePositive() { + void defaultMessagePositive() { assertThat(constraintDescriptionForField("positive")).isEqualTo("Must be positive"); } @Test - public void defaultMessagePositiveOrZero() { + void defaultMessagePositiveOrZero() { assertThat(constraintDescriptionForField("positiveOrZero")).isEqualTo("Must be positive or zero"); } @Test - public void defaultMessageRange() { + void defaultMessageRange() { assertThat(constraintDescriptionForField("range")).isEqualTo("Must be at least 10 and at most 100"); } @Test - public void defaultMessageUrl() { + void defaultMessageUrl() { assertThat(constraintDescriptionForField("url")).isEqualTo("Must be a well-formed URL"); } @Test - public void customMessage() { + void customMessage() { Thread.currentThread().setContextClassLoader(new ClassLoader() { @Override @@ -279,7 +279,7 @@ public URL getResource(String name) { } @Test - public void customResourceBundle() { + void customResourceBundle() { ResourceBundle bundle = new ListResourceBundle() { @Override @@ -294,7 +294,7 @@ protected Object[][] getContents() { } @Test - public void allBeanValidationConstraintsAreTested() throws Exception { + void allBeanValidationConstraintsAreTested() throws Exception { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources("jakarta/validation/constraints/*.class"); Set> beanValidationConstraints = new HashSet<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index 3d900b3c6..203817f81 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import org.assertj.core.description.TextDescription; import org.hibernate.validator.constraints.CompositionType; import org.hibernate.validator.constraints.ConstraintComposition; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -44,19 +44,19 @@ * * @author Andy Wilkinson */ -public class ValidatorConstraintResolverTests { +class ValidatorConstraintResolverTests { private final ValidatorConstraintResolver resolver = new ValidatorConstraintResolver(); @Test - public void singleFieldConstraint() { + void singleFieldConstraint() { List constraints = this.resolver.resolveForProperty("single", ConstrainedFields.class); assertThat(constraints).hasSize(1); assertThat(constraints.get(0).getName()).isEqualTo(NotNull.class.getName()); } @Test - public void multipleFieldConstraints() { + void multipleFieldConstraints() { List constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class); assertThat(constraints).hasSize(2); assertThat(constraints.get(0)).is(constraint(NotNull.class)); @@ -64,13 +64,13 @@ public void multipleFieldConstraints() { } @Test - public void noFieldConstraints() { + void noFieldConstraints() { List constraints = this.resolver.resolveForProperty("none", ConstrainedFields.class); assertThat(constraints).hasSize(0); } @Test - public void compositeConstraint() { + void compositeConstraint() { List constraints = this.resolver.resolveForProperty("composite", ConstrainedFields.class); assertThat(constraints).hasSize(1); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java deleted file mode 100644 index 2dfd95733..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.cookies; - -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for failures when rendering {@link RequestCookiesSnippet} due to missing or - * undocumented cookies. - * - * @author Clyde Stubbs - * @author Andy Wilkinson - */ -public class RequestCookiesSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void missingRequestCookie() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("JSESSIONID").description("one"))) - .document(this.operationBuilder.request("http://localhost/").build())) - .withMessage("Cookies with the following names were not found in the request: [JSESSIONID]"); - } - - @Test - public void undocumentedRequestCookie() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestCookiesSnippet(Collections.emptyList()).document( - this.operationBuilder.request("http://localhost/").cookie("JSESSIONID", "1234abcd5678efgh").build())) - .withMessageEndingWith("Cookies with the following names were not documented: [JSESSIONID]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java index b40198e2a..febac6e00 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,18 +20,15 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -42,138 +39,141 @@ * @author Clyde Stubbs * @author Andy Wilkinson */ -public class RequestCookiesSnippetTests extends AbstractSnippetTests { - - public RequestCookiesSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestCookiesSnippetTests { - @Test - public void requestWithCookies() throws IOException { + @RenderedSnippetTest + void requestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestCookiesSnippet( Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .cookie("tz", "Europe%2FLondon") .cookie("logged_in", "true") .build()); - assertThat(this.generatedSnippets.requestCookies()) - .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); } - @Test - public void ignoredRequestCookie() throws IOException { + @RenderedSnippetTest + void ignoredRequestCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestCookiesSnippet( Arrays.asList(cookieWithName("tz").ignored(), cookieWithName("logged_in").description("two"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .cookie("tz", "Europe%2FLondon") .cookie("logged_in", "true") .build()); - assertThat(this.generatedSnippets.requestCookies()) - .is(tableWithHeader("Name", "Description").row("`logged_in`", "two")); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`logged_in`", "two")); } - @Test - public void allUndocumentedCookiesCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedCookiesCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestCookiesSnippet( Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")), true) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .cookie("tz", "Europe%2FLondon") .cookie("logged_in", "true") .cookie("user_session", "abcd1234efgh5678") .build()); - assertThat(this.generatedSnippets.requestCookies()) - .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); } - @Test - public void missingOptionalCookie() throws IOException { + @RenderedSnippetTest + void missingOptionalCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestCookiesSnippet(Arrays.asList(cookieWithName("tz").description("one").optional(), cookieWithName("logged_in").description("two"))) - .document(this.operationBuilder.request("http://localhost/").cookie("logged_in", "true").build()); - assertThat(this.generatedSnippets.requestCookies()) - .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); + .document(operationBuilder.request("http://localhost/").cookie("logged_in", "true").build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); } - @Test - public void requestCookiesWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-cookies")) - .willReturn(snippetResource("request-cookies-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-cookies", template = "request-cookies-with-title") + void requestCookiesWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestCookiesSnippet(Collections.singletonList(cookieWithName("tz").description("one")), attributes(key("title").value("Custom title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .cookie("tz", "Europe%2FLondon") - .build()); - assertThat(this.generatedSnippets.requestCookies()).contains("Custom title"); + .document(operationBuilder.request("http://localhost/").cookie("tz", "Europe%2FLondon").build()); + assertThat(snippets.requestCookies()).contains("Custom title"); } - @Test - public void requestCookiesWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-cookies")) - .willReturn(snippetResource("request-cookies-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-cookies", template = "request-cookies-with-extra-column") + void requestCookiesWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestCookiesSnippet( Arrays.asList(cookieWithName("tz").description("one").attributes(key("foo").value("alpha")), cookieWithName("logged_in").description("two").attributes(key("foo").value("bravo")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .cookie("tz", "Europe%2FLondon") .cookie("logged_in", "true") .build()); - assertThat(this.generatedSnippets.requestCookies()).is(// - tableWithHeader("Name", "Description", "Foo").row("tz", "one", "alpha") - .row("logged_in", "two", "bravo")); + assertThat(snippets.requestCookies()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("tz", "one", "alpha") + .row("logged_in", "two", "bravo")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestCookiesSnippet( Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two"))) .and(cookieWithName("user_session").description("three")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .cookie("tz", "Europe%2FLondon") .cookie("logged_in", "true") .cookie("user_session", "abcd1234efgh5678") .build()); - assertThat(this.generatedSnippets.requestCookies()).is(tableWithHeader("Name", "Description").row("`tz`", "one") + assertThat(snippets.requestCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`tz`", "one") .row("`logged_in`", "two") .row("`user_session`", "three")); } - @Test - public void additionalDescriptorsWithRelaxedRequestCookies() throws IOException { + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedRequestCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestCookiesSnippet( Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")), true) .and(cookieWithName("user_session").description("three")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .cookie("tz", "Europe%2FLondon") .cookie("logged_in", "true") .cookie("user_session", "abcd1234efgh5678") .cookie("color_theme", "light") .build()); - assertThat(this.generatedSnippets.requestCookies()).is(tableWithHeader("Name", "Description").row("`tz`", "one") + assertThat(snippets.requestCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`tz`", "one") .row("`logged_in`", "two") .row("`user_session`", "three")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestCookiesSnippet(Collections.singletonList(cookieWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.request("http://localhost/").cookie("Foo|Bar", "baz").build()); - assertThat(this.generatedSnippets.requestCookies()).is(tableWithHeader("Name", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.request("http://localhost/").cookie("Foo|Bar", "baz").build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void missingRequestCookie(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("JSESSIONID").description("one"))) + .document(operationBuilder.request("http://localhost/").build())) + .withMessage("Cookies with the following names were not found in the request: [JSESSIONID]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedRequestCookie(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestCookiesSnippet(Collections.emptyList()).document( + operationBuilder.request("http://localhost/").cookie("JSESSIONID", "1234abcd5678efgh").build())) + .withMessageEndingWith("Cookies with the following names were not documented: [JSESSIONID]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java deleted file mode 100644 index e1249a85e..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.cookies; - -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; - -/** - * Tests for failures when rendering {@link ResponseCookiesSnippet} due to missing or - * undocumented cookies. - * - * @author Clyde Stubbs - * @author Andy Wilkinson - */ -public class ResponseCookiesSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void missingResponseCookie() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseCookiesSnippet( - Collections.singletonList(cookieWithName("JSESSIONID").description("one"))) - .document(this.operationBuilder.response().build())) - .withMessage("Cookies with the following names were not found in the response: [JSESSIONID]"); - } - - @Test - public void undocumentedResponseCookie() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseCookiesSnippet(Collections.emptyList()) - .document(this.operationBuilder.response().cookie("JSESSIONID", "1234abcd5678efgh").build())) - .withMessageEndingWith("Cookies with the following names were not documented: [JSESSIONID]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java index ad052243d..2d7883e45 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,18 +20,12 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -42,141 +36,127 @@ * @author Clyde Stubbs * @author Andy Wilkinson */ -public class ResponseCookiesSnippetTests extends AbstractSnippetTests { - - public ResponseCookiesSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class ResponseCookiesSnippetTests { - @Test - public void responseWithCookies() throws IOException { + @RenderedSnippetTest + void responseWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"), cookieWithName("user_session").description("two"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .cookie("has_recent_activity", "true") .cookie("user_session", "1234abcd5678efgh") .build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`has_recent_activity`", "one") - .row("`user_session`", "two")); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two")); } - @Test - public void ignoredResponseCookie() throws IOException { + @RenderedSnippetTest + void ignoredResponseCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").ignored(), cookieWithName("user_session").description("two"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .cookie("has_recent_activity", "true") .cookie("user_session", "1234abcd5678efgh") .build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`user_session`", "two")); + assertThat(snippets.responseCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`user_session`", "two")); } - @Test - public void allUndocumentedResponseCookiesCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedResponseCookiesCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"), cookieWithName("user_session").description("two")), true) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .cookie("has_recent_activity", "true") .cookie("user_session", "1234abcd5678efgh") .cookie("some_cookie", "value") .build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`has_recent_activity`", "one") - .row("`user_session`", "two")); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two")); } - @Test - public void missingOptionalResponseCookie() throws IOException { + @RenderedSnippetTest + void missingOptionalResponseCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one").optional(), cookieWithName("user_session").description("two"))) - .document(this.operationBuilder.response().cookie("user_session", "1234abcd5678efgh").build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`has_recent_activity`", "one") - .row("`user_session`", "two")); + .document(operationBuilder.response().cookie("user_session", "1234abcd5678efgh").build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two")); } - @Test - public void responseCookiesWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-cookies")) - .willReturn(snippetResource("response-cookies-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-cookies", template = "response-cookies-with-title") + void responseCookiesWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseCookiesSnippet(Collections.singletonList(cookieWithName("has_recent_activity").description("one")), attributes(key("title").value("Custom title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() - .cookie("has_recent_activity", "true") - .build()); - assertThat(this.generatedSnippets.responseCookies()).contains("Custom title"); + .document(operationBuilder.response().cookie("has_recent_activity", "true").build()); + assertThat(snippets.responseCookies()).contains("Custom title"); } - @Test - public void responseCookiesWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-cookies")) - .willReturn(snippetResource("response-cookies-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-cookies", template = "response-cookies-with-extra-column") + void responseCookiesWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseCookiesSnippet(Arrays.asList( cookieWithName("has_recent_activity").description("one").attributes(key("foo").value("alpha")), cookieWithName("user_session").description("two").attributes(key("foo").value("bravo")), cookieWithName("color_theme").description("three").attributes(key("foo").value("charlie")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() + .document(operationBuilder.response() .cookie("has_recent_activity", "true") .cookie("user_session", "1234abcd5678efgh") .cookie("color_theme", "high_contrast") .build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description", "Foo").row("has_recent_activity", "one", "alpha") - .row("user_session", "two", "bravo") - .row("color_theme", "three", "charlie")); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("has_recent_activity", "one", "alpha") + .row("user_session", "two", "bravo") + .row("color_theme", "three", "charlie")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { CookieDocumentation .responseCookies(cookieWithName("has_recent_activity").description("one"), cookieWithName("user_session").description("two")) .and(cookieWithName("color_theme").description("three")) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .cookie("has_recent_activity", "true") .cookie("user_session", "1234abcd5678efgh") .cookie("color_theme", "light") .build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`has_recent_activity`", "one") - .row("`user_session`", "two") - .row("`color_theme`", "three")); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two") + .row("`color_theme`", "three")); } - @Test - public void additionalDescriptorsWithRelaxedResponseCookies() throws IOException { + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedResponseCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { CookieDocumentation.relaxedResponseCookies(cookieWithName("has_recent_activity").description("one")) .and(cookieWithName("color_theme").description("two")) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .cookie("has_recent_activity", "true") .cookie("user_session", "1234abcd5678efgh") .cookie("color_theme", "light") .build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`has_recent_activity`", "one").row("`color_theme`", "two")); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`color_theme`", "two")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseCookiesSnippet(Collections.singletonList(cookieWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.response().cookie("Foo|Bar", "baz").build()); - assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + .document(operationBuilder.response().cookie("Foo|Bar", "baz").build()); + assertThat(snippets.responseCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java deleted file mode 100644 index a980632c2..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.headers; - -import java.util.Arrays; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; - -/** - * Tests for failures when rendering {@link RequestHeadersSnippet} due to missing or - * undocumented headers. - * - * @author Andy Wilkinson - */ -public class RequestHeadersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void missingRequestHeader() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description("one"))) - .document(this.operationBuilder.request("http://localhost/").build())) - .withMessage("Headers with the following names were not found in the request: [Accept]"); - } - - @Test - public void undocumentedRequestHeaderAndMissingRequestHeader() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description("one"))) - .document(this.operationBuilder.request("http://localhost/").header("X-Test", "test").build())) - .withMessageEndingWith("Headers with the following names were not found in the request: [Accept]"); - - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index edb90cb58..51f16ccc3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,15 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,19 +38,15 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class RequestHeadersSnippetTests extends AbstractSnippetTests { - - public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestHeadersSnippetTests { - @Test - public void requestWithHeaders() throws IOException { + @RenderedSnippetTest + void requestWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), headerWithName("Accept-Encoding").description("three"), headerWithName("Accept-Language").description("four"), headerWithName("Cache-Control").description("five"), headerWithName("Connection").description("six"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .header("X-Test", "test") .header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate") @@ -61,79 +54,69 @@ public void requestWithHeaders() throws IOException { .header("Cache-Control", "max-age=0") .header("Connection", "keep-alive") .build()); - assertThat(this.generatedSnippets.requestHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one") - .row("`Accept`", "two") - .row("`Accept-Encoding`", "three") - .row("`Accept-Language`", "four") - .row("`Cache-Control`", "five") - .row("`Connection`", "six")); + assertThat(snippets.requestHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four") + .row("`Cache-Control`", "five") + .row("`Connection`", "six")); } - @Test - public void caseInsensitiveRequestHeaders() throws IOException { + @RenderedSnippetTest + void caseInsensitiveRequestHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.request("/").header("X-test", "test").build()); - assertThat(this.generatedSnippets.requestHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + .document(operationBuilder.request("/").header("X-test", "test").build()); + assertThat(snippets.requestHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void undocumentedRequestHeader() throws IOException { - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.request("http://localhost/") - .header("X-Test", "test") - .header("Accept", "*/*") - .build()); - assertThat(this.generatedSnippets.requestHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + @RenderedSnippetTest + void undocumentedRequestHeader(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))).document( + operationBuilder.request("http://localhost/").header("X-Test", "test").header("Accept", "*/*").build()); + assertThat(snippets.requestHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void requestHeadersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")) - .willReturn(snippetResource("request-headers-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-headers", template = "request-headers-with-title") + void requestHeadersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one")), attributes(key("title").value("Custom title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .header("X-Test", "test") - .build()); - assertThat(this.generatedSnippets.requestHeaders()).contains("Custom title"); + .document(operationBuilder.request("http://localhost/").header("X-Test", "test").build()); + assertThat(snippets.requestHeaders()).contains("Custom title"); } - @Test - public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")) - .willReturn(snippetResource("request-headers-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-headers", template = "request-headers-with-extra-column") + void requestHeadersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one").attributes(key("foo").value("alpha")), headerWithName("Accept-Encoding").description("two").attributes(key("foo").value("bravo")), headerWithName("Accept").description("three").attributes(key("foo").value("charlie")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .header("X-Test", "test") .header("Accept-Encoding", "gzip, deflate") .header("Accept", "*/*") .build()); - assertThat(this.generatedSnippets.requestHeaders()).is(// - tableWithHeader("Name", "Description", "Foo").row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo") - .row("Accept", "three", "charlie")); + assertThat(snippets.requestHeaders()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") + .row("Accept", "three", "charlie")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { HeaderDocumentation .requestHeaders(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), headerWithName("Accept-Encoding").description("three"), headerWithName("Accept-Language").description("four")) .and(headerWithName("Cache-Control").description("five"), headerWithName("Connection").description("six")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .header("X-Test", "test") .header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate") @@ -141,28 +124,39 @@ public void additionalDescriptors() throws IOException { .header("Cache-Control", "max-age=0") .header("Connection", "keep-alive") .build()); - assertThat(this.generatedSnippets.requestHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one") - .row("`Accept`", "two") - .row("`Accept-Encoding`", "three") - .row("`Accept-Language`", "four") - .row("`Cache-Control`", "five") - .row("`Connection`", "six")); + assertThat(snippets.requestHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four") + .row("`Cache-Control`", "five") + .row("`Connection`", "six")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestHeadersSnippet(Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.request("http://localhost/").header("Foo|Bar", "baz").build()); - assertThat(this.generatedSnippets.requestHeaders()).is(tableWithHeader("Name", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.request("http://localhost/").header("Foo|Bar", "baz").build()); + assertThat(snippets.requestHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void missingRequestHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description("one"))) + .document(operationBuilder.request("http://localhost/").build())) + .withMessage("Headers with the following names were not found in the request: [Accept]"); + } + + @SnippetTest + void undocumentedRequestHeaderAndMissingRequestHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description("one"))) + .document(operationBuilder.request("http://localhost/").header("X-Test", "test").build())) + .withMessageEndingWith("Headers with the following names were not found in the request: [Accept]"); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java deleted file mode 100644 index 6efbe91d5..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.headers; - -import java.util.Arrays; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; - -/** - * Tests for failures when rendering {@link ResponseHeadersSnippet} due to missing or - * undocumented headers. - * - * @author Andy Wilkinson - */ -public class ResponseHeadersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void missingResponseHeader() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy( - () -> new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(this.operationBuilder.response().build())) - .withMessage("Headers with the following names were not found" + " in the response: [Content-Type]"); - } - - @Test - public void undocumentedResponseHeaderAndMissingResponseHeader() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy( - () -> new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(this.operationBuilder.response().header("X-Test", "test").build())) - .withMessageEndingWith("Headers with the following names were not found in the response: [Content-Type]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 4427d6c34..484ee4cbc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,15 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,119 +38,120 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class ResponseHeadersSnippetTests extends AbstractSnippetTests { - - public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class ResponseHeadersSnippetTests { - @Test - public void responseWithHeaders() throws IOException { + @RenderedSnippetTest + void responseWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Content-Type").description("two"), headerWithName("Etag").description("three"), headerWithName("Cache-Control").description("five"), headerWithName("Vary").description("six"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .header("Cache-Control", "max-age=0") .header("Vary", "User-Agent") .build()); - assertThat(this.generatedSnippets.responseHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one") - .row("`Content-Type`", "two") - .row("`Etag`", "three") - .row("`Cache-Control`", "five") - .row("`Vary`", "six")); + assertThat(snippets.responseHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Content-Type`", "two") + .row("`Etag`", "three") + .row("`Cache-Control`", "five") + .row("`Vary`", "six")); } - @Test - public void caseInsensitiveResponseHeaders() throws IOException { + @RenderedSnippetTest + void caseInsensitiveResponseHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.response().header("X-test", "test").build()); - assertThat(this.generatedSnippets.responseHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + .document(operationBuilder.response().header("X-test", "test").build()); + assertThat(snippets.responseHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void undocumentedResponseHeader() throws IOException { + @RenderedSnippetTest + void undocumentedResponseHeader(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.response().header("X-Test", "test").header("Content-Type", "*/*").build()); - assertThat(this.generatedSnippets.responseHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + .document(operationBuilder.response().header("X-Test", "test").header("Content-Type", "*/*").build()); + assertThat(snippets.responseHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void responseHeadersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")) - .willReturn(snippetResource("response-headers-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-headers", template = "response-headers-with-title") + void responseHeadersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one")), attributes(key("title").value("Custom title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() - .header("X-Test", "test") - .build()); - assertThat(this.generatedSnippets.responseHeaders()).contains("Custom title"); + .document(operationBuilder.response().header("X-Test", "test").build()); + assertThat(snippets.responseHeaders()).contains("Custom title"); } - @Test - public void responseHeadersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")) - .willReturn(snippetResource("response-headers-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-headers", template = "response-headers-with-extra-column") + void responseHeadersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one").attributes(key("foo").value("alpha")), headerWithName("Content-Type").description("two").attributes(key("foo").value("bravo")), headerWithName("Etag").description("three").attributes(key("foo").value("charlie")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() + .document(operationBuilder.response() .header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .build()); - assertThat(this.generatedSnippets.responseHeaders()) - .is(tableWithHeader("Name", "Description", "Foo").row("X-Test", "one", "alpha") - .row("Content-Type", "two", "bravo") - .row("Etag", "three", "charlie")); + assertThat(snippets.responseHeaders()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Content-Type", "two", "bravo") + .row("Etag", "three", "charlie")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { HeaderDocumentation .responseHeaders(headerWithName("X-Test").description("one"), headerWithName("Content-Type").description("two"), headerWithName("Etag").description("three")) .and(headerWithName("Cache-Control").description("five"), headerWithName("Vary").description("six")) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .header("Cache-Control", "max-age=0") .header("Vary", "User-Agent") .build()); - assertThat(this.generatedSnippets.responseHeaders()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one") - .row("`Content-Type`", "two") - .row("`Etag`", "three") - .row("`Cache-Control`", "five") - .row("`Vary`", "six")); + assertThat(snippets.responseHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Content-Type`", "two") + .row("`Etag`", "three") + .row("`Cache-Control`", "five") + .row("`Vary`", "six")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseHeadersSnippet(Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.response().header("Foo|Bar", "baz").build()); - assertThat(this.generatedSnippets.responseHeaders()).is(tableWithHeader("Name", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.response().header("Foo|Bar", "baz").build()); + assertThat(snippets.responseHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void missingResponseHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy( + () -> new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(operationBuilder.response().build())) + .withMessage("Headers with the following names were not found" + " in the response: [Content-Type]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedResponseHeaderAndMissingResponseHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy( + () -> new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(operationBuilder.response().header("X-Test", "test").build())) + .withMessageEndingWith("Headers with the following names were not found in the response: [Content-Type]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 447e00c05..a37e3fd3c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,14 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,162 +35,160 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpRequestSnippetTests extends AbstractSnippetTests { +class HttpRequestSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void getRequest() throws IOException { + @RenderedSnippetTest + void getRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo").header("Alpha", "a").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); + .document(operationBuilder.request("http://localhost/foo").header("Alpha", "a").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); } - @Test - public void getRequestWithQueryParameters() throws IOException { + @RenderedSnippetTest + void getRequestWithQueryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?b=bravo").header("Alpha", "a").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo?b=bravo").header("Alpha", "a") - .header(HttpHeaders.HOST, "localhost")); + .document(operationBuilder.request("http://localhost/foo?b=bravo").header("Alpha", "a").build()); + assertThat(snippets.httpRequest()).isHttpRequest( + (request) -> request.get("/foo?b=bravo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); } - @Test - public void getRequestWithPort() throws IOException { + @RenderedSnippetTest + void getRequestWithPort(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost:8080/foo").header("Alpha", "a").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost:8080")); + .document(operationBuilder.request("http://localhost:8080/foo").header("Alpha", "a").build()); + assertThat(snippets.httpRequest()).isHttpRequest( + (request) -> request.get("/foo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost:8080")); } - @Test - public void getRequestWithCookies() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo") + @RenderedSnippetTest + void getRequestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo") .cookie("name1", "value1") .cookie("name2", "value2") .build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, "localhost") - .header(HttpHeaders.COOKIE, "name1=value1; name2=value2")); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.get("/foo") + .header(HttpHeaders.HOST, "localhost") + .header(HttpHeaders.COOKIE, "name1=value1; name2=value2")); } - @Test - public void getRequestWithQueryString() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?bar=baz").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); + @RenderedSnippetTest + void getRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo?bar=baz").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); } - @Test - public void getRequestWithQueryStringWithNoValue() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?bar").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo?bar").header(HttpHeaders.HOST, "localhost")); + @RenderedSnippetTest + void getRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo?bar").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo?bar").header(HttpHeaders.HOST, "localhost")); } - @Test - public void postRequestWithContent() throws IOException { + @RenderedSnippetTest + void postRequestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String content = "Hello, world"; new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo").method("POST").content(content).build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo").header(HttpHeaders.HOST, "localhost") - .content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .document(operationBuilder.request("http://localhost/foo").method("POST").content(content).build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/foo") + .header(HttpHeaders.HOST, "localhost") + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void postRequestWithContentAndQueryParameters() throws IOException { + @RenderedSnippetTest + void postRequestWithContentAndQueryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { String content = "Hello, world"; - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?a=alpha").method("POST").content(content).build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo?a=alpha").header(HttpHeaders.HOST, "localhost") - .content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + new HttpRequestSnippet() + .document(operationBuilder.request("http://localhost/foo?a=alpha").method("POST").content(content).build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/foo?a=alpha") + .header(HttpHeaders.HOST, "localhost") + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void postRequestWithCharset() throws IOException { + @RenderedSnippetTest + void postRequestWithCharset(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo") + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo") .method("POST") .header("Content-Type", "text/plain;charset=UTF-8") .content(contentBytes) .build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo").header("Content-Type", "text/plain;charset=UTF-8") - .header(HttpHeaders.HOST, "localhost") - .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) - .content(japaneseContent)); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/foo") + .header("Content-Type", "text/plain;charset=UTF-8") + .header(HttpHeaders.HOST, "localhost") + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) + .content(japaneseContent)); } - @Test - public void putRequestWithContent() throws IOException { + @RenderedSnippetTest + void putRequestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String content = "Hello, world"; new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo").method("PUT").content(content).build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.PUT, "/foo").header(HttpHeaders.HOST, "localhost") - .content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .document(operationBuilder.request("http://localhost/foo").method("PUT").content(content).build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.put("/foo") + .header(HttpHeaders.HOST, "localhost") + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void multipartPost() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) .build()); String expectedContent = createPart( String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/upload") + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost") .content(expectedContent)); } - @Test - public void multipartPut() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPut(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") .method("PUT") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) .build()); String expectedContent = createPart( String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.PUT, "/upload") + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.put("/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost") .content(expectedContent)); } - @Test - public void multipartPatch() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPatch(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") .method("PATCH") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) .build()); String expectedContent = createPart( String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.PATCH, "/upload") + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.patch("/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost") .content(expectedContent)); } - @Test - public void multipartPostWithFilename() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPostWithFilename(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) @@ -204,15 +196,16 @@ public void multipartPostWithFilename() throws IOException { .build()); String expectedContent = createPart(String .format("Content-Disposition: " + "form-data; " + "name=image; filename=image.png%n%n<< data >>")); - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/upload") + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost") .content(expectedContent)); } - @Test - public void multipartPostWithContentType() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload") + @RenderedSnippetTest + void multipartPostWithContentType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") .method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) @@ -220,38 +213,35 @@ public void multipartPostWithContentType() throws IOException { .build()); String expectedContent = createPart(String .format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/upload") + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost") .content(expectedContent)); } - @Test - public void getRequestWithCustomHost() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com") - .build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); + @RenderedSnippetTest + void getRequestWithCustomHost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document( + operationBuilder.request("http://localhost/foo").header(HttpHeaders.HOST, "api.example.com").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo").header(HttpHeaders.HOST, "api.example.com")); } - @Test - public void requestWithCustomSnippetAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-request")).willReturn(snippetResource("http-request-with-title")); - new HttpRequestSnippet(attributes(key("title").value("Title for the request"))).document( - this.operationBuilder.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo") - .build()); - assertThat(this.generatedSnippets.httpRequest()).contains("Title for the request"); + @RenderedSnippetTest + @SnippetTemplate(snippet = "http-request", template = "http-request-with-title") + void requestWithCustomSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) + .document(operationBuilder.request("http://localhost/foo").build()); + assertThat(snippets.httpRequest()).contains("Title for the request"); } - @Test - public void deleteWithQueryString() throws IOException { + @RenderedSnippetTest + void deleteWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.DELETE, "/foo?a=alpha&b=bravo").header("Host", "localhost")); + .document(operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.delete("/foo?a=alpha&b=bravo").header("Host", "localhost")); } private String createPart(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index edd7e4704..0ce5785ea 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,21 +18,16 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -42,72 +37,66 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpResponseSnippetTests extends AbstractSnippetTests { - - public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class HttpResponseSnippetTests { - @Test - public void basicResponse() throws IOException { - new HttpResponseSnippet().document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.httpResponse()).is(httpResponse(HttpStatus.OK)); + @RenderedSnippetTest + void basicResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.ok()); } - @Test - public void nonOkResponse() throws IOException { - new HttpResponseSnippet().document(this.operationBuilder.response().status(HttpStatus.BAD_REQUEST).build()); - assertThat(this.generatedSnippets.httpResponse()).is(httpResponse(HttpStatus.BAD_REQUEST)); + @RenderedSnippetTest + void nonOkResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.response().status(HttpStatus.BAD_REQUEST).build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.badRequest()); } - @Test - public void responseWithHeaders() throws IOException { - new HttpResponseSnippet().document(this.operationBuilder.response() + @RenderedSnippetTest + void responseWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha") .build()); - assertThat(this.generatedSnippets.httpResponse()) - .is(httpResponse(HttpStatus.OK).header("Content-Type", "application/json").header("a", "alpha")); + assertThat(snippets.httpResponse()).isHttpResponse( + (response) -> response.ok().header("Content-Type", "application/json").header("a", "alpha")); } - @Test - public void responseWithContent() throws IOException { + @RenderedSnippetTest + void responseWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String content = "content"; - new HttpResponseSnippet().document(this.operationBuilder.response().content(content).build()); - assertThat(this.generatedSnippets.httpResponse()).is(httpResponse(HttpStatus.OK).content(content) + new HttpResponseSnippet().document(operationBuilder.response().content(content).build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.ok() + .content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void responseWithCharset() throws IOException { + @RenderedSnippetTest + void responseWithCharset(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - new HttpResponseSnippet().document(this.operationBuilder.response() + new HttpResponseSnippet().document(operationBuilder.response() .header("Content-Type", "text/plain;charset=UTF-8") .content(contentBytes) .build()); - assertThat(this.generatedSnippets.httpResponse()) - .is(httpResponse(HttpStatus.OK).header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent) - .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.ok() + .header("Content-Type", "text/plain;charset=UTF-8") + .content(japaneseContent) + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); } - @Test - public void responseWithCustomSnippetAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-response")) - .willReturn(snippetResource("http-response-with-title")); - new HttpResponseSnippet(attributes(key("title").value("Title for the response"))).document( - this.operationBuilder.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .build()); - assertThat(this.generatedSnippets.httpResponse()).contains("Title for the response"); + @RenderedSnippetTest + @SnippetTemplate(snippet = "http-response", template = "http-response-with-title") + void responseWithCustomSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) + .document(operationBuilder.build()); + assertThat(snippets.httpResponse()).contains("Title for the response"); } - @Test - public void responseWithCustomStatus() throws IOException { - new HttpResponseSnippet() - .document(this.operationBuilder.response().status(HttpStatusCode.valueOf(215)).build()); - assertThat(this.generatedSnippets.httpResponse()).is(httpResponse(215)); + @RenderedSnippetTest + void responseWithCustomStatus(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.response().status(HttpStatusCode.valueOf(215)).build()); + assertThat(snippets.httpResponse()).isHttpResponse(((response) -> response.status(215))); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index f15407574..2e51b0ac9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -37,18 +37,18 @@ * * @author Andy Wilkinson */ -public class ContentTypeLinkExtractorTests { +class ContentTypeLinkExtractorTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); @Test - public void extractionFailsWithNullContentType() { + void extractionFailsWithNullContentType() { assertThatIllegalStateException().isThrownBy(() -> new ContentTypeLinkExtractor() .extractLinks(this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), null))); } @Test - public void extractorCalledWithMatchingContextType() throws IOException { + void extractorCalledWithMatchingContextType() throws IOException { Map extractors = new HashMap<>(); LinkExtractor extractor = mock(LinkExtractor.class); extractors.put(MediaType.APPLICATION_JSON, extractor); @@ -60,7 +60,7 @@ public void extractorCalledWithMatchingContextType() throws IOException { } @Test - public void extractorCalledWithCompatibleContextType() throws IOException { + void extractorCalledWithCompatibleContextType() throws IOException { Map extractors = new HashMap<>(); LinkExtractor extractor = mock(LinkExtractor.class); extractors.put(MediaType.APPLICATION_JSON, extractor); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index bf8a05fcb..5346d512c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,9 @@ import java.util.List; import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; @@ -39,13 +38,13 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with - * various payloads. + * Tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with various payloads. * * @author Andy Wilkinson */ -@RunWith(Parameterized.class) -public class LinkExtractorsPayloadTests { +@ParameterizedClass(name = "{1}") +@MethodSource("parameters") +class LinkExtractorsPayloadTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); @@ -53,25 +52,24 @@ public class LinkExtractorsPayloadTests { private final String linkType; - @Parameters(name = "{1}") - public static Collection data() { + static Collection parameters() { return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" }, new Object[] { new AtomLinkExtractor(), "atom" }); } - public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { + LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { this.linkExtractor = linkExtractor; this.linkType = linkType; } @Test - public void singleLink() throws IOException { + void singleLink() throws IOException { Map> links = this.linkExtractor.extractLinks(createResponse("single-link")); assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com/", "Alpha")), links); } @Test - public void multipleLinksWithDifferentRels() throws IOException { + void multipleLinksWithDifferentRels() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("multiple-links-different-rels")); assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com/", "Alpha"), @@ -79,20 +77,20 @@ public void multipleLinksWithDifferentRels() throws IOException { } @Test - public void multipleLinksWithSameRels() throws IOException { + void multipleLinksWithSameRels() throws IOException { Map> links = this.linkExtractor.extractLinks(createResponse("multiple-links-same-rels")); assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com/one", "Alpha one"), new Link("alpha", "https://alpha.example.com/two")), links); } @Test - public void noLinks() throws IOException { + void noLinks() throws IOException { Map> links = this.linkExtractor.extractLinks(createResponse("no-links")); assertLinks(Collections.emptyList(), links); } @Test - public void linksInTheWrongFormat() throws IOException { + void linksInTheWrongFormat() throws IOException { Map> links = this.linkExtractor.extractLinks(createResponse("wrong-format")); assertLinks(Collections.emptyList(), links); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java deleted file mode 100644 index d62a1d80a..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.hypermedia; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for failures when rendering {@link LinksSnippet} due to missing or undocumented - * links. - * - * @author Andy Wilkinson - */ -public class LinksSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedLink() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections.emptyList()) - .document(this.operationBuilder.build())) - .withMessage("Links with the following relations were not documented: [foo]"); - } - - @Test - public void missingLink() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor(), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(this.operationBuilder.build())) - .withMessage("Links with the following relations were not found in the response: [foo]"); - } - - @Test - public void undocumentedLinkAndMissingLink() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(this.operationBuilder.build())) - .withMessage("Links with the following relations were not documented: [a]. Links with the following" - + " relations were not found in the response: [foo]"); - } - - @Test - public void linkWithNoDescription() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Arrays.asList(new LinkDescriptor("foo"))) - .document(this.operationBuilder.build())) - .withMessage("No description was provided for the link with rel 'foo' and no title was available" - + " from the link in the payload"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index b71ab843e..7782ce42f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -39,115 +37,145 @@ * * @author Andy Wilkinson */ -public class LinksSnippetTests extends AbstractSnippetTests { - - public LinksSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class LinksSnippetTests { - @Test - public void ignoredLink() throws IOException { + @RenderedSnippetTest + void ignoredLink(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), new LinkDescriptor("b").description("Link b"))) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()).is(tableWithHeader("Relation", "Description").row("`b`", "Link b")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`b`", "Link b")); } - @Test - public void allUndocumentedLinksCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedLinksCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("b").description("Link b")), true) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()).is(tableWithHeader("Relation", "Description").row("`b`", "Link b")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`b`", "Link b")); } - @Test - public void presentOptionalLink() throws IOException { + @RenderedSnippetTest + void presentOptionalLink(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()).is(tableWithHeader("Relation", "Description").row("`foo`", "bar")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`foo`", "bar")); } - @Test - public void missingOptionalLink() throws IOException { + @RenderedSnippetTest + void missingOptionalLink(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()).is(tableWithHeader("Relation", "Description").row("`foo`", "bar")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`foo`", "bar")); } - @Test - public void documentedLinks() throws IOException { + @RenderedSnippetTest + void documentedLinks(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two"))) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()) - .is(tableWithHeader("Relation", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void linkDescriptionFromTitleInPayload() throws IOException { + @RenderedSnippetTest + void linkDescriptionFromTitleInPayload(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), new Link("b", "bravo", "Link b")), Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b"))) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()) - .is(tableWithHeader("Relation", "Description").row("`a`", "one").row("`b`", "Link b")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`a`", "one").row("`b`", "Link b")); } - @Test - public void linksWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")).willReturn(snippetResource("links-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "links", template = "links-with-title") + void linksWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two")), attributes(key("title").value("Title for the links"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .build()); - assertThat(this.generatedSnippets.links()).contains("Title for the links"); + .document(operationBuilder.build()); + assertThat(snippets.links()).contains("Title for the links"); } - @Test - public void linksWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")).willReturn(snippetResource("links-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "links", template = "links-with-extra-column") + void linksWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").description("one").attributes(key("foo").value("alpha")), new LinkDescriptor("b").description("two").attributes(key("foo").value("bravo")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .build()); - assertThat(this.generatedSnippets.links()) - .is(tableWithHeader("Relation", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); + .document(operationBuilder.build()); + assertThat(snippets.links()).isTable((table) -> table.withHeader("Relation", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), new LinkDescriptor("a").description("one")) .and(new LinkDescriptor("b").description("two")) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()) - .is(tableWithHeader("Relation", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.build()); - assertThat(this.generatedSnippets.links()).is(tableWithHeader("Relation", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedLink(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Collections.emptyList()) + .document(operationBuilder.build())) + .withMessage("Links with the following relations were not documented: [foo]"); + } + + @SnippetTest + void missingLink(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(operationBuilder.build())) + .withMessage("Links with the following relations were not found in the response: [foo]"); + } + + @SnippetTest + void undocumentedLinkAndMissingLink(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(operationBuilder.build())) + .withMessage("Links with the following relations were not documented: [a]. Links with the following" + + " relations were not found in the response: [foo]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void linkWithNoDescription(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Arrays.asList(new LinkDescriptor("foo"))) + .document(operationBuilder.build())) + .withMessage("No description was provided for the link with rel 'foo' and no title was available" + + " from the link in the payload"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index dbbee922a..107a4d0d4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.net.URI; import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -39,7 +39,7 @@ * @author Andy Wilkinson * */ -public class ContentModifyingOperationPreprocessorTests { +class ContentModifyingOperationPreprocessorTests { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); @@ -56,7 +56,7 @@ public byte[] modifyContent(byte[] originalContent, MediaType mediaType) { }); @Test - public void modifyRequestContent() { + void modifyRequestContent() { OperationRequest request = this.requestFactory.create(URI.create("http://localhost/"), HttpMethod.GET, "content".getBytes(), new HttpHeaders(), Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); @@ -64,7 +64,7 @@ public void modifyRequestContent() { } @Test - public void modifyResponseContent() { + void modifyResponseContent() { OperationResponse response = this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), "content".getBytes()); OperationResponse preprocessed = this.preprocessor.preprocess(response); @@ -72,7 +72,7 @@ public void modifyResponseContent() { } @Test - public void contentLengthIsUpdated() { + void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentLength(7); OperationRequest request = this.requestFactory.create(URI.create("http://localhost/"), HttpMethod.GET, diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java index b9f398448..a395d6402 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.operation.OperationRequest; @@ -31,10 +31,10 @@ * * @author Andy Wilkinson */ -public class DelegatingOperationRequestPreprocessorTests { +class DelegatingOperationRequestPreprocessorTests { @Test - public void delegationOccurs() { + void delegationOccurs() { OperationRequest originalRequest = mock(OperationRequest.class); OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); OperationRequest preprocessedRequest1 = mock(OperationRequest.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java index 2a2bb506f..513eeb869 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.operation.OperationResponse; @@ -31,10 +31,10 @@ * * @author Andy Wilkinson */ -public class DelegatingOperationResponsePreprocessorTests { +class DelegatingOperationResponsePreprocessorTests { @Test - public void delegationOccurs() { + void delegationOccurs() { OperationResponse originalResponse = mock(OperationResponse.class); OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse1 = mock(OperationResponse.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java index 5d3e77ac3..b68b27ff9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.function.Consumer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,12 +41,12 @@ * @author Jihoon Cha * @author Andy Wilkinson */ -public class HeadersModifyingOperationPreprocessorTests { +class HeadersModifyingOperationPreprocessorTests { private final HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); @Test - public void addNewHeader() { + void addNewHeader() { this.preprocessor.add("a", "alpha"); assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().get("a")) .isEqualTo(Arrays.asList("alpha")); @@ -55,7 +55,7 @@ public void addNewHeader() { } @Test - public void addValueToExistingHeader() { + void addValueToExistingHeader() { this.preprocessor.add("a", "alpha"); assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) .getHeaders() @@ -66,7 +66,7 @@ public void addValueToExistingHeader() { } @Test - public void setNewHeader() { + void setNewHeader() { this.preprocessor.set("a", "alpha", "avocado"); assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerSet()) .contains(entry("a", Arrays.asList("alpha", "avocado"))); @@ -75,7 +75,7 @@ public void setNewHeader() { } @Test - public void setExistingHeader() { + void setExistingHeader() { this.preprocessor.set("a", "alpha", "avocado"); assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) .getHeaders() @@ -86,14 +86,14 @@ public void setExistingHeader() { } @Test - public void removeNonExistentHeader() { + void removeNonExistentHeader() { this.preprocessor.remove("a"); assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerNames()).doesNotContain("a"); assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerNames()).doesNotContain("a"); } @Test - public void removeHeader() { + void removeHeader() { this.preprocessor.remove("a"); assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) .getHeaders() @@ -104,14 +104,14 @@ public void removeHeader() { } @Test - public void removeHeaderValueForNonExistentHeader() { + void removeHeaderValueForNonExistentHeader() { this.preprocessor.remove("a", "apple"); assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerNames()).doesNotContain("a"); assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerNames()).doesNotContain("a"); } @Test - public void removeHeaderValueWithMultipleValues() { + void removeHeaderValueWithMultipleValues() { this.preprocessor.remove("a", "apple"); assertThat( this.preprocessor.preprocess(createRequest((headers) -> headers.addAll("a", List.of("apple", "alpha")))) @@ -125,7 +125,7 @@ public void removeHeaderValueWithMultipleValues() { } @Test - public void removeHeaderValueWithSingleValueRemovesEntryEntirely() { + void removeHeaderValueWithSingleValueRemovesEntryEntirely() { this.preprocessor.remove("a", "apple"); assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) .getHeaders() @@ -136,7 +136,7 @@ public void removeHeaderValueWithSingleValueRemovesEntryEntirely() { } @Test - public void removeHeadersByNamePattern() { + void removeHeadersByNamePattern() { Consumer headersCustomizer = (headers) -> { headers.add("apple", "apple"); headers.add("alpha", "alpha"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index ad13b2551..7fe262d13 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.hypermedia.Link; @@ -38,7 +38,7 @@ * @author Andy Wilkinson * */ -public class LinkMaskingContentModifierTests { +class LinkMaskingContentModifierTests { private final ContentModifier contentModifier = new LinkMaskingContentModifier(); @@ -47,38 +47,38 @@ public class LinkMaskingContentModifierTests { private final Link[] maskedLinks = new Link[] { new Link("a", "..."), new Link("b", "...") }; @Test - public void halLinksAreMasked() throws Exception { + void halLinksAreMasked() throws Exception { assertThat(this.contentModifier.modifyContent(halPayloadWithLinks(this.links), null)) .isEqualTo(halPayloadWithLinks(this.maskedLinks)); } @Test - public void formattedHalLinksAreMasked() throws Exception { + void formattedHalLinksAreMasked() throws Exception { assertThat(this.contentModifier.modifyContent(formattedHalPayloadWithLinks(this.links), null)) .isEqualTo(formattedHalPayloadWithLinks(this.maskedLinks)); } @Test - public void atomLinksAreMasked() throws Exception { + void atomLinksAreMasked() throws Exception { assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links), null)) .isEqualTo(atomPayloadWithLinks(this.maskedLinks)); } @Test - public void formattedAtomLinksAreMasked() throws Exception { + void formattedAtomLinksAreMasked() throws Exception { assertThat(this.contentModifier.modifyContent(formattedAtomPayloadWithLinks(this.links), null)) .isEqualTo(formattedAtomPayloadWithLinks(this.maskedLinks)); } @Test - public void maskCanBeCustomized() throws Exception { + void maskCanBeCustomized() throws Exception { assertThat( new LinkMaskingContentModifier("custom").modifyContent(formattedAtomPayloadWithLinks(this.links), null)) .isEqualTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), new Link("b", "custom"))); } @Test - public void maskCanUseUtf8Characters() throws Exception { + void maskCanUseUtf8Characters() throws Exception { String ellipsis = "\u2026"; assertThat( new LinkMaskingContentModifier(ellipsis).modifyContent(formattedHalPayloadWithLinks(this.links), null)) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 838a045e3..61c24b77b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; @@ -31,10 +31,10 @@ * * @author Andy Wilkinson */ -public class PatternReplacingContentModifierTests { +class PatternReplacingContentModifierTests { @Test - public void patternsAreReplaced() { + void patternsAreReplaced() { Pattern pattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>"); @@ -44,7 +44,7 @@ public void patternsAreReplaced() { } @Test - public void contentThatDoesNotMatchIsUnchanged() { + void contentThatDoesNotMatchIsUnchanged() { Pattern pattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>"); @@ -53,7 +53,7 @@ public void contentThatDoesNotMatchIsUnchanged() { } @Test - public void encodingIsPreservedUsingCharsetFromContentType() { + void encodingIsPreservedUsingCharsetFromContentType() { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; Pattern pattern = Pattern.compile("[0-9]+"); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>"); @@ -63,7 +63,7 @@ public void encodingIsPreservedUsingCharsetFromContentType() { } @Test - public void encodingIsPreservedUsingFallbackCharset() { + void encodingIsPreservedUsingFallbackCharset() { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; Pattern pattern = Pattern.compile("[0-9]+"); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>", diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 386624a6f..a0c28376a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,11 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.testfixtures.OutputCaptureRule; +import org.springframework.restdocs.testfixtures.jupiter.CapturedOutput; +import org.springframework.restdocs.testfixtures.jupiter.OutputCaptureExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -33,19 +34,17 @@ * @author Andy Wilkinson * */ -public class PrettyPrintingContentModifierTests { - - @Rule - public OutputCaptureRule outputCapture = new OutputCaptureRule(); +@ExtendWith(OutputCaptureExtension.class) +class PrettyPrintingContentModifierTests { @Test - public void prettyPrintJson() { + void prettyPrintJson() { assertThat(new PrettyPrintingContentModifier().modifyContent("{\"a\":5}".getBytes(), null)) .isEqualTo(String.format("{%n \"a\" : 5%n}").getBytes()); } @Test - public void prettyPrintXml() { + void prettyPrintXml() { assertThat(new PrettyPrintingContentModifier() .modifyContent("".getBytes(), null)) .isEqualTo(String @@ -55,28 +54,28 @@ public void prettyPrintXml() { } @Test - public void empytContentIsHandledGracefully() { + void empytContentIsHandledGracefully() { assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes(), null)).isEqualTo("".getBytes()); } @Test - public void nonJsonAndNonXmlContentIsHandledGracefully() { + void nonJsonAndNonXmlContentIsHandledGracefully(CapturedOutput output) { String content = "abcdefg"; assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), null)) .isEqualTo(content.getBytes()); - assertThat(this.outputCapture).isEmpty(); + assertThat(output).isEmpty(); } @Test - public void nonJsonContentThatInitiallyLooksLikeJsonIsHandledGracefully() { + void nonJsonContentThatInitiallyLooksLikeJsonIsHandledGracefully(CapturedOutput output) { String content = "\"abc\",\"def\""; assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), null)) .isEqualTo(content.getBytes()); - assertThat(this.outputCapture).isEmpty(); + assertThat(output).isEmpty(); } @Test - public void encodingIsPreserved() throws Exception { + void encodingIsPreserved() throws Exception { Map input = new HashMap<>(); input.put("japanese", "\u30b3\u30f3\u30c6\u30f3\u30c4"); ObjectMapper objectMapper = new ObjectMapper(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 312167a20..0c5724eb7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.util.Collections; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,7 +41,7 @@ * * @author Andy Wilkinson */ -public class UriModifyingOperationPreprocessorTests { +class UriModifyingOperationPreprocessorTests { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); @@ -50,14 +50,14 @@ public class UriModifyingOperationPreprocessorTests { private final UriModifyingOperationPreprocessor preprocessor = new UriModifyingOperationPreprocessor(); @Test - public void requestUriSchemeCanBeModified() { + void requestUriSchemeCanBeModified() { this.preprocessor.scheme("https"); OperationRequest processed = this.preprocessor.preprocess(createRequestWithUri("http://localhost:12345/")); assertThat(processed.getUri()).isEqualTo(URI.create("https://localhost:12345/")); } @Test - public void requestUriHostCanBeModified() { + void requestUriHostCanBeModified() { this.preprocessor.host("api.example.com"); OperationRequest processed = this.preprocessor.preprocess(createRequestWithUri("https://api.foo.com:12345/")); assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com:12345/")); @@ -65,7 +65,7 @@ public void requestUriHostCanBeModified() { } @Test - public void requestUriPortCanBeModified() { + void requestUriPortCanBeModified() { this.preprocessor.port(23456); OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("https://api.example.com:12345/")); @@ -74,7 +74,7 @@ public void requestUriPortCanBeModified() { } @Test - public void requestUriPortCanBeRemoved() { + void requestUriPortCanBeRemoved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("https://api.example.com:12345/")); @@ -83,7 +83,7 @@ public void requestUriPortCanBeRemoved() { } @Test - public void requestUriPathIsPreserved() { + void requestUriPathIsPreserved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("https://api.example.com:12345/foo/bar")); @@ -91,7 +91,7 @@ public void requestUriPathIsPreserved() { } @Test - public void requestUriQueryIsPreserved() { + void requestUriQueryIsPreserved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("https://api.example.com:12345/?foo=bar")); @@ -99,7 +99,7 @@ public void requestUriQueryIsPreserved() { } @Test - public void requestUriAnchorIsPreserved() { + void requestUriAnchorIsPreserved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("https://api.example.com:12345/#foo")); @@ -107,7 +107,7 @@ public void requestUriAnchorIsPreserved() { } @Test - public void requestContentUriSchemeCanBeModified() { + void requestContentUriSchemeCanBeModified() { this.preprocessor.scheme("https"); OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( "The uri 'https://localhost:12345/' should be used. foo:bar will be unaffected")); @@ -116,7 +116,7 @@ public void requestContentUriSchemeCanBeModified() { } @Test - public void requestContentUriHostCanBeModified() { + void requestContentUriHostCanBeModified() { this.preprocessor.host("api.example.com"); OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( "The uri 'https://localhost:12345/' should be used. foo:bar will be unaffected")); @@ -125,7 +125,7 @@ public void requestContentUriHostCanBeModified() { } @Test - public void requestContentHostOfUriWithoutPortCanBeModified() { + void requestContentHostOfUriWithoutPortCanBeModified() { this.preprocessor.host("api.example.com"); OperationRequest processed = this.preprocessor.preprocess( createRequestWithContent("The uri 'https://localhost/' should be used. foo:bar will be unaffected")); @@ -134,7 +134,7 @@ public void requestContentHostOfUriWithoutPortCanBeModified() { } @Test - public void requestContentUriPortCanBeAdded() { + void requestContentUriPortCanBeAdded() { this.preprocessor.port(23456); OperationRequest processed = this.preprocessor.preprocess( createRequestWithContent("The uri 'http://localhost/' should be used. foo:bar will be unaffected")); @@ -143,7 +143,7 @@ public void requestContentUriPortCanBeAdded() { } @Test - public void requestContentUriPortCanBeModified() { + void requestContentUriPortCanBeModified() { this.preprocessor.port(23456); OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( "The uri 'http://localhost:12345/' should be used. foo:bar will be unaffected")); @@ -152,7 +152,7 @@ public void requestContentUriPortCanBeModified() { } @Test - public void requestContentUriPortCanBeRemoved() { + void requestContentUriPortCanBeRemoved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( "The uri 'http://localhost:12345/' should be used. foo:bar will be unaffected")); @@ -161,7 +161,7 @@ public void requestContentUriPortCanBeRemoved() { } @Test - public void multipleRequestContentUrisCanBeModified() { + void multipleRequestContentUrisCanBeModified() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( "Use 'http://localhost:12345/' or 'https://localhost:23456/' to access the service")); @@ -170,7 +170,7 @@ public void multipleRequestContentUrisCanBeModified() { } @Test - public void requestContentUriPathIsPreserved() { + void requestContentUriPathIsPreserved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); @@ -178,7 +178,7 @@ public void requestContentUriPathIsPreserved() { } @Test - public void requestContentUriQueryIsPreserved() { + void requestContentUriQueryIsPreserved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithContent("The uri 'http://localhost:12345/?foo=bar' should be used")); @@ -186,7 +186,7 @@ public void requestContentUriQueryIsPreserved() { } @Test - public void requestContentUriAnchorIsPreserved() { + void requestContentUriAnchorIsPreserved() { this.preprocessor.removePort(); OperationRequest processed = this.preprocessor .preprocess(createRequestWithContent("The uri 'http://localhost:12345/#foo' should be used")); @@ -194,7 +194,7 @@ public void requestContentUriAnchorIsPreserved() { } @Test - public void responseContentUriSchemeCanBeModified() { + void responseContentUriSchemeCanBeModified() { this.preprocessor.scheme("https"); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'http://localhost:12345/' should be used")); @@ -202,7 +202,7 @@ public void responseContentUriSchemeCanBeModified() { } @Test - public void responseContentUriHostCanBeModified() { + void responseContentUriHostCanBeModified() { this.preprocessor.host("api.example.com"); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'https://localhost:12345/' should be used")); @@ -211,7 +211,7 @@ public void responseContentUriHostCanBeModified() { } @Test - public void responseContentUriPortCanBeModified() { + void responseContentUriPortCanBeModified() { this.preprocessor.port(23456); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'http://localhost:12345/' should be used")); @@ -219,7 +219,7 @@ public void responseContentUriPortCanBeModified() { } @Test - public void responseContentUriPortCanBeRemoved() { + void responseContentUriPortCanBeRemoved() { this.preprocessor.removePort(); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'http://localhost:12345/' should be used")); @@ -227,7 +227,7 @@ public void responseContentUriPortCanBeRemoved() { } @Test - public void multipleResponseContentUrisCanBeModified() { + void multipleResponseContentUrisCanBeModified() { this.preprocessor.removePort(); OperationResponse processed = this.preprocessor.preprocess(createResponseWithContent( "Use 'http://localhost:12345/' or 'https://localhost:23456/' to access the service")); @@ -236,7 +236,7 @@ public void multipleResponseContentUrisCanBeModified() { } @Test - public void responseContentUriPathIsPreserved() { + void responseContentUriPathIsPreserved() { this.preprocessor.removePort(); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); @@ -244,7 +244,7 @@ public void responseContentUriPathIsPreserved() { } @Test - public void responseContentUriQueryIsPreserved() { + void responseContentUriQueryIsPreserved() { this.preprocessor.removePort(); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'http://localhost:12345/?foo=bar' should be used")); @@ -252,7 +252,7 @@ public void responseContentUriQueryIsPreserved() { } @Test - public void responseContentUriAnchorIsPreserved() { + void responseContentUriAnchorIsPreserved() { this.preprocessor.removePort(); OperationResponse processed = this.preprocessor .preprocess(createResponseWithContent("The uri 'http://localhost:12345/#foo' should be used")); @@ -260,7 +260,7 @@ public void responseContentUriAnchorIsPreserved() { } @Test - public void urisInRequestHeadersCanBeModified() { + void urisInRequestHeadersCanBeModified() { OperationRequest processed = this.preprocessor.host("api.example.com") .preprocess(createRequestWithHeader("Foo", "https://locahost:12345/")); assertThat(processed.getHeaders().getFirst("Foo")).isEqualTo("https://api.example.com:12345/"); @@ -268,14 +268,14 @@ public void urisInRequestHeadersCanBeModified() { } @Test - public void urisInResponseHeadersCanBeModified() { + void urisInResponseHeadersCanBeModified() { OperationResponse processed = this.preprocessor.host("api.example.com") .preprocess(createResponseWithHeader("Foo", "https://locahost:12345/")); assertThat(processed.getHeaders().getFirst("Foo")).isEqualTo("https://api.example.com:12345/"); } @Test - public void urisInRequestPartHeadersCanBeModified() { + void urisInRequestPartHeadersCanBeModified() { OperationRequest processed = this.preprocessor.host("api.example.com") .preprocess(createRequestWithPartWithHeader("Foo", "https://locahost:12345/")); assertThat(processed.getParts().iterator().next().getHeaders().getFirst("Foo")) @@ -283,7 +283,7 @@ public void urisInRequestPartHeadersCanBeModified() { } @Test - public void urisInRequestPartContentCanBeModified() { + void urisInRequestPartContentCanBeModified() { OperationRequest processed = this.preprocessor.host("api.example.com") .preprocess(createRequestWithPartWithContent("The uri 'https://localhost:12345/' should be used")); assertThat(new String(processed.getParts().iterator().next().getContent())) @@ -291,7 +291,7 @@ public void urisInRequestPartContentCanBeModified() { } @Test - public void modifiedUriDoesNotGetDoubleEncoded() { + void modifiedUriDoesNotGetDoubleEncoded() { this.preprocessor.scheme("https"); OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("http://localhost:12345/?foo=%7B%7D")); @@ -300,7 +300,7 @@ public void modifiedUriDoesNotGetDoubleEncoded() { } @Test - public void resultingRequestHasCookiesFromOriginalRequst() { + void resultingRequestHasCookiesFromOriginalRequst() { List cookies = Arrays.asList(new RequestCookie("a", "alpha")); OperationRequest request = this.requestFactory.create(URI.create("http://localhost:12345/"), HttpMethod.GET, new byte[0], new HttpHeaders(), Collections.emptyList(), cookies); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index 8c1144d55..ce0945462 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,21 +19,13 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.testfixtures.GeneratedSnippets; -import org.springframework.restdocs.testfixtures.OperationBuilder; -import org.springframework.restdocs.testfixtures.SnippetConditions; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest.Format; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; /** @@ -41,33 +33,17 @@ * * @author Andy Wilkinson */ -public class AsciidoctorRequestFieldsSnippetTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Rule - public GeneratedSnippets generatedSnippets = new GeneratedSnippets(TemplateFormats.asciidoctor()); - - @Test - public void requestFieldsWithListDescription() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")) - .willReturn(snippetResource("request-fields-with-list-description")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description(Arrays.asList("one", "two")))).document( - this.operationBuilder.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .content("{\"a\": \"foo\"}") - .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(SnippetConditions.tableWithHeader(TemplateFormats.asciidoctor(), "Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/asciidoctor/" + name + ".snippet"); +class AsciidoctorRequestFieldsSnippetTests { + + @RenderedSnippetTest(format = Format.ASCIIDOCTOR) + @SnippetTemplate(snippet = "request-fields", template = "request-fields-with-list-description") + void requestFieldsWithListDescription(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description(Arrays.asList("one", "two")))) + .document(operationBuilder.request("http://localhost/").content("{\"a\": \"foo\"}").build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java index 55c6638b9..a5d0b958c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; @@ -38,11 +38,11 @@ * * @author Andy Wilkinson */ -public class FieldPathPayloadSubsectionExtractorTests { +class FieldPathPayloadSubsectionExtractorTests { @Test @SuppressWarnings("unchecked") - public void extractMapSubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { + void extractMapSubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.b") .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), MediaType.APPLICATION_JSON); Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); @@ -52,8 +52,7 @@ public void extractMapSubsectionOfJsonMap() throws JsonParseException, JsonMappi @Test @SuppressWarnings("unchecked") - public void extractSingleElementArraySubsectionOfJsonMap() - throws JsonParseException, JsonMappingException, IOException { + void extractSingleElementArraySubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[]") .extractSubsection("{\"a\":[{\"b\":5}]}".getBytes(), MediaType.APPLICATION_JSON); Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); @@ -63,8 +62,7 @@ public void extractSingleElementArraySubsectionOfJsonMap() @Test @SuppressWarnings("unchecked") - public void extractMultiElementArraySubsectionOfJsonMap() - throws JsonParseException, JsonMappingException, IOException { + void extractMultiElementArraySubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a") .extractSubsection("{\"a\":[{\"b\":5},{\"b\":4}]}".getBytes(), MediaType.APPLICATION_JSON); Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); @@ -74,7 +72,7 @@ public void extractMultiElementArraySubsectionOfJsonMap() @Test @SuppressWarnings("unchecked") - public void extractMapSubsectionFromSingleElementArrayInAJsonMap() + void extractMapSubsectionFromSingleElementArrayInAJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") .extractSubsection("{\"a\":[{\"b\":{\"c\":5}}]}".getBytes(), MediaType.APPLICATION_JSON); @@ -85,7 +83,7 @@ public void extractMapSubsectionFromSingleElementArrayInAJsonMap() @Test @SuppressWarnings("unchecked") - public void extractMapSubsectionWithCommonStructureFromMultiElementArrayInAJsonMap() + void extractMapSubsectionWithCommonStructureFromMultiElementArrayInAJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") .extractSubsection("{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6}}]}".getBytes(), MediaType.APPLICATION_JSON); @@ -95,7 +93,7 @@ public void extractMapSubsectionWithCommonStructureFromMultiElementArrayInAJsonM } @Test - public void extractMapSubsectionWithVaryingStructureFromMultiElementArrayInAJsonMap() { + void extractMapSubsectionWithVaryingStructureFromMultiElementArrayInAJsonMap() { assertThatExceptionOfType(PayloadHandlingException.class) .isThrownBy(() -> new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": 7}}]}".getBytes(), MediaType.APPLICATION_JSON)) @@ -103,7 +101,7 @@ public void extractMapSubsectionWithVaryingStructureFromMultiElementArrayInAJson } @Test - public void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMap() { + void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMap() { assertThatExceptionOfType(PayloadHandlingException.class) .isThrownBy(() -> new FieldPathPayloadSubsectionExtractor("*.d").extractSubsection( "{\"a\":{\"b\":1},\"c\":{\"d\":{\"e\":1,\"f\":2}}}".getBytes(), MediaType.APPLICATION_JSON)) @@ -111,7 +109,7 @@ public void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMap() { } @Test - public void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMapWhereAllSubsectionFieldsAreOptional() { + void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMapWhereAllSubsectionFieldsAreOptional() { assertThatExceptionOfType(PayloadHandlingException.class) .isThrownBy(() -> new FieldPathPayloadSubsectionExtractor("*.d").extractSubsection( "{\"a\":{\"b\":1},\"c\":{\"d\":{\"e\":1,\"f\":2}}}".getBytes(), MediaType.APPLICATION_JSON, @@ -121,7 +119,7 @@ public void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMapWhere @Test @SuppressWarnings("unchecked") - public void extractMapSubsectionWithVaryingStructureDueToOptionalFieldsFromMultiElementArrayInAJsonMap() + void extractMapSubsectionWithVaryingStructureDueToOptionalFieldsFromMultiElementArrayInAJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": 7}}]}".getBytes(), MediaType.APPLICATION_JSON, @@ -133,7 +131,7 @@ public void extractMapSubsectionWithVaryingStructureDueToOptionalFieldsFromMulti @Test @SuppressWarnings("unchecked") - public void extractMapSubsectionWithVaryingStructureDueToOptionalParentFieldsFromMultiElementArrayInAJsonMap() + void extractMapSubsectionWithVaryingStructureDueToOptionalParentFieldsFromMultiElementArrayInAJsonMap() throws JsonParseException, JsonMappingException, IOException { byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": { \"e\": 7}}}]}".getBytes(), @@ -144,7 +142,7 @@ public void extractMapSubsectionWithVaryingStructureDueToOptionalParentFieldsFro } @Test - public void extractedSubsectionIsPrettyPrintedWhenInputIsPrettyPrinted() + void extractedSubsectionIsPrettyPrintedWhenInputIsPrettyPrinted() throws JsonParseException, JsonMappingException, JsonProcessingException, IOException { ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); byte[] prettyPrintedPayload = objectMapper @@ -157,7 +155,7 @@ public void extractedSubsectionIsPrettyPrintedWhenInputIsPrettyPrinted() } @Test - public void extractedSubsectionIsNotPrettyPrintedWhenInputIsNotPrettyPrinted() + void extractedSubsectionIsNotPrettyPrintedWhenInputIsNotPrettyPrinted() throws JsonParseException, JsonMappingException, JsonProcessingException, IOException { ObjectMapper objectMapper = new ObjectMapper(); byte[] payload = objectMapper @@ -169,7 +167,7 @@ public void extractedSubsectionIsNotPrettyPrintedWhenInputIsNotPrettyPrinted() } @Test - public void extractNonExistentSubsection() { + void extractNonExistentSubsection() { assertThatThrownBy(() -> new FieldPathPayloadSubsectionExtractor("a.c") .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), MediaType.APPLICATION_JSON)) .isInstanceOf(PayloadHandlingException.class) @@ -177,7 +175,7 @@ public void extractNonExistentSubsection() { } @Test - public void extractEmptyArraySubsection() { + void extractEmptyArraySubsection() { assertThatThrownBy(() -> new FieldPathPayloadSubsectionExtractor("a") .extractSubsection("{\"a\":[]}}".getBytes(), MediaType.APPLICATION_JSON)) .isInstanceOf(PayloadHandlingException.class) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java index daadf9d47..da5b1e902 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java index 7f069fa19..43f0100af 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -31,10 +31,10 @@ * @author Andy Wilkinson * @author Mathias Düsterhöft */ -public class JsonContentHandlerTests { +class JsonContentHandlerTests { @Test - public void typeForFieldWithNullValueMustMatch() { + void typeForFieldWithNullValueMustMatch() { FieldDescriptor descriptor = new FieldDescriptor("a").type(JsonFieldType.STRING); assertThatExceptionOfType(FieldTypesDoNotMatchException.class) .isThrownBy(() -> new JsonContentHandler("{\"a\": null}".getBytes(), Arrays.asList(descriptor)) @@ -42,7 +42,7 @@ public void typeForFieldWithNullValueMustMatch() { } @Test - public void typeForFieldWithNotNullAndThenNullValueMustMatch() { + void typeForFieldWithNotNullAndThenNullValueMustMatch() { FieldDescriptor descriptor = new FieldDescriptor("a[].id").type(JsonFieldType.STRING); assertThatExceptionOfType(FieldTypesDoNotMatchException.class).isThrownBy( () -> new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}".getBytes(), Arrays.asList(descriptor)) @@ -50,7 +50,7 @@ public void typeForFieldWithNotNullAndThenNullValueMustMatch() { } @Test - public void typeForFieldWithNullAndThenNotNullValueMustMatch() { + void typeForFieldWithNullAndThenNotNullValueMustMatch() { FieldDescriptor descriptor = new FieldDescriptor("a.[].id").type(JsonFieldType.STRING); assertThatExceptionOfType(FieldTypesDoNotMatchException.class).isThrownBy( () -> new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes(), Arrays.asList(descriptor)) @@ -58,7 +58,7 @@ public void typeForFieldWithNullAndThenNotNullValueMustMatch() { } @Test - public void typeForOptionalFieldWithNumberAndThenNullValueIsNumber() { + void typeForOptionalFieldWithNumberAndThenNullValueIsNumber() { FieldDescriptor descriptor = new FieldDescriptor("a[].id").optional(); Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}\"".getBytes(), Arrays.asList(descriptor)) @@ -67,7 +67,7 @@ public void typeForOptionalFieldWithNumberAndThenNullValueIsNumber() { } @Test - public void typeForOptionalFieldWithNullAndThenNumberIsNumber() { + void typeForOptionalFieldWithNullAndThenNumberIsNumber() { FieldDescriptor descriptor = new FieldDescriptor("a[].id").optional(); Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes(), Arrays.asList(descriptor)) @@ -76,7 +76,7 @@ public void typeForOptionalFieldWithNullAndThenNumberIsNumber() { } @Test - public void typeForFieldWithNumberAndThenNullValueIsVaries() { + void typeForFieldWithNumberAndThenNullValueIsVaries() { FieldDescriptor descriptor = new FieldDescriptor("a[].id"); Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}\"".getBytes(), Arrays.asList(descriptor)) @@ -85,7 +85,7 @@ public void typeForFieldWithNumberAndThenNullValueIsVaries() { } @Test - public void typeForFieldWithNullAndThenNumberIsVaries() { + void typeForFieldWithNullAndThenNumberIsVaries() { FieldDescriptor descriptor = new FieldDescriptor("a[].id"); Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes(), Arrays.asList(descriptor)) @@ -94,7 +94,7 @@ public void typeForFieldWithNullAndThenNumberIsVaries() { } @Test - public void typeForOptionalFieldWithNullValueCanBeProvidedExplicitly() { + void typeForOptionalFieldWithNullValueCanBeProvidedExplicitly() { FieldDescriptor descriptor = new FieldDescriptor("a").type(JsonFieldType.STRING).optional(); Object fieldType = new JsonContentHandler("{\"a\": null}".getBytes(), Arrays.asList(descriptor)) .resolveFieldType(descriptor); @@ -102,7 +102,7 @@ public void typeForOptionalFieldWithNullValueCanBeProvidedExplicitly() { } @Test - public void typeForFieldWithSometimesPresentOptionalAncestorCanBeProvidedExplicitly() { + void typeForFieldWithSometimesPresentOptionalAncestorCanBeProvidedExplicitly() { FieldDescriptor descriptor = new FieldDescriptor("a.[].b.c").type(JsonFieldType.NUMBER); FieldDescriptor ancestor = new FieldDescriptor("a.[].b").optional(); Object fieldType = new JsonContentHandler("{\"a\":[ { \"d\": 4}, {\"b\":{\"c\":5}, \"d\": 4}]}".getBytes(), @@ -112,13 +112,13 @@ public void typeForFieldWithSometimesPresentOptionalAncestorCanBeProvidedExplici } @Test - public void failsFastWithNonJsonContent() { + void failsFastWithNonJsonContent() { assertThatExceptionOfType(PayloadHandlingException.class) .isThrownBy(() -> new JsonContentHandler("Non-JSON content".getBytes(), Collections.emptyList())); } @Test - public void describedFieldThatIsNotPresentIsConsideredMissing() { + void describedFieldThatIsNotPresentIsConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a"), new FieldDescriptor("b"), new FieldDescriptor("c")); List missingFields = new JsonContentHandler("{\"a\": \"alpha\", \"b\":\"bravo\"}".getBytes(), @@ -129,7 +129,7 @@ public void describedFieldThatIsNotPresentIsConsideredMissing() { } @Test - public void describedOptionalFieldThatIsNotPresentIsNotConsideredMissing() { + void describedOptionalFieldThatIsNotPresentIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a"), new FieldDescriptor("b"), new FieldDescriptor("c").optional()); List missingFields = new JsonContentHandler("{\"a\": \"alpha\", \"b\":\"bravo\"}".getBytes(), @@ -139,7 +139,7 @@ public void describedOptionalFieldThatIsNotPresentIsNotConsideredMissing() { } @Test - public void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsPresentIsConsideredMissing() { + void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsPresentIsConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), new FieldDescriptor("b"), new FieldDescriptor("a.c")); List missingFields = new JsonContentHandler("{\"a\":\"alpha\",\"b\":\"bravo\"}".getBytes(), @@ -150,7 +150,7 @@ public void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsPresen } @Test - public void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsNotPresentIsNotConsideredMissing() { + void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsNotPresentIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), new FieldDescriptor("b"), new FieldDescriptor("a.c")); List missingFields = new JsonContentHandler("{\"b\":\"bravo\"}".getBytes(), descriptors) @@ -159,7 +159,7 @@ public void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsNotPre } @Test - public void describedFieldThatIsNotPresentNestedBeneathOptionalArrayThatIsEmptyIsNotConsideredMissing() { + void describedFieldThatIsNotPresentNestedBeneathOptionalArrayThatIsEmptyIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("outer"), new FieldDescriptor("outer[]").optional(), new FieldDescriptor("outer[].inner")); List missingFields = new JsonContentHandler("{\"outer\":[]}".getBytes(), descriptors) @@ -168,7 +168,7 @@ public void describedFieldThatIsNotPresentNestedBeneathOptionalArrayThatIsEmptyI } @Test - public void describedSometimesPresentFieldThatIsChildOfSometimesPresentOptionalArrayIsNotConsideredMissing() { + void describedSometimesPresentFieldThatIsChildOfSometimesPresentOptionalArrayIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a.[].c").optional(), new FieldDescriptor("a.[].c.d")); List missingFields = new JsonContentHandler( @@ -178,7 +178,7 @@ public void describedSometimesPresentFieldThatIsChildOfSometimesPresentOptionalA } @Test - public void describedMissingFieldThatIsChildOfNestedOptionalArrayThatIsEmptyIsNotConsideredMissing() { + void describedMissingFieldThatIsChildOfNestedOptionalArrayThatIsEmptyIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a.[].b").optional(), new FieldDescriptor("a.[].b.[]").optional(), new FieldDescriptor("a.[].b.[].c")); List missingFields = new JsonContentHandler("{\"a\":[{\"b\":[]}]}".getBytes(), descriptors) @@ -187,7 +187,7 @@ public void describedMissingFieldThatIsChildOfNestedOptionalArrayThatIsEmptyIsNo } @Test - public void describedMissingFieldThatIsChildOfNestedOptionalArrayThatContainsAnObjectIsConsideredMissing() { + void describedMissingFieldThatIsChildOfNestedOptionalArrayThatContainsAnObjectIsConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a.[].b").optional(), new FieldDescriptor("a.[].b.[]").optional(), new FieldDescriptor("a.[].b.[].c")); List missingFields = new JsonContentHandler("{\"a\":[{\"b\":[{}]}]}".getBytes(), descriptors) @@ -197,7 +197,7 @@ public void describedMissingFieldThatIsChildOfNestedOptionalArrayThatContainsAnO } @Test - public void describedMissingFieldThatIsChildOfOptionalObjectThatIsNullIsNotConsideredMissing() { + void describedMissingFieldThatIsChildOfOptionalObjectThatIsNullIsNotConsideredMissing() { List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), new FieldDescriptor("a.b")); List missingFields = new JsonContentHandler("{\"a\":null}".getBytes(), descriptors) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java index b350235dd..69324c659 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.restdocs.payload; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.payload.JsonFieldPath.PathType; @@ -28,131 +28,131 @@ * @author Andy Wilkinson * @author Jeremy Rickard */ -public class JsonFieldPathTests { +class JsonFieldPathTests { @Test - public void pathTypeOfSingleFieldIsSingle() { + void pathTypeOfSingleFieldIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a"); assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void pathTypeOfSingleNestedFieldIsSingle() { + void pathTypeOfSingleNestedFieldIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a.b"); assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void pathTypeOfTopLevelArrayIsSingle() { + void pathTypeOfTopLevelArrayIsSingle() { JsonFieldPath path = JsonFieldPath.compile("[]"); assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void pathTypeOfFieldBeneathTopLevelArrayIsMulti() { + void pathTypeOfFieldBeneathTopLevelArrayIsMulti() { JsonFieldPath path = JsonFieldPath.compile("[]a"); assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void pathTypeOfSingleNestedArrayIsSingle() { + void pathTypeOfSingleNestedArrayIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a[]"); assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void pathTypeOfArrayBeneathNestedFieldsIsSingle() { + void pathTypeOfArrayBeneathNestedFieldsIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a.b[]"); assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void pathTypeOfArrayOfArraysIsMulti() { + void pathTypeOfArrayOfArraysIsMulti() { JsonFieldPath path = JsonFieldPath.compile("a[][]"); assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void pathTypeOfFieldBeneathAnArrayIsMulti() { + void pathTypeOfFieldBeneathAnArrayIsMulti() { JsonFieldPath path = JsonFieldPath.compile("a[].b"); assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void pathTypeOfFieldBeneathTopLevelWildcardIsMulti() { + void pathTypeOfFieldBeneathTopLevelWildcardIsMulti() { JsonFieldPath path = JsonFieldPath.compile("*.a"); assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void pathTypeOfFieldBeneathNestedWildcardIsMulti() { + void pathTypeOfFieldBeneathNestedWildcardIsMulti() { JsonFieldPath path = JsonFieldPath.compile("a.*.b"); assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void pathTypeOfLeafWidlcardIsMulti() { + void pathTypeOfLeafWidlcardIsMulti() { JsonFieldPath path = JsonFieldPath.compile("a.*"); assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void compilationOfSingleElementPath() { + void compilationOfSingleElementPath() { assertThat(JsonFieldPath.compile("a").getSegments()).containsExactly("a"); } @Test - public void compilationOfMultipleElementPath() { + void compilationOfMultipleElementPath() { assertThat(JsonFieldPath.compile("a.b.c").getSegments()).containsExactly("a", "b", "c"); } @Test - public void compilationOfPathWithArraysWithNoDotSeparators() { + void compilationOfPathWithArraysWithNoDotSeparators() { assertThat(JsonFieldPath.compile("a[]b[]c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathWithArraysWithPreAndPostDotSeparators() { + void compilationOfPathWithArraysWithPreAndPostDotSeparators() { assertThat(JsonFieldPath.compile("a.[].b.[].c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathWithArraysWithPreDotSeparators() { + void compilationOfPathWithArraysWithPreDotSeparators() { assertThat(JsonFieldPath.compile("a.[]b.[]c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathWithArraysWithPostDotSeparators() { + void compilationOfPathWithArraysWithPostDotSeparators() { assertThat(JsonFieldPath.compile("a[].b[].c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathStartingWithAnArray() { + void compilationOfPathStartingWithAnArray() { assertThat(JsonFieldPath.compile("[]a.b.c").getSegments()).containsExactly("[]", "a", "b", "c"); } @Test - public void compilationOfMultipleElementPathWithBrackets() { + void compilationOfMultipleElementPathWithBrackets() { assertThat(JsonFieldPath.compile("['a']['b']['c']").getSegments()).containsExactly("a", "b", "c"); } @Test - public void compilationOfMultipleElementPathWithAndWithoutBrackets() { + void compilationOfMultipleElementPathWithAndWithoutBrackets() { assertThat(JsonFieldPath.compile("['a'][].b['c']").getSegments()).containsExactly("a", "[]", "b", "c"); } @Test - public void compilationOfMultipleElementPathWithAndWithoutBracketsAndEmbeddedDots() { + void compilationOfMultipleElementPathWithAndWithoutBracketsAndEmbeddedDots() { assertThat(JsonFieldPath.compile("['a.key'][].b['c']").getSegments()).containsExactly("a.key", "[]", "b", "c"); } @Test - public void compilationOfPathWithAWildcard() { + void compilationOfPathWithAWildcard() { assertThat(JsonFieldPath.compile("a.b.*.c").getSegments()).containsExactly("a", "b", "*", "c"); } @Test - public void compilationOfPathWithAWildcardInBrackets() { + void compilationOfPathWithAWildcardInBrackets() { assertThat(JsonFieldPath.compile("a.b.['*'].c").getSegments()).containsExactly("a", "b", "*", "c"); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java index 10b8d9a9f..2e770649c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Arrays; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; @@ -31,10 +31,10 @@ * * @author Andy Wilkinson */ -public class JsonFieldPathsTests { +class JsonFieldPathsTests { @Test - public void noUncommonPathsForSingleItem() { + void noUncommonPathsForSingleItem() { assertThat( JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": [ { \"c\": 2}, {\"c\": 3}, {\"c\": null}]}"))) .getUncommon()) @@ -42,20 +42,20 @@ public void noUncommonPathsForSingleItem() { } @Test - public void noUncommonPathsForMultipleIdenticalItems() { + void noUncommonPathsForMultipleIdenticalItems() { Object item = json("{\"a\": 1, \"b\": [ { \"c\": 2}, {\"c\": 3} ]}"); assertThat(JsonFieldPaths.from(Arrays.asList(item, item)).getUncommon()).isEmpty(); } @Test - public void noUncommonPathsForMultipleMatchingItemsWithDifferentScalarValues() { + void noUncommonPathsForMultipleMatchingItemsWithDifferentScalarValues() { assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": [ { \"c\": 2}, {\"c\": 3} ]}"), json("{\"a\": 4, \"b\": [ { \"c\": 5}, {\"c\": 6} ]}"))) .getUncommon()).isEmpty(); } @Test - public void missingEntryInMapIsIdentifiedAsUncommon() { + void missingEntryInMapIsIdentifiedAsUncommon() { assertThat( JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1}"), json("{\"a\": 1}"), json("{\"a\": 1, \"b\": 2}"))) .getUncommon()) @@ -63,21 +63,21 @@ public void missingEntryInMapIsIdentifiedAsUncommon() { } @Test - public void missingEntryInNestedMapIsIdentifiedAsUncommon() { + void missingEntryInNestedMapIsIdentifiedAsUncommon() { assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"c\": 1, \"d\": 2}}"))) .getUncommon()).containsExactly("b.d"); } @Test - public void missingEntriesInNestedMapAreIdentifiedAsUncommon() { + void missingEntriesInNestedMapAreIdentifiedAsUncommon() { assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"d\": 2}}"))) .getUncommon()).containsExactly("b.c", "b.d"); } @Test - public void absentItemFromFieldExtractionCausesAllPresentFieldsToBeIdentifiedAsUncommon() { + void absentItemFromFieldExtractionCausesAllPresentFieldsToBeIdentifiedAsUncommon() { assertThat(JsonFieldPaths .from(Arrays.asList(ExtractedField.ABSENT, json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"d\": 2}}"))) @@ -85,14 +85,14 @@ public void absentItemFromFieldExtractionCausesAllPresentFieldsToBeIdentifiedAsU } @Test - public void missingEntryBeneathArrayIsIdentifiedAsUncommon() { + void missingEntryBeneathArrayIsIdentifiedAsUncommon() { assertThat(JsonFieldPaths .from(Arrays.asList(json("[{\"b\": 1}]"), json("[{\"b\": 1}]"), json("[{\"b\": 1, \"c\": 2}]"))) .getUncommon()).containsExactly("[].c"); } @Test - public void missingEntryBeneathNestedArrayIsIdentifiedAsUncommon() { + void missingEntryBeneathNestedArrayIsIdentifiedAsUncommon() { assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": [{\"b\": 1}]}"), json("{\"a\": [{\"b\": 1}]}"), json("{\"a\": [{\"b\": 1, \"c\": 2}]}"))) .getUncommon()).containsExactly("a.[].c"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 4c2c45516..b87099857 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; @@ -37,19 +37,19 @@ * * @author Andy Wilkinson */ -public class JsonFieldProcessorTests { +class JsonFieldProcessorTests { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); @Test - public void extractTopLevelMapEntry() { + void extractTopLevelMapEntry() { Map payload = new HashMap<>(); payload.put("a", "alpha"); assertThat(this.fieldProcessor.extract("a", payload).getValue()).isEqualTo("alpha"); } @Test - public void extractNestedMapEntry() { + void extractNestedMapEntry() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -58,7 +58,7 @@ public void extractNestedMapEntry() { } @Test - public void extractTopLevelArray() { + void extractTopLevelArray() { List> payload = new ArrayList<>(); Map bravo = new HashMap<>(); bravo.put("b", "bravo"); @@ -68,7 +68,7 @@ public void extractTopLevelArray() { } @Test - public void extractArray() { + void extractArray() { Map payload = new HashMap<>(); Map bravo = new HashMap<>(); bravo.put("b", "bravo"); @@ -78,7 +78,7 @@ public void extractArray() { } @Test - public void extractArrayContents() { + void extractArrayContents() { Map payload = new HashMap<>(); Map bravo = new HashMap<>(); bravo.put("b", "bravo"); @@ -88,7 +88,7 @@ public void extractArrayContents() { } @Test - public void extractFromItemsInArray() { + void extractFromItemsInArray() { Map payload = new HashMap<>(); Map entry = new HashMap<>(); entry.put("b", "bravo"); @@ -98,7 +98,7 @@ public void extractFromItemsInArray() { } @Test - public void extractOccasionallyAbsentFieldFromItemsInArray() { + void extractOccasionallyAbsentFieldFromItemsInArray() { Map payload = new HashMap<>(); Map entry = new HashMap<>(); entry.put("b", "bravo"); @@ -109,7 +109,7 @@ public void extractOccasionallyAbsentFieldFromItemsInArray() { } @Test - public void extractOccasionallyNullFieldFromItemsInArray() { + void extractOccasionallyNullFieldFromItemsInArray() { Map payload = new HashMap<>(); Map nonNullField = new HashMap<>(); nonNullField.put("b", "bravo"); @@ -121,7 +121,7 @@ public void extractOccasionallyNullFieldFromItemsInArray() { } @Test - public void extractNestedArray() { + void extractNestedArray() { Map payload = new HashMap<>(); Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); @@ -133,7 +133,7 @@ public void extractNestedArray() { } @Test - public void extractFromItemsInNestedArray() { + void extractFromItemsInNestedArray() { Map payload = new HashMap<>(); Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); @@ -144,7 +144,7 @@ public void extractFromItemsInNestedArray() { } @Test - public void extractArraysFromItemsInNestedArray() { + void extractArraysFromItemsInNestedArray() { Map payload = new HashMap<>(); Map entry1 = createEntry("ids", Arrays.asList(1, 2)); Map entry2 = createEntry("ids", Arrays.asList(3)); @@ -156,27 +156,27 @@ public void extractArraysFromItemsInNestedArray() { } @Test - public void nonExistentTopLevelField() { + void nonExistentTopLevelField() { assertThat(this.fieldProcessor.extract("a", Collections.emptyMap()).getValue()) .isEqualTo(ExtractedField.ABSENT); } @Test - public void nonExistentNestedField() { + void nonExistentNestedField() { HashMap payload = new HashMap<>(); - payload.put("a", new HashMap()); + payload.put("a", new HashMap<>()); assertThat(this.fieldProcessor.extract("a.b", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } @Test - public void nonExistentNestedFieldWhenParentIsNotAMap() { + void nonExistentNestedFieldWhenParentIsNotAMap() { HashMap payload = new HashMap<>(); payload.put("a", 5); assertThat(this.fieldProcessor.extract("a.b", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } @Test - public void nonExistentFieldWhenParentIsAnArray() { + void nonExistentFieldWhenParentIsAnArray() { HashMap payload = new HashMap<>(); HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); @@ -185,20 +185,20 @@ public void nonExistentFieldWhenParentIsAnArray() { } @Test - public void nonExistentArrayField() { + void nonExistentArrayField() { HashMap payload = new HashMap<>(); assertThat(this.fieldProcessor.extract("a[]", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } @Test - public void nonExistentArrayFieldAsTypeDoesNotMatch() { + void nonExistentArrayFieldAsTypeDoesNotMatch() { HashMap payload = new HashMap<>(); payload.put("a", 5); assertThat(this.fieldProcessor.extract("a[]", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } @Test - public void nonExistentFieldBeneathAnArray() { + void nonExistentFieldBeneathAnArray() { HashMap payload = new HashMap<>(); HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); @@ -208,7 +208,7 @@ public void nonExistentFieldBeneathAnArray() { } @Test - public void removeTopLevelMapEntry() { + void removeTopLevelMapEntry() { Map payload = new HashMap<>(); payload.put("a", "alpha"); this.fieldProcessor.remove("a", payload); @@ -216,7 +216,7 @@ public void removeTopLevelMapEntry() { } @Test - public void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() { + void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -226,7 +226,7 @@ public void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() { } @Test - public void removeSubsectionRemovesMapWithEntries() { + void removeSubsectionRemovesMapWithEntries() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -236,7 +236,7 @@ public void removeSubsectionRemovesMapWithEntries() { } @Test - public void removeNestedMapEntry() { + void removeNestedMapEntry() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -247,7 +247,7 @@ public void removeNestedMapEntry() { @SuppressWarnings("unchecked") @Test - public void removeItemsInArray() throws IOException { + void removeItemsInArray() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); this.fieldProcessor.remove("a[].b", payload); @@ -256,7 +256,7 @@ public void removeItemsInArray() throws IOException { @SuppressWarnings("unchecked") @Test - public void removeItemsInNestedArray() throws IOException { + void removeItemsInNestedArray() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); this.fieldProcessor.remove("a[][].id", payload); @@ -265,7 +265,7 @@ public void removeItemsInNestedArray() throws IOException { @SuppressWarnings("unchecked") @Test - public void removeDoesNotRemoveArrayWithMapEntries() throws IOException { + void removeDoesNotRemoveArrayWithMapEntries() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); this.fieldProcessor.remove("a[]", payload); @@ -274,7 +274,7 @@ public void removeDoesNotRemoveArrayWithMapEntries() throws IOException { @SuppressWarnings("unchecked") @Test - public void removeDoesNotRemoveArrayWithListEntries() throws IOException { + void removeDoesNotRemoveArrayWithListEntries() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", Map.class); this.fieldProcessor.remove("a[]", payload); assertThat(payload.size()).isEqualTo(1); @@ -282,7 +282,7 @@ public void removeDoesNotRemoveArrayWithListEntries() throws IOException { @SuppressWarnings("unchecked") @Test - public void removeRemovesArrayWithOnlyScalarEntries() throws IOException { + void removeRemovesArrayWithOnlyScalarEntries() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [\"bravo\", \"charlie\"]}", Map.class); this.fieldProcessor.remove("a", payload); assertThat(payload.size()).isEqualTo(0); @@ -290,7 +290,7 @@ public void removeRemovesArrayWithOnlyScalarEntries() throws IOException { @SuppressWarnings("unchecked") @Test - public void removeSubsectionRemovesArrayWithMapEntries() throws IOException { + void removeSubsectionRemovesArrayWithMapEntries() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); this.fieldProcessor.removeSubsection("a[]", payload); @@ -299,14 +299,14 @@ public void removeSubsectionRemovesArrayWithMapEntries() throws IOException { @SuppressWarnings("unchecked") @Test - public void removeSubsectionRemovesArrayWithListEntries() throws IOException { + void removeSubsectionRemovesArrayWithListEntries() throws IOException { Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", Map.class); this.fieldProcessor.removeSubsection("a[]", payload); assertThat(payload.size()).isEqualTo(0); } @Test - public void extractNestedEntryWithDotInKeys() { + void extractNestedEntryWithDotInKeys() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a.key", alpha); @@ -316,7 +316,7 @@ public void extractNestedEntryWithDotInKeys() { @SuppressWarnings("unchecked") @Test - public void extractNestedEntriesUsingTopLevelWildcard() { + void extractNestedEntriesUsingTopLevelWildcard() { Map payload = new LinkedHashMap<>(); Map alpha = new LinkedHashMap<>(); payload.put("a", alpha); @@ -330,7 +330,7 @@ public void extractNestedEntriesUsingTopLevelWildcard() { @SuppressWarnings("unchecked") @Test - public void extractNestedEntriesUsingMidLevelWildcard() { + void extractNestedEntriesUsingMidLevelWildcard() { Map payload = new LinkedHashMap<>(); Map alpha = new LinkedHashMap<>(); payload.put("a", alpha); @@ -344,7 +344,7 @@ public void extractNestedEntriesUsingMidLevelWildcard() { @SuppressWarnings("unchecked") @Test - public void extractUsingLeafWildcardMatchingSingleItem() { + void extractUsingLeafWildcardMatchingSingleItem() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -357,7 +357,7 @@ public void extractUsingLeafWildcardMatchingSingleItem() { @SuppressWarnings("unchecked") @Test - public void extractUsingLeafWildcardMatchingMultipleItems() { + void extractUsingLeafWildcardMatchingMultipleItems() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -368,7 +368,7 @@ public void extractUsingLeafWildcardMatchingMultipleItems() { } @Test - public void removeUsingLeafWildcard() { + void removeUsingLeafWildcard() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -379,7 +379,7 @@ public void removeUsingLeafWildcard() { } @Test - public void removeUsingTopLevelWildcard() { + void removeUsingTopLevelWildcard() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); @@ -390,7 +390,7 @@ public void removeUsingTopLevelWildcard() { } @Test - public void removeUsingMidLevelWildcard() { + void removeUsingMidLevelWildcard() { Map payload = new LinkedHashMap<>(); Map alpha = new LinkedHashMap<>(); payload.put("a", alpha); @@ -407,28 +407,28 @@ public void removeUsingMidLevelWildcard() { } @Test - public void hasFieldIsTrueForNonNullFieldInMap() { + void hasFieldIsTrueForNonNullFieldInMap() { Map payload = new HashMap<>(); payload.put("a", "alpha"); assertThat(this.fieldProcessor.hasField("a", payload)).isTrue(); } @Test - public void hasFieldIsTrueForNullFieldInMap() { + void hasFieldIsTrueForNullFieldInMap() { Map payload = new HashMap<>(); payload.put("a", null); assertThat(this.fieldProcessor.hasField("a", payload)).isTrue(); } @Test - public void hasFieldIsFalseForAbsentFieldInMap() { + void hasFieldIsFalseForAbsentFieldInMap() { Map payload = new HashMap<>(); payload.put("a", null); assertThat(this.fieldProcessor.hasField("b", payload)).isFalse(); } @Test - public void hasFieldIsTrueForNeverNullFieldBeneathArray() { + void hasFieldIsTrueForNeverNullFieldBeneathArray() { Map payload = new HashMap<>(); Map nested = new HashMap<>(); nested.put("b", "bravo"); @@ -437,7 +437,7 @@ public void hasFieldIsTrueForNeverNullFieldBeneathArray() { } @Test - public void hasFieldIsTrueForAlwaysNullFieldBeneathArray() { + void hasFieldIsTrueForAlwaysNullFieldBeneathArray() { Map payload = new HashMap<>(); Map nested = new HashMap<>(); nested.put("b", null); @@ -446,7 +446,7 @@ public void hasFieldIsTrueForAlwaysNullFieldBeneathArray() { } @Test - public void hasFieldIsFalseForAlwaysAbsentFieldBeneathArray() { + void hasFieldIsFalseForAlwaysAbsentFieldBeneathArray() { Map payload = new HashMap<>(); Map nested = new HashMap<>(); nested.put("b", "bravo"); @@ -455,7 +455,7 @@ public void hasFieldIsFalseForAlwaysAbsentFieldBeneathArray() { } @Test - public void hasFieldIsFalseForOccasionallyAbsentFieldBeneathArray() { + void hasFieldIsFalseForOccasionallyAbsentFieldBeneathArray() { Map payload = new HashMap<>(); Map nested = new HashMap<>(); nested.put("b", "bravo"); @@ -464,7 +464,7 @@ public void hasFieldIsFalseForOccasionallyAbsentFieldBeneathArray() { } @Test - public void hasFieldIsFalseForOccasionallyNullFieldBeneathArray() { + void hasFieldIsFalseForOccasionallyNullFieldBeneathArray() { Map payload = new HashMap<>(); Map fieldPresent = new HashMap<>(); fieldPresent.put("b", "bravo"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java index e9bf48d81..4c469005b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.io.IOException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -29,148 +29,148 @@ * * @author Andy Wilkinson */ -public class JsonFieldTypesDiscovererTests { +class JsonFieldTypesDiscovererTests { private final JsonFieldTypesDiscoverer fieldTypeDiscoverer = new JsonFieldTypesDiscoverer(); @Test - public void arrayField() throws IOException { + void arrayField() throws IOException { assertThat(discoverFieldTypes("[]")).containsExactly(JsonFieldType.ARRAY); } @Test - public void topLevelArray() throws IOException { + void topLevelArray() throws IOException { assertThat(discoverFieldTypes("[]", "[{\"a\":\"alpha\"}]")).containsExactly(JsonFieldType.ARRAY); } @Test - public void nestedArray() throws IOException { + void nestedArray() throws IOException { assertThat(discoverFieldTypes("a[]", "{\"a\": [{\"b\":\"bravo\"}]}")).containsExactly(JsonFieldType.ARRAY); } @Test - public void arrayNestedBeneathAnArray() throws IOException { + void arrayNestedBeneathAnArray() throws IOException { assertThat(discoverFieldTypes("a[].b[]", "{\"a\": [{\"b\": [ 1, 2 ]}]}")).containsExactly(JsonFieldType.ARRAY); } @Test - public void specificFieldOfObjectInArrayNestedBeneathAnArray() throws IOException { + void specificFieldOfObjectInArrayNestedBeneathAnArray() throws IOException { assertThat(discoverFieldTypes("a[].b[].c", "{\"a\": [{\"b\": [ {\"c\": 5}, {\"c\": 5}]}]}")) .containsExactly(JsonFieldType.NUMBER); } @Test - public void booleanField() throws IOException { + void booleanField() throws IOException { assertThat(discoverFieldTypes("true")).containsExactly(JsonFieldType.BOOLEAN); } @Test - public void objectField() throws IOException { + void objectField() throws IOException { assertThat(discoverFieldTypes("{}")).containsExactly(JsonFieldType.OBJECT); } @Test - public void nullField() throws IOException { + void nullField() throws IOException { assertThat(discoverFieldTypes("null")).containsExactly(JsonFieldType.NULL); } @Test - public void numberField() throws IOException { + void numberField() throws IOException { assertThat(discoverFieldTypes("1.2345")).containsExactly(JsonFieldType.NUMBER); } @Test - public void stringField() throws IOException { + void stringField() throws IOException { assertThat(discoverFieldTypes("\"Foo\"")).containsExactly(JsonFieldType.STRING); } @Test - public void nestedField() throws IOException { + void nestedField() throws IOException { assertThat(discoverFieldTypes("a.b.c", "{\"a\":{\"b\":{\"c\":{}}}}")).containsExactly(JsonFieldType.OBJECT); } @Test - public void multipleFieldsWithSameType() throws IOException { + void multipleFieldsWithSameType() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}]}")) .containsExactly(JsonFieldType.NUMBER); } @Test - public void multipleFieldsWithDifferentTypes() throws IOException { + void multipleFieldsWithDifferentTypes() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}]}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN); } @Test - public void multipleFieldsWithDifferentTypesAndSometimesAbsent() throws IOException { + void multipleFieldsWithDifferentTypesAndSometimesAbsent() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}, {}]}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN, JsonFieldType.NULL); } @Test - public void multipleFieldsWhenSometimesAbsent() throws IOException { + void multipleFieldsWhenSometimesAbsent() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}, {}]}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.NULL); } @Test - public void multipleFieldsWhenSometimesNull() throws IOException { + void multipleFieldsWhenSometimesNull() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}, {\"id\":null}]}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.NULL); } @Test - public void multipleFieldsWithDifferentTypesAndSometimesNull() throws IOException { + void multipleFieldsWithDifferentTypesAndSometimesNull() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}, {\"id\":null}]}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN, JsonFieldType.NULL); } @Test - public void multipleFieldsWhenEitherNullOrAbsent() throws IOException { + void multipleFieldsWhenEitherNullOrAbsent() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{},{\"id\":null}]}")) .containsExactlyInAnyOrder(JsonFieldType.NULL); } @Test - public void multipleFieldsThatAreAllNull() throws IOException { + void multipleFieldsThatAreAllNull() throws IOException { assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":null},{\"id\":null}]}")) .containsExactlyInAnyOrder(JsonFieldType.NULL); } @Test - public void nonExistentSingleFieldProducesFieldDoesNotExistException() { + void nonExistentSingleFieldProducesFieldDoesNotExistException() { assertThatExceptionOfType(FieldDoesNotExistException.class) .isThrownBy(() -> discoverFieldTypes("a.b", "{\"a\":{}}")) .withMessage("The payload does not contain a field with the path 'a.b'"); } @Test - public void nonExistentMultipleFieldsProducesFieldDoesNotExistException() { + void nonExistentMultipleFieldsProducesFieldDoesNotExistException() { assertThatExceptionOfType(FieldDoesNotExistException.class) .isThrownBy(() -> discoverFieldTypes("a[].b", "{\"a\":[{\"c\":1},{\"c\":2}]}")) .withMessage("The payload does not contain a field with the path 'a[].b'"); } @Test - public void leafWildcardWithCommonType() throws IOException { + void leafWildcardWithCommonType() throws IOException { assertThat(discoverFieldTypes("a.*", "{\"a\": {\"b\": 5, \"c\": 6}}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER); } @Test - public void leafWildcardWithVaryingType() throws IOException { + void leafWildcardWithVaryingType() throws IOException { assertThat(discoverFieldTypes("a.*", "{\"a\": {\"b\": 5, \"c\": \"six\"}}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.STRING); } @Test - public void intermediateWildcardWithCommonType() throws IOException { + void intermediateWildcardWithCommonType() throws IOException { assertThat(discoverFieldTypes("a.*.d", "{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": 5}}}}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER); } @Test - public void intermediateWildcardWithVaryingType() throws IOException { + void intermediateWildcardWithVaryingType() throws IOException { assertThat(discoverFieldTypes("a.*.d", "{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": \"four\"}}}}")) .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.STRING); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java index 0828262f8..1d2a869d4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.EnumSet; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -27,32 +27,32 @@ * * @author Andy Wilkinson */ -public class JsonFieldTypesTests { +class JsonFieldTypesTests { @Test - public void singleTypeCoalescesToThatType() { + void singleTypeCoalescesToThatType() { assertThat(new JsonFieldTypes(JsonFieldType.NUMBER).coalesce(false)).isEqualTo(JsonFieldType.NUMBER); } @Test - public void singleTypeCoalescesToThatTypeWhenOptional() { + void singleTypeCoalescesToThatTypeWhenOptional() { assertThat(new JsonFieldTypes(JsonFieldType.NUMBER).coalesce(true)).isEqualTo(JsonFieldType.NUMBER); } @Test - public void multipleTypesCoalescesToVaries() { + void multipleTypesCoalescesToVaries() { assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NUMBER)).coalesce(false)) .isEqualTo(JsonFieldType.VARIES); } @Test - public void nullAndNonNullTypesCoalescesToVaries() { + void nullAndNonNullTypesCoalescesToVaries() { assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NULL)).coalesce(false)) .isEqualTo(JsonFieldType.VARIES); } @Test - public void nullAndNonNullTypesCoalescesToNonNullTypeWhenOptional() { + void nullAndNonNullTypesCoalescesToNonNullTypeWhenOptional() { assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NULL)).coalesce(true)) .isEqualTo(JsonFieldType.ARRAY); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index 7c57c3c19..11b6df5fa 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.payload.PayloadDocumentation.applyPathPrefix; @@ -31,10 +31,10 @@ * * @author Andy Wilkinson */ -public class PayloadDocumentationTests { +class PayloadDocumentationTests { @Test - public void applyPathPrefixAppliesPrefixToDescriptorPaths() { + void applyPathPrefixAppliesPrefixToDescriptorPaths() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo"), fieldWithPath("charlie"))); assertThat(descriptors.size()).isEqualTo(2); @@ -42,21 +42,21 @@ public void applyPathPrefixAppliesPrefixToDescriptorPaths() { } @Test - public void applyPathPrefixCopiesIgnored() { + void applyPathPrefixCopiesIgnored() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").ignored())); assertThat(descriptors.size()).isEqualTo(1); assertThat(descriptors.get(0).isIgnored()).isTrue(); } @Test - public void applyPathPrefixCopiesOptional() { + void applyPathPrefixCopiesOptional() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").optional())); assertThat(descriptors.size()).isEqualTo(1); assertThat(descriptors.get(0).isOptional()).isTrue(); } @Test - public void applyPathPrefixCopiesDescription() { + void applyPathPrefixCopiesDescription() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").description("Some field"))); assertThat(descriptors.size()).isEqualTo(1); @@ -64,7 +64,7 @@ public void applyPathPrefixCopiesDescription() { } @Test - public void applyPathPrefixCopiesType() { + void applyPathPrefixCopiesType() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").type(JsonFieldType.OBJECT))); assertThat(descriptors.size()).isEqualTo(1); @@ -72,7 +72,7 @@ public void applyPathPrefixCopiesType() { } @Test - public void applyPathPrefixCopiesAttributes() { + void applyPathPrefixCopiesAttributes() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").attributes(key("a").value("alpha"), key("b").value("bravo")))); assertThat(descriptors.size()).isEqualTo(1); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java index 67e25f57c..f9c94dbd9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,14 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.snippet.Attributes.attributes; @@ -41,89 +36,84 @@ * * @author Andy Wilkinson */ -public class RequestBodyPartSnippetTests extends AbstractSnippetTests { - - public RequestBodyPartSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestBodyPartSnippetTests { - @Test - public void requestPartWithBody() throws IOException { + @RenderedSnippetTest + void requestPartWithBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { requestPartBody("one") - .document(this.operationBuilder.request("http://localhost/").part("one", "some content".getBytes()).build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock(null, "nowrap").withContent("some content")); + .document(operationBuilder.request("http://localhost/").part("one", "some content".getBytes()).build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("some content")); } - @Test - public void requestPartWithNoBody() throws IOException { - requestPartBody("one") - .document(this.operationBuilder.request("http://localhost/").part("one", new byte[0]).build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock(null, "nowrap").withContent("")); + @RenderedSnippetTest + void requestPartWithNoBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost/").part("one", new byte[0]).build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("")); } - @Test - public void requestPartWithJsonMediaType() throws IOException { - requestPartBody("one").document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestPartWithJsonMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost/") .part("one", "".getBytes()) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock("json", "nowrap").withContent("")); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); } - @Test - public void requestPartWithJsonSubtypeMediaType() throws IOException { - requestPartBody("one").document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestPartWithJsonSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost/") .part("one", "".getBytes()) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock("json", "nowrap").withContent("")); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); } - @Test - public void requestPartWithXmlMediaType() throws IOException { - requestPartBody("one").document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestPartWithXmlMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost/") .part("one", "".getBytes()) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock("xml", "nowrap").withContent("")); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); } - @Test - public void requestPartWithXmlSubtypeMediaType() throws IOException { - requestPartBody("one").document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestPartWithXmlSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost/") .part("one", "".getBytes()) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock("xml", "nowrap").withContent("")); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); } - @Test - public void subsectionOfRequestPartBody() throws IOException { - requestPartBody("one", beneathPath("a.b")).document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void subsectionOfRequestPartBody(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one", beneathPath("a.b")).document(operationBuilder.request("http://localhost/") .part("one", "{\"a\":{\"b\":{\"c\":5}}}".getBytes()) .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body-beneath-a.b")) - .is(codeBlock(null, "nowrap").withContent("{\"c\":5}")); + assertThat(snippets.requestPartBody("one", "beneath-a.b")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("{\"c\":5}")); } - @Test - public void customSnippetAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-part-body")) - .willReturn(snippetResource("request-part-body-with-language")); - requestPartBody("one", attributes(key("language").value("json"))).document( - this.operationBuilder.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .part("one", "{\"a\":\"alpha\"}".getBytes()) - .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-body")) - .is(codeBlock("json", "nowrap").withContent("{\"a\":\"alpha\"}")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-part-body", template = "request-part-body-with-language") + void customSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestPartBody("one", attributes(key("language").value("json"))) + .document(operationBuilder.request("http://localhost/").part("one", "{\"a\":\"alpha\"}".getBytes()).build()); + assertThat(snippets.requestPartBody("one")).isCodeBlock( + (codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("{\"a\":\"alpha\"}")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java index 47ab24b40..c332fdcc7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,14 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestBody; import static org.springframework.restdocs.snippet.Attributes.attributes; @@ -41,77 +36,74 @@ * * @author Andy Wilkinson */ -public class RequestBodySnippetTests extends AbstractSnippetTests { - - public RequestBodySnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestBodySnippetTests { - @Test - public void requestWithBody() throws IOException { - requestBody().document(this.operationBuilder.request("http://localhost/").content("some content").build()); - assertThat(this.generatedSnippets.snippet("request-body")) - .is(codeBlock(null, "nowrap").withContent("some content")); + @RenderedSnippetTest + void requestWithBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost/").content("some content").build()); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("some content")); } - @Test - public void requestWithNoBody() throws IOException { - requestBody().document(this.operationBuilder.request("http://localhost/").build()); - assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock(null, "nowrap").withContent("")); + @RenderedSnippetTest + void requestWithNoBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost/").build()); + assertThat(snippets.requestBody()).isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("")); } - @Test - public void requestWithJsonMediaType() throws IOException { - requestBody().document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestWithJsonMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost/") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("json", "nowrap").withContent("")); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); } - @Test - public void requestWithJsonSubtypeMediaType() throws IOException { - requestBody().document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestWithJsonSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestBody().document(operationBuilder.request("http://localhost/") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("json", "nowrap").withContent("")); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); } - @Test - public void requestWithXmlMediaType() throws IOException { - requestBody().document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestWithXmlMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost/") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("xml", "nowrap").withContent("")); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); } - @Test - public void requestWithXmlSubtypeMediaType() throws IOException { - requestBody().document(this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void requestWithXmlSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestBody().document(operationBuilder.request("http://localhost/") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("xml", "nowrap").withContent("")); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); } - @Test - public void subsectionOfRequestBody() throws IOException { + @RenderedSnippetTest + void subsectionOfRequestBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { requestBody(beneathPath("a.b")) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\":{\"b\":{\"c\":5}}}").build()); - assertThat(this.generatedSnippets.snippet("request-body-beneath-a.b")) - .is(codeBlock(null, "nowrap").withContent("{\"c\":5}")); + .document(operationBuilder.request("http://localhost/").content("{\"a\":{\"b\":{\"c\":5}}}").build()); + assertThat(snippets.requestBody("beneath-a.b")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("{\"c\":5}")); } - @Test - public void customSnippetAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-body")) - .willReturn(snippetResource("request-body-with-language")); - requestBody(attributes(key("language").value("json"))).document( - this.operationBuilder.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .content("{\"a\":\"alpha\"}") - .build()); - assertThat(this.generatedSnippets.snippet("request-body")) - .is(codeBlock("json", "nowrap").withContent("{\"a\":\"alpha\"}")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-body", template = "request-body-with-language") + void customSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody(attributes(key("language").value("json"))) + .document(operationBuilder.request("http://localhost/").content("{\"a\":\"alpha\"}").build()); + assertThat(snippets.requestBody()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("{\"a\":\"alpha\"}")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java deleted file mode 100644 index 1eb5a5b9f..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.payload; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; - -/** - * Tests for failures when rendering {@link RequestFieldsSnippet} due to missing or - * undocumented fields. - * - * @author Andy Wilkinson - */ -public class RequestFieldsSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedRequestField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5}").build())) - .withMessageStartingWith("The following parts of the payload were not documented:"); - } - - @Test - public void missingRequestField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost/").content("{}").build())) - .withMessage("Fields with the following paths were not found in the payload: [a.b]"); - } - - @Test - public void missingOptionalRequestFieldWithNoTypeProvided() { - assertThatExceptionOfType(FieldTypeRequiredException.class).isThrownBy( - () -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").optional())) - .document(this.operationBuilder.request("http://localhost/").content("{ }").build())); - } - - @Test - public void undocumentedRequestFieldAndMissingRequestField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost/").content("{ \"a\": { \"c\": 5 }}").build())) - .withMessageStartingWith("The following parts of the payload were not documented:") - .withMessageEndingWith("Fields with the following paths were not found in the payload: [a.b]"); - } - - @Test - public void attemptToDocumentFieldsWithNoRequestBody() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").build())) - .withMessage("Cannot document request fields as the request body is empty"); - } - - @Test - public void fieldWithExplicitTypeThatDoesNotMatchThePayload() { - assertThatExceptionOfType(FieldTypesDoNotMatchException.class) - .isThrownBy(() -> new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.request("http://localhost/").content("{ \"a\": 5 }").build())) - .withMessage("The documented type of the field 'a' is Object but the actual type is Number"); - } - - @Test - public void fieldWithExplicitSpecificTypeThatActuallyVaries() { - assertThatExceptionOfType(FieldTypesDoNotMatchException.class) - .isThrownBy(() -> new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.request("http://localhost/") - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") - .build())) - .withMessage("The documented type of the field '[].a' is Object but the actual type is Varies"); - } - - @Test - public void undocumentedXmlRequestField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost/") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessageStartingWith("The following parts of the payload were not documented:"); - } - - @Test - public void xmlDescendentsAreNotDocumentedByFieldDescriptor() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").type("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessageStartingWith("The following parts of the payload were not documented:"); - } - - @Test - public void xmlRequestFieldWithNoType() { - assertThatExceptionOfType(FieldTypeRequiredException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())); - } - - @Test - public void missingXmlRequestField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/") - .content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessage("Fields with the following paths were not found in the payload: [a/b]"); - } - - @Test - public void undocumentedXmlRequestFieldAndMissingXmlRequestField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(this.operationBuilder.request("http://localhost/") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessageStartingWith("The following parts of the payload were not documented:") - .withMessageEndingWith("Fields with the following paths were not found in the payload: [a/b]"); - } - - @Test - public void unsupportedContent() { - assertThatExceptionOfType(PayloadHandlingException.class) - .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost/") - .content("Some plain text") - .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) - .build())) - .withMessage("Cannot handle text/plain content as it could not be parsed as JSON or XML"); - } - - @Test - public void nonOptionalFieldBeneathArrayThatIsSometimesNull() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), - fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.request("http://localhost/") - .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"b\": null, \"c\": 2}," + " {\"b\": 1,\"c\": 2}]}") - .build())) - .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); - } - - @Test - public void nonOptionalFieldBeneathArrayThatIsSometimesAbsent() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), - fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.request("http://localhost/") - .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") - .build())) - .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index bb6ff7b42..7c6394d96 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,21 +18,19 @@ import java.io.IOException; import java.util.Arrays; - -import org.junit.Test; +import java.util.Collections; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; @@ -46,342 +44,492 @@ * @author Andy Wilkinson * @author Sungjun Lee */ -public class RequestFieldsSnippetTests extends AbstractSnippetTests { - - public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestFieldsSnippetTests { - @Test - public void mapRequestWithFields() throws IOException { + @RenderedSnippetTest + void mapRequestWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one") - .row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); } - @Test - public void mapRequestWithNullField() throws IOException { + @RenderedSnippetTest + void mapRequestWithNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": {\"b\": null}}").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Null`", "one")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": {\"b\": null}}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`Null`", "one")); } - @Test - public void entireSubsectionsCanBeDocumented() throws IOException { + @RenderedSnippetTest + void entireSubsectionsCanBeDocumented(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Object`", "one")); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Object`", "one")); } - @Test - public void subsectionOfMapRequest() throws IOException { + @RenderedSnippetTest + void subsectionOfMapRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { requestFields(beneathPath("a"), fieldWithPath("b").description("one"), fieldWithPath("c").description("two")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); - assertThat(this.generatedSnippets.snippet("request-fields-beneath-a")) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "one") + assertThat(snippets.requestFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") .row("`c`", "`String`", "two")); } - @Test - public void subsectionOfMapRequestWithCommonPrefix() throws IOException { + @RenderedSnippetTest + void subsectionOfMapRequestWithCommonPrefix(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { requestFields(beneathPath("a")).andWithPrefix("b.", fieldWithPath("c").description("two")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": {\"c\": \"charlie\"}}}") .build()); - assertThat(this.generatedSnippets.snippet("request-fields-beneath-a")) - .is(tableWithHeader("Path", "Type", "Description").row("`b.c`", "`String`", "two")); + assertThat(snippets.requestFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b.c`", "`String`", "two")); } - @Test - public void arrayRequestWithFields() throws IOException { + @RenderedSnippetTest + void arrayRequestWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet( Arrays.asList(fieldWithPath("[]").description("one"), fieldWithPath("[]a.b").description("two"), fieldWithPath("[]a.c").description("three"), fieldWithPath("[]a").description("four"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("[{\"a\": {\"b\": 5, \"c\":\"charlie\"}}," + "{\"a\": {\"b\": 4, \"c\":\"chalk\"}}]") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`[]`", "`Array`", "one") - .row("`[]a.b`", "`Number`", "two") - .row("`[]a.c`", "`String`", "three") - .row("`[]a`", "`Object`", "four")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`[]`", "`Array`", "one") + .row("`[]a.b`", "`Number`", "two") + .row("`[]a.c`", "`String`", "three") + .row("`[]a`", "`Object`", "four")); } - @Test - public void arrayRequestWithAlwaysNullField() throws IOException { + @RenderedSnippetTest + void arrayRequestWithAlwaysNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("[{\"a\": {\"b\": null}}," + "{\"a\": {\"b\": null}}]") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`[]a.b`", "`Null`", "one")); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`[]a.b`", "`Null`", "one")); } - @Test - public void subsectionOfArrayRequest() throws IOException { + @RenderedSnippetTest + void subsectionOfArrayRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { requestFields(beneathPath("[].a"), fieldWithPath("b").description("one"), fieldWithPath("c").description("two")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("[{\"a\": {\"b\": 5, \"c\": \"charlie\"}}]") .build()); - assertThat(this.generatedSnippets.snippet("request-fields-beneath-[].a")) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "one") + assertThat(snippets.requestFields("beneath-[].a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") .row("`c`", "`String`", "two")); } - @Test - public void ignoredRequestField() throws IOException { + @RenderedSnippetTest + void ignoredRequestField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5, \"b\": 4}").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); } - @Test - public void entireSubsectionCanBeIgnored() throws IOException { + @RenderedSnippetTest + void entireSubsectionCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet( Arrays.asList(subsectionWithPath("a").ignored(), fieldWithPath("c").description("Field c"))) - .document( - this.operationBuilder.request("http://localhost/").content("{\"a\": {\"b\": 5}, \"c\": 4}").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`c`", "`Number`", "Field c")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": {\"b\": 5}, \"c\": 4}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`c`", "`Number`", "Field c")); } - @Test - public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedRequestFieldsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5, \"b\": 4}").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); } - @Test - public void allUndocumentedFieldsContinueToBeIgnoredAfterAddingDescriptors() throws IOException { + @RenderedSnippetTest + void allUndocumentedFieldsContinueToBeIgnoredAfterAddingDescriptors(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) .andWithPrefix("c.", fieldWithPath("d").description("Field d")) - .document(this.operationBuilder.request("http://localhost/") - .content("{\"a\":5,\"b\":4,\"c\":{\"d\": 3}}") - .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b") - .row("`c.d`", "`Number`", "Field d")); + .document( + operationBuilder.request("http://localhost/").content("{\"a\":5,\"b\":4,\"c\":{\"d\": 3}}").build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "Field b") + .row("`c.d`", "`Number`", "Field d")); } - @Test - public void missingOptionalRequestField() throws IOException { + @RenderedSnippetTest + void missingOptionalRequestField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.request("http://localhost/").content("{}").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + .document(operationBuilder.request("http://localhost/").content("{}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); } - @Test - public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() throws IOException { + @RenderedSnippetTest + void missingIgnoredOptionalRequestFieldDoesNotRequireAType(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(this.operationBuilder.request("http://localhost/").content("{}").build()); - assertThat(this.generatedSnippets.requestFields()).is(tableWithHeader("Path", "Type", "Description")); + .document(operationBuilder.request("http://localhost/").content("{}").build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description")); } - @Test - public void presentOptionalRequestField() throws IOException { + @RenderedSnippetTest + void presentOptionalRequestField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) - .document( - this.operationBuilder.request("http://localhost/").content("{\"a\": { \"b\": \"bravo\"}}").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": { \"b\": \"bravo\"}}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); } - @Test - public void requestFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")) - .willReturn(snippetResource("request-fields-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-fields", template = "request-fields-with-title") + void requestFieldsWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes(key("title").value("Custom title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .content("{\"a\": \"foo\"}") - .build()); - assertThat(this.generatedSnippets.requestFields()).contains("Custom title"); + .document(operationBuilder.request("http://localhost/").content("{\"a\": \"foo\"}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withTitleAndHeader("Custom title", "Path", "Type", "Description") + .row("a", "String", "one")); } - @Test - public void requestFieldsWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")) - .willReturn(snippetResource("request-fields-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-fields", template = "request-fields-with-extra-column") + void requestFieldsWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").attributes(key("foo").value("alpha")), fieldWithPath("a.c").description("two").attributes(key("foo").value("bravo")), fieldWithPath("a").description("three").attributes(key("foo").value("charlie")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description", "Foo").row("a.b", "Number", "one", "alpha") - .row("a.c", "String", "two", "bravo") - .row("a", "Object", "three", "charlie")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); } - @Test - public void fieldWithExplicitExactlyMatchingType() throws IOException { + @RenderedSnippetTest + void fieldWithExplicitExactlyMatchingType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5 }").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": 5 }").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); } - @Test - public void fieldWithExplicitVariesType() throws IOException { + @RenderedSnippetTest + void fieldWithExplicitVariesType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": 5 }").build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); + .document(operationBuilder.request("http://localhost/").content("{\"a\": 5 }").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); } - @Test - public void applicationXmlRequestFields() throws IOException { - xmlRequestFields(MediaType.APPLICATION_XML); + @RenderedSnippetTest + void applicationXmlRequestFields(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + xmlRequestFields(MediaType.APPLICATION_XML, operationBuilder, snippets); } - @Test - public void textXmlRequestFields() throws IOException { - xmlRequestFields(MediaType.TEXT_XML); + @RenderedSnippetTest + void textXmlRequestFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlRequestFields(MediaType.TEXT_XML, operationBuilder, snippets); } - @Test - public void customXmlRequestFields() throws IOException { - xmlRequestFields(MediaType.parseMediaType("application/vnd.com.example+xml")); + @RenderedSnippetTest + void customXmlRequestFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlRequestFields(MediaType.parseMediaType("application/vnd.com.example+xml"), operationBuilder, snippets); } - private void xmlRequestFields(MediaType contentType) throws IOException { + private void xmlRequestFields(MediaType contentType, OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one").type("b"), fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, contentType.toString()) .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a/b`", "`b`", "one") - .row("`a/c`", "`c`", "two") - .row("`a`", "`a`", "three")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a/b`", "`b`", "one") + .row("`a/c`", "`c`", "two") + .row("`a`", "`a`", "three")); } - @Test - public void entireSubsectionOfXmlPayloadCanBeDocumented() throws IOException { + @RenderedSnippetTest + void entireSubsectionOfXmlPayloadCanBeDocumented(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").description("one").type("a"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { PayloadDocumentation .requestFields(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) .and(fieldWithPath("a").description("three")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one") - .row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); } - @Test - public void prefixedAdditionalDescriptors() throws IOException { + @RenderedSnippetTest + void prefixedAdditionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Object`", "one") - .row("`a.b`", "`Number`", "two") - .row("`a.c`", "`String`", "three")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one") + .row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); } - @Test - public void requestWithFieldsWithEscapedContent() throws IOException { + @RenderedSnippetTest + void requestWithFieldsWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(this.operationBuilder.request("http://localhost/").content("{\"Foo|Bar\": 5}").build()); - assertThat(this.generatedSnippets.requestFields()).is(tableWithHeader("Path", "Type", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); + .document(operationBuilder.request("http://localhost/").content("{\"Foo|Bar\": 5}").build()); + assertThat(snippets.requestFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`Foo|Bar`", "`one|two`", "three|four")); } - @Test - public void mapRequestWithVaryingKeysMatchedUsingWildcard() throws IOException { + @RenderedSnippetTest + void mapRequestWithVaryingKeysMatchedUsingWildcard(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("things.*.size").description("one"), fieldWithPath("things.*.type").description("two"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"things\": {\"12abf\": {\"type\":" + "\"Whale\", \"size\": \"HUGE\"}," + "\"gzM33\" : {\"type\": \"Screw\"," + "\"size\": \"SMALL\"}}}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`things.*.size`", "`String`", "one") - .row("`things.*.type`", "`String`", "two")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`things.*.size`", "`String`", "one") + .row("`things.*.type`", "`String`", "two")); } - @Test - public void requestWithArrayContainingFieldThatIsSometimesNull() throws IOException { + @RenderedSnippetTest + void requestWithArrayContainingFieldThatIsSometimesNull(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new RequestFieldsSnippet( Arrays.asList(fieldWithPath("assets[].name").description("one").type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"assets\": [" + "{\"name\": \"sample1\"}, " + "{\"name\": null}, " + "{\"name\": \"sample2\"}]}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`assets[].name`", "`String`", "one")); + assertThat(snippets.requestFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`assets[].name`", "`String`", "one")); } - @Test - public void optionalFieldBeneathArrayThatIsSometimesAbsent() throws IOException { + @RenderedSnippetTest + void optionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER).optional(), fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") .build()); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a[].b`", "`Number`", "one") - .row("`a[].c`", "`Number`", "two")); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a[].b`", "`Number`", "one") + .row("`a[].c`", "`Number`", "two")); } - @Test - public void typeDeterminationDoesNotSetTypeOnDescriptor() throws IOException { + @RenderedSnippetTest + void typeDeterminationDoesNotSetTypeOnDescriptor(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { FieldDescriptor descriptor = fieldWithPath("a.b").description("one"); new RequestFieldsSnippet(Arrays.asList(descriptor)) - .document(this.operationBuilder.request("http://localhost/").content("{\"a\": {\"b\": 5}}").build()); + .document(operationBuilder.request("http://localhost/").content("{\"a\": {\"b\": 5}}").build()); assertThat(descriptor.getType()).isNull(); - assertThat(this.generatedSnippets.requestFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one")); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one")); + } + + @SnippetTest + void undocumentedRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost/").content("{\"a\": 5}").build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost/").content("{}").build())) + .withMessage("Fields with the following paths were not found in the payload: [a.b]"); + } + + @SnippetTest + void missingOptionalRequestFieldWithNoTypeProvided(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypeRequiredException.class).isThrownBy( + () -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").optional())) + .document(operationBuilder.request("http://localhost/").content("{ }").build())); + } + + @SnippetTest + void undocumentedRequestFieldAndMissingRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost/").content("{ \"a\": { \"c\": 5 }}").build())) + .withMessageStartingWith("The following parts of the payload were not documented:") + .withMessageEndingWith("Fields with the following paths were not found in the payload: [a.b]"); + } + + @SnippetTest + void attemptToDocumentFieldsWithNoRequestBody(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost/").build())) + .withMessage("Cannot document request fields as the request body is empty"); + } + + @SnippetTest + void fieldWithExplicitTypeThatDoesNotMatchThePayload(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.request("http://localhost/").content("{ \"a\": 5 }").build())) + .withMessage("The documented type of the field 'a' is Object but the actual type is Number"); + } + + @SnippetTest + void fieldWithExplicitSpecificTypeThatActuallyVaries(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class).isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.request("http://localhost/").content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build())) + .withMessage("The documented type of the field '[].a' is Object but the actual type is Varies"); + } + + @SnippetTest + void undocumentedXmlRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost/") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void xmlDescendentsAreNotDocumentedByFieldDescriptor(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").type("a").description("one"))) + .document(operationBuilder.request("http://localhost/") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void xmlRequestFieldWithNoType(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypeRequiredException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost/") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())); + } + + @SnippetTest + void missingXmlRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost/") + .content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void undocumentedXmlRequestFieldAndMissingXmlRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(operationBuilder.request("http://localhost/") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:") + .withMessageEndingWith("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void unsupportedContent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost/") + .content("Some plain text") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) + .build())) + .withMessage("Cannot handle text/plain content as it could not be parsed as JSON or XML"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesNull(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.request("http://localhost/") + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"b\": null, \"c\": 2}," + " {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.request("http://localhost/") + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java deleted file mode 100644 index 52f1cead0..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.payload; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; - -/** - * Tests for failures when rendering {@link RequestPartFieldsSnippet} due to missing or - * undocumented fields. - * - * @author Mathieu Pousse - * @author Andy Wilkinson - */ -public class RequestPartFieldsSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedRequestPartField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestPartFieldsSnippet("part", Collections.emptyList()).document( - this.operationBuilder.request("http://localhost/").part("part", "{\"a\": 5}".getBytes()).build())) - .withMessageStartingWith("The following parts of the payload were not documented:"); - } - - @Test - public void missingRequestPartField() { - assertThatExceptionOfType(SnippetException.class).isThrownBy(() -> new RequestPartFieldsSnippet("part", - Arrays.asList(fieldWithPath("b").description("one"))) - .document(this.operationBuilder.request("http://localhost/").part("part", "{\"a\": 5}".getBytes()).build())) - .withMessageStartingWith("The following parts of the payload were not documented:"); - } - - @Test - public void missingRequestPart() { - assertThatExceptionOfType(SnippetException.class).isThrownBy( - () -> new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost/") - .part("part", "{\"a\": {\"b\": 5}}".getBytes()) - .build())) - .withMessage("A request part named 'another' was not found in the request"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java index 7967bc7d5..f4924d752 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,15 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -36,86 +38,110 @@ * @author Mathieu Pousse * @author Andy Wilkinson */ -public class RequestPartFieldsSnippetTests extends AbstractSnippetTests { - - public RequestPartFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +public class RequestPartFieldsSnippetTests { - @Test - public void mapRequestPartFields() throws IOException { + @RenderedSnippetTest + void mapRequestPartFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestPartFieldsSnippet("one", Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) .build()); - assertThat(this.generatedSnippets.requestPartFields("one")) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one") - .row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); + assertThat(snippets.requestPartFields("one")).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); } - @Test - public void mapRequestPartSubsectionFields() throws IOException { + @RenderedSnippetTest + void mapRequestPartSubsectionFields(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartFieldsSnippet("one", beneathPath("a"), Arrays.asList(fieldWithPath("b").description("one"), fieldWithPath("c").description("two"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) .build()); - assertThat(this.generatedSnippets.snippet("request-part-one-fields-beneath-a")) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "one") + assertThat(snippets.requestPartFields("one", "beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") .row("`c`", "`String`", "two")); } - @Test - public void multipleRequestParts() throws IOException { - Operation operation = this.operationBuilder.request("http://localhost/") + @RenderedSnippetTest + void multipleRequestParts(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + Operation operation = operationBuilder.request("http://localhost/") .part("one", "{}".getBytes()) .and() .part("two", "{}".getBytes()) .build(); new RequestPartFieldsSnippet("one", Collections.emptyList()).document(operation); new RequestPartFieldsSnippet("two", Collections.emptyList()).document(operation); - assertThat(this.generatedSnippets.requestPartFields("one")).isNotNull(); - assertThat(this.generatedSnippets.requestPartFields("two")).isNotNull(); + assertThat(snippets.requestPartFields("one")).isNotNull(); + assertThat(snippets.requestPartFields("two")).isNotNull(); } - @Test - public void allUndocumentedRequestPartFieldsCanBeIgnored() throws IOException { - new RequestPartFieldsSnippet("one", Arrays.asList(fieldWithPath("b").description("Field b")), true) - .document(this.operationBuilder.request("http://localhost/") - .part("one", "{\"a\": 5, \"b\": 4}".getBytes()) - .build()); - assertThat(this.generatedSnippets.requestPartFields("one")) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + @RenderedSnippetTest + void allUndocumentedRequestPartFieldsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestPartFieldsSnippet("one", Arrays.asList(fieldWithPath("b").description("Field b")), true).document( + operationBuilder.request("http://localhost/").part("one", "{\"a\": 5, \"b\": 4}".getBytes()).build()); + assertThat(snippets.requestPartFields("one")) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { PayloadDocumentation .requestPartFields("one", fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) .and(fieldWithPath("a").description("three")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) .build()); - assertThat(this.generatedSnippets.requestPartFields("one")) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one") - .row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); + assertThat(snippets.requestPartFields("one")).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); } - @Test - public void prefixedAdditionalDescriptors() throws IOException { + @RenderedSnippetTest + void prefixedAdditionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { PayloadDocumentation.requestPartFields("one", fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) .build()); - assertThat(this.generatedSnippets.requestPartFields("one")) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Object`", "one") - .row("`a.b`", "`Number`", "two") - .row("`a.c`", "`String`", "three")); + assertThat(snippets.requestPartFields("one")).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one") + .row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + } + + @SnippetTest + void undocumentedRequestPartField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartFieldsSnippet("part", Collections.emptyList()) + .document(operationBuilder.request("http://localhost/").part("part", "{\"a\": 5}".getBytes()).build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingRequestPartField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("b").description("one"))) + .document(operationBuilder.request("http://localhost/").part("part", "{\"a\": 5}".getBytes()).build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingRequestPart(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class).isThrownBy( + () -> new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost/") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()) + .build())) + .withMessage("A request part named 'another' was not found in the request"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java index ee4f34b83..74c659954 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,14 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.snippet.Attributes.attributes; @@ -41,77 +36,72 @@ * * @author Andy Wilkinson */ -public class ResponseBodySnippetTests extends AbstractSnippetTests { +class ResponseBodySnippetTests { - public ResponseBodySnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); + @RenderedSnippetTest + void responseWithBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document(operationBuilder.response().content("some content").build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("some content")); } - @Test - public void responseWithBody() throws IOException { - new ResponseBodySnippet().document(this.operationBuilder.response().content("some content").build()); - assertThat(this.generatedSnippets.snippet("response-body")) - .is(codeBlock(null, "nowrap").withContent("some content")); + @RenderedSnippetTest + void responseWithNoBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document(operationBuilder.response().build()); + assertThat(snippets.responseBody()).isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("")); } - @Test - public void responseWithNoBody() throws IOException { - new ResponseBodySnippet().document(this.operationBuilder.response().build()); - assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock(null, "nowrap").withContent("")); + @RenderedSnippetTest + void responseWithJsonMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document( + operationBuilder.response().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); } - @Test - public void responseWithJsonMediaType() throws IOException { - new ResponseBodySnippet().document(this.operationBuilder.response() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .build()); - assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("json", "nowrap").withContent("")); - } - - @Test - public void responseWithJsonSubtypeMediaType() throws IOException { - new ResponseBodySnippet().document(this.operationBuilder.response() + @RenderedSnippetTest + void responseWithJsonSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseBodySnippet().document(operationBuilder.response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("json", "nowrap").withContent("")); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); } - @Test - public void responseWithXmlMediaType() throws IOException { - new ResponseBodySnippet().document(this.operationBuilder.response() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("xml", "nowrap").withContent("")); + @RenderedSnippetTest + void responseWithXmlMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document( + operationBuilder.response().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); } - @Test - public void responseWithXmlSubtypeMediaType() throws IOException { - new ResponseBodySnippet().document(this.operationBuilder.response() + @RenderedSnippetTest + void responseWithXmlSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseBodySnippet().document(operationBuilder.response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE) .build()); - assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("xml", "nowrap").withContent("")); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); } - @Test - public void subsectionOfResponseBody() throws IOException { + @RenderedSnippetTest + void subsectionOfResponseBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { responseBody(beneathPath("a.b")) - .document(this.operationBuilder.response().content("{\"a\":{\"b\":{\"c\":5}}}").build()); - assertThat(this.generatedSnippets.snippet("response-body-beneath-a.b")) - .is(codeBlock(null, "nowrap").withContent("{\"c\":5}")); + .document(operationBuilder.response().content("{\"a\":{\"b\":{\"c\":5}}}").build()); + assertThat(snippets.responseBody("beneath-a.b")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("{\"c\":5}")); } - @Test - public void customSnippetAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-body")) - .willReturn(snippetResource("response-body-with-language")); - new ResponseBodySnippet(attributes(key("language").value("json"))).document( - this.operationBuilder.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() - .content("{\"a\":\"alpha\"}") - .build()); - assertThat(this.generatedSnippets.snippet("response-body")) - .is(codeBlock("json", "nowrap").withContent("{\"a\":\"alpha\"}")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-body", template = "response-body-with-language") + void customSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet(attributes(key("language").value("json"))) + .document(operationBuilder.response().content("{\"a\":\"alpha\"}").build()); + assertThat(snippets.responseBody()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("{\"a\":\"alpha\"}")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java deleted file mode 100644 index 3e41d9fec..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.payload; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; - -/** - * Tests for failures when rendering {@link ResponseFieldsSnippet} due to missing or - * undocumented fields. - * - * @author Andy Wilkinson - */ -public class ResponseFieldsSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void attemptToDocumentFieldsWithNoResponseBody() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.build())) - .withMessage("Cannot document response fields as the response body is empty"); - } - - @Test - public void fieldWithExplicitTypeThatDoesNotMatchThePayload() { - assertThatExceptionOfType(FieldTypesDoNotMatchException.class) - .isThrownBy(() -> new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.response().content("{ \"a\": 5 }}").build())) - .withMessage("The documented type of the field 'a' is Object but the actual type is Number"); - } - - @Test - public void fieldWithExplicitSpecificTypeThatActuallyVaries() { - assertThatExceptionOfType(FieldTypesDoNotMatchException.class) - .isThrownBy(() -> new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.response().content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build())) - .withMessage("The documented type of the field '[].a' is Object but the actual type is Varies"); - } - - @Test - public void undocumentedXmlResponseField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessageStartingWith("The following parts of the payload were not documented:"); - } - - @Test - public void missingXmlAttribute() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), - fieldWithPath("a/@id").description("two").type("c"))) - .document(this.operationBuilder.response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessage("Fields with the following paths were not found in the payload: [a/@id]"); - } - - @Test - public void documentedXmlAttributesAreRemoved() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy( - () -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) - .document(this.operationBuilder.response() - .content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessage(String.format("The following parts of the payload were not documented:%nbar%n")); - } - - @Test - public void xmlResponseFieldWithNoType() { - assertThatExceptionOfType(FieldTypeRequiredException.class) - .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())); - } - - @Test - public void missingXmlResponseField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))) - .document(this.operationBuilder.response() - .content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessage("Fields with the following paths were not found in the payload: [a/b]"); - } - - @Test - public void undocumentedXmlResponseFieldAndMissingXmlResponseField() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(this.operationBuilder.response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build())) - .withMessageStartingWith("The following parts of the payload were not documented:") - .withMessageEndingWith("Fields with the following paths were not found in the payload: [a/b]"); - } - - @Test - public void unsupportedContent() { - assertThatExceptionOfType(PayloadHandlingException.class) - .isThrownBy(() -> new ResponseFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.response() - .content("Some plain text") - .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) - .build())) - .withMessage("Cannot handle text/plain content as it could not be parsed as JSON or XML"); - } - - @Test - public void nonOptionalFieldBeneathArrayThatIsSometimesNull() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), - fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.response() - .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"b\": null, \"c\": 2}," + " {\"b\": 1,\"c\": 2}]}") - .build())) - .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); - } - - @Test - public void nonOptionalFieldBeneathArrayThatIsSometimesAbsent() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), - fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.response() - .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") - .build())) - .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 8372ae5e7..e238851b9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,21 +18,19 @@ import java.io.IOException; import java.util.Arrays; - -import org.junit.Test; +import java.util.Collections; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -45,355 +43,486 @@ * @author Andy Wilkinson * @author Sungjun Lee */ -public class ResponseFieldsSnippetTests extends AbstractSnippetTests { - - public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +public class ResponseFieldsSnippetTests { - @Test - public void mapResponseWithFields() throws IOException { + @RenderedSnippetTest + void mapResponseWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets").description("three"), fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`id`", "`Number`", "one") - .row("`date`", "`String`", "two") - .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Array`", "four") - .row("`assets[].id`", "`Number`", "five") - .row("`assets[].name`", "`String`", "six")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`id`", "`Number`", "one") + .row("`date`", "`String`", "two") + .row("`assets`", "`Array`", "three") + .row("`assets[]`", "`Array`", "four") + .row("`assets[].id`", "`Number`", "five") + .row("`assets[].name`", "`String`", "six")); } - @Test - public void mapResponseWithNullField() throws IOException { + @RenderedSnippetTest + void mapResponseWithNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.response().content("{\"a\": {\"b\": null}}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`Null`", "one")); + .document(operationBuilder.response().content("{\"a\": {\"b\": null}}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`Null`", "one")); } - @Test - public void subsectionOfMapResponse() throws IOException { + @RenderedSnippetTest + void subsectionOfMapResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { responseFields(beneathPath("a"), fieldWithPath("b").description("one"), fieldWithPath("c").description("two")) - .document(this.operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); - assertThat(this.generatedSnippets.snippet("response-fields-beneath-a")) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "one") + .document(operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + assertThat(snippets.responseFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") .row("`c`", "`String`", "two")); } - @Test - public void subsectionOfMapResponseBeneathAnArray() throws IOException { + @RenderedSnippetTest + void subsectionOfMapResponseBeneathAnArray(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { responseFields(beneathPath("a.b.[]"), fieldWithPath("c").description("one"), fieldWithPath("d.[].e").description("two")) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("{\"a\": {\"b\": [{\"c\": 1, \"d\": [{\"e\": 5}]}, {\"c\": 3, \"d\": [{\"e\": 4}]}]}}") .build()); - assertThat(this.generatedSnippets.snippet("response-fields-beneath-a.b.[]")) - .is(tableWithHeader("Path", "Type", "Description").row("`c`", "`Number`", "one") + assertThat(snippets.responseFields("beneath-a.b.[]")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`c`", "`Number`", "one") .row("`d.[].e`", "`Number`", "two")); } - @Test - public void subsectionOfMapResponseWithCommonsPrefix() throws IOException { + @RenderedSnippetTest + void subsectionOfMapResponseWithCommonsPrefix(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { responseFields(beneathPath("a")).andWithPrefix("b.", fieldWithPath("c").description("two")) - .document(this.operationBuilder.response().content("{\"a\": {\"b\": {\"c\": \"charlie\"}}}").build()); - assertThat(this.generatedSnippets.snippet("response-fields-beneath-a")) - .is(tableWithHeader("Path", "Type", "Description").row("`b.c`", "`String`", "two")); + .document(operationBuilder.response().content("{\"a\": {\"b\": {\"c\": \"charlie\"}}}").build()); + assertThat(snippets.responseFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b.c`", "`String`", "two")); } - @Test - public void arrayResponseWithFields() throws IOException { + @RenderedSnippetTest + void arrayResponseWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("[{\"a\": {\"b\": 5, \"c\":\"charlie\"}}," + "{\"a\": {\"b\": 4, \"c\":\"chalk\"}}]") .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`[]a.b`", "`Number`", "one") - .row("`[]a.c`", "`String`", "two") - .row("`[]a`", "`Object`", "three")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`[]a.b`", "`Number`", "one") + .row("`[]a.c`", "`String`", "two") + .row("`[]a`", "`Object`", "three")); } - @Test - public void arrayResponseWithAlwaysNullField() throws IOException { - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"))) - .document(this.operationBuilder.response() - .content("[{\"a\": {\"b\": null}}," + "{\"a\": {\"b\": null}}]") - .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`[]a.b`", "`Null`", "one")); + @RenderedSnippetTest + void arrayResponseWithAlwaysNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"))).document( + operationBuilder.response().content("[{\"a\": {\"b\": null}}," + "{\"a\": {\"b\": null}}]").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`[]a.b`", "`Null`", "one")); } - @Test - public void arrayResponse() throws IOException { + @RenderedSnippetTest + void arrayResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(this.operationBuilder.response().content("[\"a\", \"b\", \"c\"]").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`[]`", "`Array`", "one")); + .document(operationBuilder.response().content("[\"a\", \"b\", \"c\"]").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`[]`", "`Array`", "one")); } - @Test - public void ignoredResponseField() throws IOException { + @RenderedSnippetTest + void ignoredResponseField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(this.operationBuilder.response().content("{\"a\": 5, \"b\": 4}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + .document(operationBuilder.response().content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); } - @Test - public void allUndocumentedFieldsCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedFieldsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) - .document(this.operationBuilder.response().content("{\"a\": 5, \"b\": 4}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + .document(operationBuilder.response().content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); } - @Test - public void allUndocumentedFieldsContinueToBeIgnoredAfterAddingDescriptors() throws IOException { + @RenderedSnippetTest + void allUndocumentedFieldsContinueToBeIgnoredAfterAddingDescriptors(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) .andWithPrefix("c.", fieldWithPath("d").description("Field d")) - .document(this.operationBuilder.response().content("{\"a\":5,\"b\":4,\"c\":{\"d\": 3}}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b") - .row("`c.d`", "`Number`", "Field d")); + .document(operationBuilder.response().content("{\"a\":5,\"b\":4,\"c\":{\"d\": 3}}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "Field b") + .row("`c.d`", "`Number`", "Field d")); } - @Test - public void responseFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")) - .willReturn(snippetResource("response-fields-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-fields", template = "response-fields-with-title") + void responseFieldsWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes(key("title").value("Custom title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() - .content("{\"a\": \"foo\"}") - .build()); - assertThat(this.generatedSnippets.responseFields()).contains("Custom title"); + .document(operationBuilder.response().content("{\"a\": \"foo\"}").build()); + assertThat(snippets.responseFields()).contains("Custom title"); } - @Test - public void missingOptionalResponseField() throws IOException { + @RenderedSnippetTest + void missingOptionalResponseField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.response().content("{}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + .document(operationBuilder.response().content("{}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); } - @Test - public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() throws IOException { + @RenderedSnippetTest + void missingIgnoredOptionalResponseFieldDoesNotRequireAType(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(this.operationBuilder.response().content("{}").build()); - assertThat(this.generatedSnippets.responseFields()).is(tableWithHeader("Path", "Type", "Description")); + .document(operationBuilder.response().content("{}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description")); } - @Test - public void presentOptionalResponseField() throws IOException { + @RenderedSnippetTest + void presentOptionalResponseField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.response().content("{\"a\": { \"b\": \"bravo\"}}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + .document(operationBuilder.response().content("{\"a\": { \"b\": \"bravo\"}}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); } - @Test - public void responseFieldsWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")) - .willReturn(snippetResource("response-fields-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-fields", template = "response-fields-with-extra-column") + void responseFieldsWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").attributes(key("foo").value("alpha")), fieldWithPath("a.c").description("two").attributes(key("foo").value("bravo")), fieldWithPath("a").description("three").attributes(key("foo").value("charlie")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response() - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description", "Foo").row("a.b", "Number", "one", "alpha") - .row("a.c", "String", "two", "bravo") - .row("a", "Object", "three", "charlie")); + .document(operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); } - @Test - public void fieldWithExplicitExactlyMatchingType() throws IOException { + @RenderedSnippetTest + void fieldWithExplicitExactlyMatchingType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.response().content("{\"a\": 5 }").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); + .document(operationBuilder.response().content("{\"a\": 5 }").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); } - @Test - public void fieldWithExplicitVariesType() throws IOException { + @RenderedSnippetTest + void fieldWithExplicitVariesType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) - .document(this.operationBuilder.response().content("{\"a\": 5 }").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); + .document(operationBuilder.response().content("{\"a\": 5 }").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); } - @Test - public void applicationXmlResponseFields() throws IOException { - xmlResponseFields(MediaType.APPLICATION_XML); + @RenderedSnippetTest + void applicationXmlResponseFields(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + xmlResponseFields(MediaType.APPLICATION_XML, operationBuilder, snippets); } - @Test - public void textXmlResponseFields() throws IOException { - xmlResponseFields(MediaType.TEXT_XML); + @RenderedSnippetTest + void textXmlResponseFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlResponseFields(MediaType.TEXT_XML, operationBuilder, snippets); } - @Test - public void customXmlResponseFields() throws IOException { - xmlResponseFields(MediaType.parseMediaType("application/vnd.com.example+xml")); + @RenderedSnippetTest + void customXmlResponseFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlResponseFields(MediaType.parseMediaType("application/vnd.com.example+xml"), operationBuilder, snippets); } - private void xmlResponseFields(MediaType contentType) throws IOException { + private void xmlResponseFields(MediaType contentType, OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one").type("b"), fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, contentType.toString()) .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a/b`", "`b`", "one") - .row("`a/c`", "`c`", "two") - .row("`a`", "`a`", "three")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a/b`", "`b`", "one") + .row("`a/c`", "`c`", "two") + .row("`a`", "`a`", "three")); } - @Test - public void xmlAttribute() throws IOException { + @RenderedSnippetTest + void xmlAttribute(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`b`", "one") + .row("`a/@id`", "`c`", "two")); } - @Test - public void missingOptionalXmlAttribute() throws IOException { + @RenderedSnippetTest + void missingOptionalXmlAttribute(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c").optional())) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`b`", "one") + .row("`a/@id`", "`c`", "two")); } - @Test - public void undocumentedAttributeDoesNotCauseFailure() throws IOException { + @RenderedSnippetTest + void undocumentedAttributeDoesNotCauseFailure(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("a"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { PayloadDocumentation .responseFields(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets").description("three")) .and(fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six")) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`id`", "`Number`", "one") - .row("`date`", "`String`", "two") - .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Array`", "four") - .row("`assets[].id`", "`Number`", "five") - .row("`assets[].name`", "`String`", "six")); - } - - @Test - public void prefixedAdditionalDescriptors() throws IOException { + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`id`", "`Number`", "one") + .row("`date`", "`String`", "two") + .row("`assets`", "`Array`", "three") + .row("`assets[]`", "`Array`", "four") + .row("`assets[].id`", "`Number`", "five") + .row("`assets[].name`", "`String`", "six")); + } + + @RenderedSnippetTest + void prefixedAdditionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(this.operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a`", "`Object`", "one") - .row("`a.b`", "`Number`", "two") - .row("`a.c`", "`String`", "three")); + .document(operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one") + .row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); } - @Test - public void responseWithFieldsWithEscapedContent() throws IOException { + @RenderedSnippetTest + void responseWithFieldsWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(this.operationBuilder.response().content("{\"Foo|Bar\": 5}").build()); - assertThat(this.generatedSnippets.responseFields()).is(tableWithHeader("Path", "Type", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); + .document(operationBuilder.response().content("{\"Foo|Bar\": 5}").build()); + assertThat(snippets.responseFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`Foo|Bar`", "`one|two`", "three|four")); } - @Test - public void mapResponseWithVaryingKeysMatchedUsingWildcard() throws IOException { + @RenderedSnippetTest + void mapResponseWithVaryingKeysMatchedUsingWildcard(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("things.*.size").description("one"), fieldWithPath("things.*.type").description("two"))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("{\"things\": {\"12abf\": {\"type\":" + "\"Whale\", \"size\": \"HUGE\"}," + "\"gzM33\" : {\"type\": \"Screw\"," + "\"size\": \"SMALL\"}}}") .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`things.*.size`", "`String`", "one") - .row("`things.*.type`", "`String`", "two")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`things.*.size`", "`String`", "one") + .row("`things.*.type`", "`String`", "two")); } - @Test - public void responseWithArrayContainingFieldThatIsSometimesNull() throws IOException { + @RenderedSnippetTest + void responseWithArrayContainingFieldThatIsSometimesNull(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("assets[].name").description("one").type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("{\"assets\": [" + "{\"name\": \"sample1\"}, " + "{\"name\": null}, " + "{\"name\": \"sample2\"}]}") .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`assets[].name`", "`String`", "one")); + assertThat(snippets.responseFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`assets[].name`", "`String`", "one")); } - @Test - public void optionalFieldBeneathArrayThatIsSometimesAbsent() throws IOException { + @RenderedSnippetTest + void optionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER).optional(), fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.response() + .document(operationBuilder.response() .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") .build()); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`a[].b`", "`Number`", "one") - .row("`a[].c`", "`Number`", "two")); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a[].b`", "`Number`", "one") + .row("`a[].c`", "`Number`", "two")); } - @Test - public void typeDeterminationDoesNotSetTypeOnDescriptor() throws IOException { + @RenderedSnippetTest + void typeDeterminationDoesNotSetTypeOnDescriptor(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { FieldDescriptor descriptor = fieldWithPath("id").description("one"); new ResponseFieldsSnippet(Arrays.asList(descriptor)) - .document(this.operationBuilder.response().content("{\"id\": 67}").build()); + .document(operationBuilder.response().content("{\"id\": 67}").build()); assertThat(descriptor.getType()).isNull(); - assertThat(this.generatedSnippets.responseFields()) - .is(tableWithHeader("Path", "Type", "Description").row("`id`", "`Number`", "one")); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`id`", "`Number`", "one")); + } + + @SnippetTest + void attemptToDocumentFieldsWithNoResponseBody(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.build())) + .withMessage("Cannot document response fields as the response body is empty"); + } + + @SnippetTest + void fieldWithExplicitTypeThatDoesNotMatchThePayload(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.response().content("{ \"a\": 5 }}").build())) + .withMessage("The documented type of the field 'a' is Object but the actual type is Number"); + } + + @SnippetTest + void fieldWithExplicitSpecificTypeThatActuallyVaries(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.response().content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build())) + .withMessage("The documented type of the field '[].a' is Object but the actual type is Varies"); + } + + @SnippetTest + void undocumentedXmlResponseField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingXmlAttribute(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(operationBuilder.response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage("Fields with the following paths were not found in the payload: [a/@id]"); + } + + @SnippetTest + void documentedXmlAttributesAreRemoved(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy( + () -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) + .document(operationBuilder.response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage(String.format("The following parts of the payload were not documented:%nbar%n")); + } + + @SnippetTest + void xmlResponseFieldWithNoType(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypeRequiredException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())); + } + + @SnippetTest + void missingXmlResponseField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))) + .document(operationBuilder.response() + .content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void undocumentedXmlResponseFieldAndMissingXmlResponseField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(operationBuilder.response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:") + .withMessageEndingWith("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void unsupportedContent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.response() + .content("Some plain text") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) + .build())) + .withMessage("Cannot handle text/plain content as it could not be parsed as JSON or XML"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesNull(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.response() + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"b\": null, \"c\": 2}," + " {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.response() + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java index fb601c44e..6b98dac40 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java deleted file mode 100644 index 0b200e522..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.request; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; - -/** - * Tests for failures when rendering {@link FormParametersSnippet} due to missing or - * undocumented form parameters. - * - * @author Andy Wilkinson - */ -public class FormParametersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedParameter() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new FormParametersSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost/").content("a=alpha").build())) - .withMessage("Form parameters with the following names were not documented: [a]"); - } - - @Test - public void missingParameter() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").build())) - .withMessage("Form parameters with the following names were not found in the request: [a]"); - } - - @Test - public void undocumentedAndMissingParameters() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").content("b=bravo").build())) - .withMessage("Form parameters with the following names were not documented: [b]. Form parameters" - + " with the following names were not found in the request: [a]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java index 75840975f..0cf383690 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -40,147 +38,151 @@ * * @author Andy Wilkinson */ -public class FormParametersSnippetTests extends AbstractSnippetTests { - - public FormParametersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class FormParametersSnippetTests { - @Test - public void formParameters() throws IOException { + @RenderedSnippetTest + void formParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new FormParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void formParameterWithNoValue() throws IOException { + @RenderedSnippetTest + void formParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").content("a=").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one")); + .document(operationBuilder.request("http://localhost/").content("a=").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); } - @Test - public void ignoredFormParameter() throws IOException { + @RenderedSnippetTest + void ignoredFormParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new FormParametersSnippet( Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); } - @Test - public void allUndocumentedFormParametersCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedFormParametersCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new FormParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true) - .document(this.operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); } - @Test - public void missingOptionalFormParameter() throws IOException { + @RenderedSnippetTest + void missingOptionalFormParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/").content("b=bravo").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").content("b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void presentOptionalFormParameter() throws IOException { + @RenderedSnippetTest + void presentOptionalFormParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())) - .document(this.operationBuilder.request("http://localhost/").content("a=alpha").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one")); + .document(operationBuilder.request("http://localhost/").content("a=alpha").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); } - @Test - public void formParametersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("form-parameters")) - .willReturn(snippetResource("form-parameters-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "form-parameters", template = "form-parameters-with-title") + void formParametersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new FormParametersSnippet( Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), parameterWithName("b").description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .content("a=alpha&b=bravo") - .build()); - assertThat(this.generatedSnippets.formParameters()).contains("The title"); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()).contains("The title"); } - @Test - public void formParametersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("form-parameters")) - .willReturn(snippetResource("form-parameters-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "form-parameters", template = "form-parameters-with-extra-column") + void formParametersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new FormParametersSnippet( Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), parameterWithName("b").description("two").attributes(key("foo").value("bravo")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .content("a=alpha&b=bravo") - .build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()).isTable((table) -> table.withHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void formParametersWithOptionalColumn() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("form-parameters")) - .willReturn(snippetResource("form-parameters-with-optional-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "form-parameters", template = "form-parameters-with-optional-column") + void formParametersWithOptionalColumn(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") - .content("a=alpha&b=bravo") - .build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Optional", "Description").row("a", "true", "one") + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Optional", "Description") + .row("a", "true", "one") .row("b", "false", "two")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { RequestDocumentation.formParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void additionalDescriptorsWithRelaxedFormParameters() throws IOException { + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedFormParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { RequestDocumentation.relaxedFormParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost/") - .content("a=alpha&b=bravo&c=undocumented") - .build()); - assertThat(this.generatedSnippets.formParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").content("a=alpha&b=bravo&c=undocumented").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void formParametersWithEscapedContent() throws IOException { + @RenderedSnippetTest + void formParametersWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { RequestDocumentation.formParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder.request("http://localhost/").content("Foo%7CBar=baz").build()); - assertThat(this.generatedSnippets.formParameters()).is(tableWithHeader("Parameter", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.request("http://localhost/").content("Foo%7CBar=baz").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new FormParametersSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost/").content("a=alpha").build())) + .withMessage("Form parameters with the following names were not documented: [a]"); + } + + @SnippetTest + void missingParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost/").build())) + .withMessage("Form parameters with the following names were not found in the request: [a]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedAndMissingParameters(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost/").content("b=bravo").build())) + .withMessage("Form parameters with the following names were not documented: [b]. Form parameters" + + " with the following names were not found in the request: [a]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java deleted file mode 100644 index b8c7d3747..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.request; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; - -/** - * Tests for failures when rendering {@link PathParametersSnippet} due to missing or - * undocumented path parameters. - * - * @author Andy Wilkinson - */ -public class PathParametersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedPathParameter() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new PathParametersSnippet(Collections.emptyList()).document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/") - .build())) - .withMessage("Path parameters with the following names were not documented: [a]"); - } - - @Test - public void missingPathParameter() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/") - .build())) - .withMessage("Path parameters with the following names were not found in the request: [a]"); - } - - @Test - public void undocumentedAndMissingPathParameters() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{b}") - .build())) - .withMessage("Path parameters with the following names were not documented: [b]. Path parameters with the" - + " following names were not found in the request: [a]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 6a0009a16..d178bde51 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,20 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,164 +41,192 @@ * * @author Andy Wilkinson */ -public class PathParametersSnippetTests extends AbstractSnippetTests { - - public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class PathParametersSnippetTests { - @Test - public void pathParameters() throws IOException { + @RenderedSnippetTest + void pathParameters(OperationBuilder operationBuilder, AssertableSnippets snippets, TemplateFormat templateFormat) + throws IOException { new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void ignoredPathParameter() throws IOException { + @RenderedSnippetTest + void ignoredPathParameter(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`b`", "two")); } - @Test - public void allUndocumentedPathParametersCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedPathParametersCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true).document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); + operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}").build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`b`", "two")); } - @Test - public void missingOptionalPathParameter() throws IOException { + @RenderedSnippetTest + void missingOptionalPathParameter(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two").optional())) - .document(this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle("/{a}"), "Parameter", "Description").row("`a`", "one") + .document( + operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}").build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat, "/{a}"), "Parameter", "Description") + .row("`a`", "one") .row("`b`", "two")); } - @Test - public void presentOptionalPathParameter() throws IOException { - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())) - .document(this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle("/{a}"), "Parameter", "Description").row("`a`", "one")); + @RenderedSnippetTest + void presentOptionalPathParameter(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())).document( + operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}").build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat, "/{a}"), "Parameter", "Description") + .row("`a`", "one")); } - @Test - public void pathParametersWithQueryString() throws IOException { + @RenderedSnippetTest + void pathParametersWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(this.operationBuilder + .document(operationBuilder .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}?foo=bar") .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void pathParametersWithQueryStringWithParameters() throws IOException { + @RenderedSnippetTest + void pathParametersWithQueryStringWithParameters(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(this.operationBuilder + .document(operationBuilder .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}?foo={c}") .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void pathParametersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")) - .willReturn(snippetResource("path-parameters-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "path-parameters", template = "path-parameters-with-title") + void pathParametersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), parameterWithName("b").description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .build()); - assertThat(this.generatedSnippets.pathParameters()).contains("The title"); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()).contains("The title"); } - @Test - public void pathParametersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")) - .willReturn(snippetResource("path-parameters-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "path-parameters", template = "path-parameters-with-extra-column") + void pathParametersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), parameterWithName("b").description("two").attributes(key("foo").value("bravo")))) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()).isTable((table) -> table.withHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { RequestDocumentation.pathParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()).isTable( + (table) -> table.withTitleAndHeader(getTitle(templateFormat, "/{a}/{b}"), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void additionalDescriptorsWithRelaxedRequestParameters() throws IOException { + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedRequestParameters(OperationBuilder operationBuilder, + AssertableSnippets snippets, TemplateFormat templateFormat) throws IOException { RequestDocumentation.relaxedPathParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(this.operationBuilder - .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}/{c}") + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}/{c}") .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle("/{a}/{b}/{c}"), "Parameter", "Description").row("`a`", "one") - .row("`b`", "two")); + assertThat(snippets.pathParameters()).isTable((table) -> table + .withTitleAndHeader(getTitle(templateFormat, "/{a}/{b}/{c}"), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void pathParametersWithEscapedContent() throws IOException { + @RenderedSnippetTest + void pathParametersWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { RequestDocumentation.pathParameters(parameterWithName("Foo|Bar").description("one|two")) - .document( - this.operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{Foo|Bar}") - .build()); - assertThat(this.generatedSnippets.pathParameters()) - .is(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{Foo|Bar}") + .build()); + assertThat(snippets.pathParameters()).isTable( + (table) -> table.withTitleAndHeader(getTitle(templateFormat, "{Foo|Bar}"), "Parameter", "Description") + .row("`Foo|Bar`", "one|two")); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedPathParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new PathParametersSnippet(Collections.emptyList()) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/") + .build())) + .withMessage("Path parameters with the following names were not documented: [a]"); + } + + @SnippetTest + void missingPathParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/") + .build())) + .withMessage("Path parameters with the following names were not found in the request: [a]"); + } + + @SnippetTest + void undocumentedAndMissingPathParameters(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{b}") + .build())) + .withMessage("Path parameters with the following names were not documented: [b]. Path parameters with the" + + " following names were not found in the request: [a]"); } - private String getTitle() { - return getTitle("/{a}/{b}"); + private String getTitle(TemplateFormat templateFormat) { + return getTitle(templateFormat, "/{a}/{b}"); } - private String getTitle(String title) { - if (this.templateFormat.getId().equals(TemplateFormats.asciidoctor().getId())) { + private String getTitle(TemplateFormat templateFormat, String title) { + if (templateFormat.getId().equals(TemplateFormats.asciidoctor().getId())) { return "+" + title + "+"; } return "`" + title + "`"; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetFailureTests.java deleted file mode 100644 index 2590575de..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetFailureTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.request; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; - -/** - * Tests for failures when rendering {@link QueryParametersSnippet} due to missing or - * undocumented query parameters. - * - * @author Andy Wilkinson - */ -public class QueryParametersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedParameter() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new QueryParametersSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost/?a=alpha").build())) - .withMessage("Query parameters with the following names were not documented: [a]"); - } - - @Test - public void missingParameter() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").build())) - .withMessage("Query parameters with the following names were not found in the request: [a]"); - } - - @Test - public void undocumentedAndMissingParameters() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/?b=bravo").build())) - .withMessage("Query parameters with the following names were not documented: [b]. Query parameters" - + " with the following names were not found in the request: [a]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java index 2398f9885..8084a2382 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -40,142 +38,151 @@ * * @author Andy Wilkinson */ -public class QueryParametersSnippetTests extends AbstractSnippetTests { - - public QueryParametersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class QueryParametersSnippetTests { - @Test - public void queryParameters() throws IOException { + @RenderedSnippetTest + void queryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new QueryParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void queryParameterWithNoValue() throws IOException { + @RenderedSnippetTest + void queryParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/?a").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one")); + .document(operationBuilder.request("http://localhost/?a").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); } - @Test - public void ignoredQueryParameter() throws IOException { + @RenderedSnippetTest + void ignoredQueryParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new QueryParametersSnippet( Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); } - @Test - public void allUndocumentedQueryParametersCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedQueryParametersCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new QueryParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true) - .document(this.operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); } - @Test - public void missingOptionalQueryParameter() throws IOException { + @RenderedSnippetTest + void missingOptionalQueryParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/?b=bravo").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/?b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void presentOptionalQueryParameter() throws IOException { + @RenderedSnippetTest + void presentOptionalQueryParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())) - .document(this.operationBuilder.request("http://localhost/?a=alpha").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one")); + .document(operationBuilder.request("http://localhost/?a=alpha").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); } - @Test - public void queryParametersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("query-parameters")) - .willReturn(snippetResource("query-parameters-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "query-parameters", template = "query-parameters-with-title") + void queryParametersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new QueryParametersSnippet( Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), parameterWithName("b").description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/?a=alpha&b=bravo") - .build()); - assertThat(this.generatedSnippets.queryParameters()).contains("The title"); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()).contains("The title"); } - @Test - public void queryParametersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("query-parameters")) - .willReturn(snippetResource("query-parameters-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "query-parameters", template = "query-parameters-with-extra-column") + void queryParametersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new QueryParametersSnippet( Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), parameterWithName("b").description("two").attributes(key("foo").value("bravo")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/?a=alpha&b=bravo") - .build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()).isTable((table) -> table.withHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void queryParametersWithOptionalColumn() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("query-parameters")) - .willReturn(snippetResource("query-parameters-with-optional-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "query-parameters", template = "query-parameters-with-optional-column") + void queryParametersWithOptionalColumn(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/?a=alpha&b=bravo") - .build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Optional", "Description").row("a", "true", "one") + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Optional", "Description") + .row("a", "true", "one") .row("b", "false", "two")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { RequestDocumentation.queryParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void additionalDescriptorsWithRelaxedQueryParameters() throws IOException { + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedQueryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { RequestDocumentation.relaxedQueryParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost/?a=alpha&b=bravo&c=undocumented").build()); - assertThat(this.generatedSnippets.queryParameters()) - .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/?a=alpha&b=bravo&c=undocumented").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void queryParametersWithEscapedContent() throws IOException { + @RenderedSnippetTest + void queryParametersWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { RequestDocumentation.queryParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder.request("http://localhost/?Foo%7CBar=baz").build()); - assertThat(this.generatedSnippets.queryParameters()).is(tableWithHeader("Parameter", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.request("http://localhost/?Foo%7CBar=baz").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new QueryParametersSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost/?a=alpha").build())) + .withMessage("Query parameters with the following names were not documented: [a]"); + } + + @SnippetTest + void missingParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost/").build())) + .withMessage("Query parameters with the following names were not found in the request: [a]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedAndMissingParameters(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost/?b=bravo").build())) + .withMessage("Query parameters with the following names were not documented: [b]. Query parameters" + + " with the following names were not found in the request: [a]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java deleted file mode 100644 index e8ad15874..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.request; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.OperationBuilder; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.restdocs.request.RequestDocumentation.partWithName; - -/** - * Tests for failures when rendering {@link RequestPartsSnippet} due to missing or - * undocumented request parts. - * - * @author Andy Wilkinson - */ -public class RequestPartsSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Test - public void undocumentedPart() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestPartsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost/").part("a", "alpha".getBytes()).build())) - .withMessage("Request parts with the following names were not documented: [a]"); - } - - @Test - public void missingPart() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").build())) - .withMessage("Request parts with the following names were not found in the request: [a]"); - } - - @Test - public void undocumentedAndMissingParts() { - assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost/").part("b", "bravo".getBytes()).build())) - .withMessage("Request parts with the following names were not documented: [b]. Request parts with the" - + " following names were not found in the request: [a]"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 51c3fbb0d..9dcc12f9f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -40,145 +38,157 @@ * * @author Andy Wilkinson */ -public class RequestPartsSnippetTests extends AbstractSnippetTests { - - public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestPartsSnippetTests { - @Test - public void requestParts() throws IOException { + @RenderedSnippetTest + void requestParts(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one"), partWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "bravo".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()) - .is(tableWithHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void ignoredRequestPart() throws IOException { + @RenderedSnippetTest + void ignoredRequestPart(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), partWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "bravo".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()).is(tableWithHeader("Part", "Description").row("`b`", "two")); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`b`", "two")); } - @Test - public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { + @RenderedSnippetTest + void allUndocumentedRequestPartsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "bravo".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()).is(tableWithHeader("Part", "Description").row("`b`", "two")); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`b`", "two")); } - @Test - public void missingOptionalRequestPart() throws IOException { + @RenderedSnippetTest + void missingOptionalRequestPart(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional(), partWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost/").part("b", "bravo".getBytes()).build()); - assertThat(this.generatedSnippets.requestParts()) - .is(tableWithHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.request("http://localhost/").part("b", "bravo".getBytes()).build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void presentOptionalRequestPart() throws IOException { + @RenderedSnippetTest + void presentOptionalRequestPart(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one").optional())) - .document(this.operationBuilder.request("http://localhost/").part("a", "one".getBytes()).build()); - assertThat(this.generatedSnippets.requestParts()).is(tableWithHeader("Part", "Description").row("`a`", "one")); + .document(operationBuilder.request("http://localhost/").part("a", "one".getBytes()).build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one")); } - @Test - public void requestPartsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parts")) - .willReturn(snippetResource("request-parts-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-parts", template = "request-parts-with-title") + void requestPartsWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").attributes(key("foo").value("alpha")), partWithName("b").description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "alpha".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()).contains("The title"); + assertThat(snippets.requestParts()).contains("The title"); } - @Test - public void requestPartsWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parts")) - .willReturn(snippetResource("request-parts-with-extra-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-parts", template = "request-parts-with-extra-column") + void requestPartsWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").attributes(key("foo").value("alpha")), partWithName("b").description("two").attributes(key("foo").value("bravo")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "alpha".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()) - .is(tableWithHeader("Part", "Description", "Foo").row("a", "one", "alpha").row("b", "two", "bravo")); + assertThat(snippets.requestParts()).isTable((table) -> table.withHeader("Part", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void requestPartsWithOptionalColumn() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parts")) - .willReturn(snippetResource("request-parts-with-optional-column")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-parts", template = "request-parts-with-optional-column") + void requestPartsWithOptionalColumn(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional(), partWithName("b").description("two"))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "alpha".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()) - .is(tableWithHeader("Part", "Optional", "Description").row("a", "true", "one").row("b", "false", "two")); + assertThat(snippets.requestParts()).isTable((table) -> table.withHeader("Part", "Optional", "Description") + .row("a", "true", "one") + .row("b", "false", "two")); } - @Test - public void additionalDescriptors() throws IOException { + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { RequestDocumentation.requestParts(partWithName("a").description("one")) .and(partWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost/") + .document(operationBuilder.request("http://localhost/") .part("a", "bravo".getBytes()) .and() .part("b", "bravo".getBytes()) .build()); - assertThat(this.generatedSnippets.requestParts()) - .is(tableWithHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void requestPartsWithEscapedContent() throws IOException { + @RenderedSnippetTest + void requestPartsWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder.request("http://localhost/").part("Foo|Bar", "baz".getBytes()).build()); - assertThat(this.generatedSnippets.requestParts()).is(tableWithHeader("Part", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + .document(operationBuilder.request("http://localhost/").part("Foo|Bar", "baz".getBytes()).build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedPart(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost/").part("a", "alpha".getBytes()).build())) + .withMessage("Request parts with the following names were not documented: [a]"); + } + + @SnippetTest + void missingPart(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost/").build())) + .withMessage("Request parts with the following names were not found in the request: [a]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedAndMissingParts(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost/").part("b", "bravo".getBytes()).build())) + .withMessage("Request parts with the following names were not documented: [b]. Request parts with the" + + " following names were not found in the request: [a]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index ab592d380..22b285c30 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.restdocs.snippet; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; @@ -30,82 +30,82 @@ * @author Andy Wilkinson * */ -public class RestDocumentationContextPlaceholderResolverTests { +class RestDocumentationContextPlaceholderResolverTests { @Test - public void kebabCaseMethodName() { + void kebabCaseMethodName() { assertThat(createResolver("dashSeparatedMethodName").resolvePlaceholder("method-name")) .isEqualTo("dash-separated-method-name"); } @Test - public void kebabCaseMethodNameWithUpperCaseOpeningSection() { + void kebabCaseMethodNameWithUpperCaseOpeningSection() { assertThat(createResolver("URIDashSeparatedMethodName").resolvePlaceholder("method-name")) .isEqualTo("uri-dash-separated-method-name"); } @Test - public void kebabCaseMethodNameWithUpperCaseMidSection() { + void kebabCaseMethodNameWithUpperCaseMidSection() { assertThat(createResolver("dashSeparatedMethodNameWithURIInIt").resolvePlaceholder("method-name")) .isEqualTo("dash-separated-method-name-with-uri-in-it"); } @Test - public void kebabCaseMethodNameWithUpperCaseEndSection() { + void kebabCaseMethodNameWithUpperCaseEndSection() { assertThat(createResolver("dashSeparatedMethodNameWithURI").resolvePlaceholder("method-name")) .isEqualTo("dash-separated-method-name-with-uri"); } @Test - public void snakeCaseMethodName() { + void snakeCaseMethodName() { assertThat(createResolver("underscoreSeparatedMethodName").resolvePlaceholder("method_name")) .isEqualTo("underscore_separated_method_name"); } @Test - public void snakeCaseMethodNameWithUpperCaseOpeningSection() { + void snakeCaseMethodNameWithUpperCaseOpeningSection() { assertThat(createResolver("URIUnderscoreSeparatedMethodName").resolvePlaceholder("method_name")) .isEqualTo("uri_underscore_separated_method_name"); } @Test - public void snakeCaseMethodNameWithUpperCaseMidSection() { + void snakeCaseMethodNameWithUpperCaseMidSection() { assertThat(createResolver("underscoreSeparatedMethodNameWithURIInIt").resolvePlaceholder("method_name")) .isEqualTo("underscore_separated_method_name_with_uri_in_it"); } @Test - public void snakeCaseMethodNameWithUpperCaseEndSection() { + void snakeCaseMethodNameWithUpperCaseEndSection() { assertThat(createResolver("underscoreSeparatedMethodNameWithURI").resolvePlaceholder("method_name")) .isEqualTo("underscore_separated_method_name_with_uri"); } @Test - public void camelCaseMethodName() { + void camelCaseMethodName() { assertThat(createResolver("camelCaseMethodName").resolvePlaceholder("methodName")) .isEqualTo("camelCaseMethodName"); } @Test - public void kebabCaseClassName() { + void kebabCaseClassName() { assertThat(createResolver().resolvePlaceholder("class-name")) .isEqualTo("rest-documentation-context-placeholder-resolver-tests"); } @Test - public void snakeCaseClassName() { + void snakeCaseClassName() { assertThat(createResolver().resolvePlaceholder("class_name")) .isEqualTo("rest_documentation_context_placeholder_resolver_tests"); } @Test - public void camelCaseClassName() { + void camelCaseClassName() { assertThat(createResolver().resolvePlaceholder("ClassName")) .isEqualTo("RestDocumentationContextPlaceholderResolverTests"); } @Test - public void stepCount() { + void stepCount() { assertThat(createResolver("stepCount").resolvePlaceholder("step")).isEqualTo("1"); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index e4f600bef..13f3b6cd2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,8 @@ import java.io.IOException; import java.io.Writer; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; @@ -40,10 +39,10 @@ * * @author Andy Wilkinson */ -public class StandardWriterResolverTests { +class StandardWriterResolverTests { - @Rule - public final TemporaryFolder temp = new TemporaryFolder(); + @TempDir + File temp; private final PlaceholderResolverFactory placeholderResolverFactory = mock(PlaceholderResolverFactory.class); @@ -51,21 +50,21 @@ public class StandardWriterResolverTests { TemplateFormats.asciidoctor()); @Test - public void absoluteInput() { + void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); assertThat(this.resolver.resolveFile(absolutePath, "bar.txt", createContext(absolutePath))) .isEqualTo(new File(absolutePath, "bar.txt")); } @Test - public void configuredOutputAndRelativeInput() { + void configuredOutputAndRelativeInput() { File outputDir = new File("foo").getAbsoluteFile(); assertThat(this.resolver.resolveFile("bar", "baz.txt", createContext(outputDir.getAbsolutePath()))) .isEqualTo(new File(outputDir, "bar/baz.txt")); } @Test - public void configuredOutputAndAbsoluteInput() { + void configuredOutputAndAbsoluteInput() { File outputDir = new File("foo").getAbsoluteFile(); String absolutePath = new File("bar").getAbsolutePath(); assertThat(this.resolver.resolveFile(absolutePath, "baz.txt", createContext(outputDir.getAbsolutePath()))) @@ -73,8 +72,8 @@ public void configuredOutputAndAbsoluteInput() { } @Test - public void placeholdersAreResolvedInOperationName() throws IOException { - File outputDirectory = this.temp.newFolder(); + void placeholdersAreResolvedInOperationName() throws IOException { + File outputDirectory = this.temp; RestDocumentationContext context = createContext(outputDirectory.getAbsolutePath()); PlaceholderResolver resolver = mock(PlaceholderResolver.class); given(resolver.resolvePlaceholder("a")).willReturn("alpha"); @@ -84,8 +83,8 @@ public void placeholdersAreResolvedInOperationName() throws IOException { } @Test - public void placeholdersAreResolvedInSnippetName() throws IOException { - File outputDirectory = this.temp.newFolder(); + void placeholdersAreResolvedInSnippetName() throws IOException { + File outputDirectory = this.temp; RestDocumentationContext context = createContext(outputDirectory.getAbsolutePath()); PlaceholderResolver resolver = mock(PlaceholderResolver.class); given(resolver.resolvePlaceholder("b")).willReturn("bravo"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index 8892891af..724e68a5d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,12 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.testfixtures.GeneratedSnippets; -import org.springframework.restdocs.testfixtures.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; import static org.assertj.core.api.Assertions.assertThat; @@ -36,16 +35,10 @@ * * @author Andy Wilkinson */ -public class TemplatedSnippetTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); - - @Rule - public GeneratedSnippets snippets = new GeneratedSnippets(TemplateFormats.asciidoctor()); +class TemplatedSnippetTests { @Test - public void attributesAreCopied() { + void attributesAreCopied() { Map attributes = new HashMap<>(); attributes.put("a", "alpha"); TemplatedSnippet snippet = new TestTemplatedSnippet(attributes); @@ -55,22 +48,23 @@ public void attributesAreCopied() { } @Test - public void nullAttributesAreTolerated() { + void nullAttributesAreTolerated() { assertThat(new TestTemplatedSnippet(null).getAttributes()).isNotNull(); assertThat(new TestTemplatedSnippet(null).getAttributes()).isEmpty(); } @Test - public void snippetName() { + void snippetName() { assertThat(new TestTemplatedSnippet(Collections.emptyMap()).getSnippetName()).isEqualTo("test"); } - @Test - public void multipleSnippetsCanBeProducedFromTheSameTemplate() throws IOException { - new TestTemplatedSnippet("one", "multiple-snippets").document(this.operationBuilder.build()); - new TestTemplatedSnippet("two", "multiple-snippets").document(this.operationBuilder.build()); - assertThat(this.snippets.snippet("multiple-snippets-one")).isNotNull(); - assertThat(this.snippets.snippet("multiple-snippets-two")).isNotNull(); + @RenderedSnippetTest + void multipleSnippetsCanBeProducedFromTheSameTemplate(OperationBuilder operationBuilder, AssertableSnippets snippet) + throws IOException { + new TestTemplatedSnippet("one", "multiple-snippets").document(operationBuilder.build()); + new TestTemplatedSnippet("two", "multiple-snippets").document(operationBuilder.build()); + assertThat(snippet.named("multiple-snippets-one")).exists(); + assertThat(snippet.named("multiple-snippets-two")).exists(); } private static class TestTemplatedSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index f7d0c9f7a..4f5646337 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import java.util.Map; import java.util.concurrent.Callable; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.core.io.Resource; @@ -34,7 +34,7 @@ * * @author Andy Wilkinson */ -public class StandardTemplateResourceResolverTests { +class StandardTemplateResourceResolverTests { private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver( TemplateFormats.asciidoctor()); @@ -42,7 +42,7 @@ public class StandardTemplateResourceResolverTests { private final TestClassLoader classLoader = new TestClassLoader(); @Test - public void formatSpecificCustomSnippetHasHighestPrecedence() throws IOException { + void formatSpecificCustomSnippetHasHighestPrecedence() throws IOException { this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/test.snippet", getClass().getResource("test-format-specific-custom.snippet")); this.classLoader.addResource("org/springframework/restdocs/templates/test.snippet", @@ -62,7 +62,7 @@ public Resource call() { } @Test - public void generalCustomSnippetIsUsedInAbsenceOfFormatSpecificCustomSnippet() throws IOException { + void generalCustomSnippetIsUsedInAbsenceOfFormatSpecificCustomSnippet() throws IOException { this.classLoader.addResource("org/springframework/restdocs/templates/test.snippet", getClass().getResource("test-custom.snippet")); this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/default-test.snippet", @@ -80,7 +80,7 @@ public Resource call() { } @Test - public void defaultSnippetIsUsedInAbsenceOfCustomSnippets() throws Exception { + void defaultSnippetIsUsedInAbsenceOfCustomSnippets() throws Exception { this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/default-test.snippet", getClass().getResource("test-default.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -96,7 +96,7 @@ public Resource call() { } @Test - public void failsIfCustomAndDefaultSnippetsDoNotExist() { + void failsIfCustomAndDefaultSnippetsDoNotExist() { assertThatIllegalStateException() .isThrownBy(() -> doWithThreadContextClassLoader(this.classLoader, () -> StandardTemplateResourceResolverTests.this.resolver.resolveTemplateResource("test"))) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java index 161471d00..635a80ae5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.io.IOException; import java.io.StringWriter; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.mustache.Template.Fragment; @@ -32,10 +32,10 @@ * * @author Andy Wilkinson */ -public class AsciidoctorTableCellContentLambdaTests { +class AsciidoctorTableCellContentLambdaTests { @Test - public void verticalBarCharactersAreEscaped() throws IOException { + void verticalBarCharactersAreEscaped() throws IOException { Fragment fragment = mock(Fragment.class); given(fragment.execute()).willReturn("|foo|bar|baz|"); StringWriter writer = new StringWriter(); @@ -44,7 +44,7 @@ public void verticalBarCharactersAreEscaped() throws IOException { } @Test - public void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { + void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { Fragment fragment = mock(Fragment.class); given(fragment.execute()).willReturn("\\|foo|bar\\|baz|"); StringWriter writer = new StringWriter(); diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet index 24bb63fa9..ff141ad37 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet @@ -1,4 +1,5 @@ {{title}} + Path | Type | Description ---- | ---- | ----------- {{#fields}} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java deleted file mode 100644 index a00d0d096..000000000 --- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.testfixtures; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - -import org.junit.runners.model.Statement; - -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.util.FileCopyUtils; - -import static org.assertj.core.api.Assertions.fail; - -/** - * The {@code GeneratedSnippets} rule is used to capture the snippets generated by a test - * and assert their existence and content. - * - * @author Andy Wilkinson - * @author Andreas Evers - */ -public class GeneratedSnippets extends OperationTestRule { - - private final TemplateFormat templateFormat; - - private String operationName; - - private File outputDirectory; - - public GeneratedSnippets(TemplateFormat templateFormat) { - this.templateFormat = templateFormat; - } - - @Override - public Statement apply(Statement base, File outputDirectory, String operationName) { - this.outputDirectory = outputDirectory; - this.operationName = operationName; - return base; - } - - public String curlRequest() { - return snippet("curl-request"); - } - - public String httpieRequest() { - return snippet("httpie-request"); - } - - public String requestHeaders() { - return snippet("request-headers"); - } - - public String responseHeaders() { - return snippet("response-headers"); - } - - public String requestCookies() { - return snippet("request-cookies"); - } - - public String responseCookies() { - return snippet("response-cookies"); - } - - public String httpRequest() { - return snippet("http-request"); - } - - public String httpResponse() { - return snippet("http-response"); - } - - public String links() { - return snippet("links"); - } - - public String requestFields() { - return snippet("request-fields"); - } - - public String requestParts() { - return snippet("request-parts"); - } - - public String requestPartFields(String partName) { - return snippet("request-part-" + partName + "-fields"); - } - - public String responseFields() { - return snippet("response-fields"); - } - - public String pathParameters() { - return snippet("path-parameters"); - } - - public String queryParameters() { - return snippet("query-parameters"); - } - - public String formParameters() { - return snippet("form-parameters"); - } - - public String snippet(String name) { - File snippetFile = getSnippetFile(name); - try { - return FileCopyUtils - .copyToString(new InputStreamReader(new FileInputStream(snippetFile), StandardCharsets.UTF_8)); - } - catch (Exception ex) { - fail("Failed to read '" + snippetFile + "'", ex); - return null; - } - } - - private File getSnippetFile(String name) { - if (this.outputDirectory == null) { - fail("Output directory was null"); - } - if (this.operationName == null) { - fail("Operation name was null"); - } - File snippetDir = new File(this.outputDirectory, this.operationName); - return new File(snippetDir, name + "." + this.templateFormat.getFileExtension()); - } - -} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationTestRule.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationTestRule.java deleted file mode 100644 index 79e19f0ff..000000000 --- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationTestRule.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2014-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.testfixtures; - -import java.io.File; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * Abstract base class for Operation-related {@link TestRule TestRules}. - * - * @author Andy Wilkinson - */ -abstract class OperationTestRule implements TestRule { - - @Override - public final Statement apply(Statement base, Description description) { - return apply(base, determineOutputDirectory(description), determineOperationName(description)); - } - - private File determineOutputDirectory(Description description) { - return new File("build/" + description.getTestClass().getSimpleName()); - } - - private String determineOperationName(Description description) { - String operationName = description.getMethodName(); - int index = operationName.indexOf('['); - if (index > 0) { - operationName = operationName.substring(0, index); - } - return operationName; - } - - protected abstract Statement apply(Statement base, File outputDirectory, String operationName); - -} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCaptureRule.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCaptureRule.java deleted file mode 100644 index 38710f25b..000000000 --- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCaptureRule.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2014-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.testfixtures; - -import org.junit.Rule; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * JUnit {@code @Rule} to capture output from System.out and System.err. - *

    - * To use add as a {@link Rule @Rule}: - * - *

    - * public class MyTest {
    - *
    - *     @Rule
    - *     public OutputCaptureRule output = new OutputCaptureRule();
    - *
    - *     @Test
    - *     public void test() {
    - *         assertThat(output).contains("ok");
    - *     }
    - *
    - * }
    - * 
    - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -public class OutputCaptureRule implements TestRule, CapturedOutput { - - private final OutputCapture delegate = new OutputCapture(); - - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - OutputCaptureRule.this.delegate.push(); - try { - base.evaluate(); - } - finally { - OutputCaptureRule.this.delegate.pop(); - } - } - }; - } - - @Override - public String getAll() { - return this.delegate.getAll(); - } - - @Override - public String getOut() { - return this.delegate.getOut(); - } - - @Override - public String getErr() { - return this.delegate.getErr(); - } - - @Override - public String toString() { - return this.delegate.toString(); - } - -} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java new file mode 100644 index 000000000..fdab049e1 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java @@ -0,0 +1,679 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.UnaryOperator; + +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.Assertions; + +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.util.StringUtils; + +/** + * AssertJ {@link AssertProvider} for asserting that the generated snippets are correct. + * + * @author Andy Wilkinson + */ +public class AssertableSnippets { + + private final File outputDirectory; + + private final String operationName; + + private final TemplateFormat templateFormat; + + AssertableSnippets(File outputDirectory, String operationName, TemplateFormat templateFormat) { + this.outputDirectory = outputDirectory; + this.operationName = operationName; + this.templateFormat = templateFormat; + } + + public File named(String name) { + return getSnippetFile(name); + } + + private File getSnippetFile(String name) { + File snippetDir = new File(this.outputDirectory, this.operationName); + return new File(snippetDir, name + "." + this.templateFormat.getFileExtension()); + } + + public CodeBlockSnippetAssertProvider curlRequest() { + return new CodeBlockSnippetAssertProvider("curl-request"); + } + + public TableSnippetAssertProvider formParameters() { + return new TableSnippetAssertProvider("form-parameters"); + } + + public CodeBlockSnippetAssertProvider httpieRequest() { + return new CodeBlockSnippetAssertProvider("httpie-request"); + } + + public HttpRequestSnippetAssertProvider httpRequest() { + return new HttpRequestSnippetAssertProvider("http-request"); + } + + public HttpResponseSnippetAssertProvider httpResponse() { + return new HttpResponseSnippetAssertProvider("http-response"); + } + + public TableSnippetAssertProvider links() { + return new TableSnippetAssertProvider("links"); + } + + public TableSnippetAssertProvider pathParameters() { + return new TableSnippetAssertProvider("path-parameters"); + } + + public TableSnippetAssertProvider queryParameters() { + return new TableSnippetAssertProvider("query-parameters"); + } + + public CodeBlockSnippetAssertProvider requestBody() { + return new CodeBlockSnippetAssertProvider("request-body"); + } + + public CodeBlockSnippetAssertProvider requestBody(String suffix) { + return new CodeBlockSnippetAssertProvider("request-body-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider requestCookies() { + return new TableSnippetAssertProvider("request-cookies"); + } + + public TableSnippetAssertProvider requestCookies(String suffix) { + return new TableSnippetAssertProvider("request-cookies-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider requestFields() { + return new TableSnippetAssertProvider("request-fields"); + } + + public TableSnippetAssertProvider requestFields(String suffix) { + return new TableSnippetAssertProvider("request-fields-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider requestHeaders() { + return new TableSnippetAssertProvider("request-headers"); + } + + public TableSnippetAssertProvider requestHeaders(String suffix) { + return new TableSnippetAssertProvider("request-headers-%s".formatted(suffix)); + } + + public CodeBlockSnippetAssertProvider requestPartBody(String partName) { + return new CodeBlockSnippetAssertProvider("request-part-%s-body".formatted(partName)); + } + + public CodeBlockSnippetAssertProvider requestPartBody(String partName, String suffix) { + return new CodeBlockSnippetAssertProvider("request-part-%s-body-%s".formatted(partName, suffix)); + } + + public TableSnippetAssertProvider requestPartFields(String partName) { + return new TableSnippetAssertProvider("request-part-%s-fields".formatted(partName)); + } + + public TableSnippetAssertProvider requestPartFields(String partName, String suffix) { + return new TableSnippetAssertProvider("request-part-%s-fields-%s".formatted(partName, suffix)); + } + + public TableSnippetAssertProvider requestParts() { + return new TableSnippetAssertProvider("request-parts"); + } + + public CodeBlockSnippetAssertProvider responseBody() { + return new CodeBlockSnippetAssertProvider("response-body"); + } + + public CodeBlockSnippetAssertProvider responseBody(String suffix) { + return new CodeBlockSnippetAssertProvider("response-body-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider responseCookies() { + return new TableSnippetAssertProvider("response-cookies"); + } + + public TableSnippetAssertProvider responseFields() { + return new TableSnippetAssertProvider("response-fields"); + } + + public TableSnippetAssertProvider responseFields(String suffix) { + return new TableSnippetAssertProvider("response-fields-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider responseHeaders() { + return new TableSnippetAssertProvider("response-headers"); + } + + public final class TableSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private TableSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public TableSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new TableSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class TableSnippetAssert extends AbstractStringAssert { + + private TableSnippetAssert(String actual) { + super(actual, TableSnippetAssert.class); + } + + public void isTable(UnaryOperator> tableOperator) { + Table table = tableOperator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorTable() : new MarkdownTable()); + table.getLinesAsString(); + Assertions.assertThat(this.actual).isEqualTo(table.getLinesAsString()); + } + + } + + public abstract class Table> extends SnippetContent { + + public abstract T withHeader(String... columns); + + public abstract T withTitleAndHeader(String title, String... columns); + + public abstract T row(String... entries); + + public abstract T configuration(String string); + + } + + private final class AsciidoctorTable extends Table { + + @Override + public AsciidoctorTable withHeader(String... columns) { + return withTitleAndHeader("", columns); + } + + @Override + public AsciidoctorTable withTitleAndHeader(String title, String... columns) { + if (!title.isBlank()) { + this.addLine("." + title); + } + this.addLine("|==="); + String header = "|" + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); + this.addLine(header); + this.addLine(""); + this.addLine("|==="); + return this; + } + + @Override + public AsciidoctorTable row(String... entries) { + for (String entry : entries) { + this.addLine(-1, "|" + escapeEntry(entry)); + } + this.addLine(-1, ""); + return this; + } + + private String escapeEntry(String entry) { + entry = entry.replace("|", "\\|"); + if (entry.startsWith("`") && entry.endsWith("`")) { + return "`+" + entry.substring(1, entry.length() - 1) + "+`"; + } + return entry; + } + + @Override + public AsciidoctorTable configuration(String configuration) { + this.addLine(0, configuration); + return this; + } + + } + + private final class MarkdownTable extends Table { + + @Override + public MarkdownTable withHeader(String... columns) { + return withTitleAndHeader("", columns); + } + + @Override + public MarkdownTable withTitleAndHeader(String title, String... columns) { + if (StringUtils.hasText(title)) { + this.addLine(title); + this.addLine(""); + } + String header = StringUtils.collectionToDelimitedString(Arrays.asList(columns), " | "); + this.addLine(header); + List components = new ArrayList<>(); + for (String column : columns) { + StringBuilder dashes = new StringBuilder(); + for (int i = 0; i < column.length(); i++) { + dashes.append("-"); + } + components.add(dashes.toString()); + } + this.addLine(StringUtils.collectionToDelimitedString(components, " | ")); + this.addLine(""); + return this; + } + + @Override + public MarkdownTable row(String... entries) { + this.addLine(-1, StringUtils.collectionToDelimitedString(Arrays.asList(entries), " | ")); + return this; + } + + @Override + public MarkdownTable configuration(String configuration) { + throw new UnsupportedOperationException("Markdown tables do not support configuration"); + } + + } + + public final class CodeBlockSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private CodeBlockSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public CodeBlockSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new CodeBlockSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class CodeBlockSnippetAssert extends AbstractStringAssert { + + private CodeBlockSnippetAssert(String actual) { + super(actual, CodeBlockSnippetAssert.class); + } + + public void isCodeBlock(UnaryOperator> codeBlockOperator) { + CodeBlock codeBlock = codeBlockOperator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorCodeBlock() : new MarkdownCodeBlock()); + Assertions.assertThat(this.actual).isEqualTo(codeBlock.getLinesAsString()); + } + + } + + public abstract class CodeBlock> extends SnippetContent { + + public abstract T withLanguage(String language); + + public abstract T withOptions(String options); + + public abstract T withLanguageAndOptions(String language, String options); + + public abstract T content(String string); + + } + + private final class AsciidoctorCodeBlock extends CodeBlock { + + @Override + public AsciidoctorCodeBlock withLanguage(String language) { + addLine("[source,%s]".formatted(language)); + return this; + } + + @Override + public AsciidoctorCodeBlock withOptions(String options) { + addLine("[source,options=\"%s\"]".formatted(options)); + return this; + } + + @Override + public AsciidoctorCodeBlock withLanguageAndOptions(String language, String options) { + addLine("[source,%s,options=\"%s\"]".formatted(language, options)); + return this; + } + + @Override + public AsciidoctorCodeBlock content(String content) { + addLine("----"); + addLine(content); + addLine("----"); + return this; + } + + } + + private final class MarkdownCodeBlock extends CodeBlock { + + @Override + public MarkdownCodeBlock withLanguage(String language) { + addLine("```%s".formatted(language)); + return this; + } + + @Override + public MarkdownCodeBlock withOptions(String options) { + addLine("```"); + return this; + } + + @Override + public MarkdownCodeBlock withLanguageAndOptions(String language, String options) { + addLine("```%s".formatted(language)); + return this; + } + + @Override + public MarkdownCodeBlock content(String content) { + addLine(content); + addLine("```"); + return this; + } + + } + + public final class HttpRequestSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private HttpRequestSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public HttpRequestSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new HttpRequestSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class HttpRequestSnippetAssert extends AbstractStringAssert { + + private HttpRequestSnippetAssert(String actual) { + super(actual, HttpRequestSnippetAssert.class); + } + + public void isHttpRequest(UnaryOperator> operator) { + HttpRequest codeBlock = operator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorHttpRequest() : new MarkdownHttpRequest()); + Assertions.assertThat(this.actual).isEqualTo(codeBlock.getLinesAsString()); + } + + } + + public abstract class HttpRequest> extends SnippetContent { + + public T get(String uri) { + return request("GET", uri); + } + + public T post(String uri) { + return request("POST", uri); + } + + public T put(String uri) { + return request("PUT", uri); + } + + public T patch(String uri) { + return request("PATCH", uri); + } + + public T delete(String uri) { + return request("DELETE", uri); + } + + protected abstract T request(String method, String uri); + + public abstract T header(String name, Object value); + + @SuppressWarnings("unchecked") + public T content(String content) { + addLine(-1, content); + return (T) this; + } + + } + + private final class AsciidoctorHttpRequest extends HttpRequest { + + private int headerOffset = 3; + + @Override + protected AsciidoctorHttpRequest request(String method, String uri) { + addLine("[source,http,options=\"nowrap\"]"); + addLine("----"); + addLine("%s %s HTTP/1.1".formatted(method, uri)); + addLine(""); + addLine("----"); + return this; + } + + @Override + public AsciidoctorHttpRequest header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + private final class MarkdownHttpRequest extends HttpRequest { + + private int headerOffset = 2; + + @Override + public MarkdownHttpRequest request(String method, String uri) { + addLine("```http"); + addLine("%s %s HTTP/1.1".formatted(method, uri)); + addLine(""); + addLine("```"); + return this; + } + + @Override + public MarkdownHttpRequest header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + public final class HttpResponseSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private HttpResponseSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public HttpResponseSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new HttpResponseSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class HttpResponseSnippetAssert extends AbstractStringAssert { + + private HttpResponseSnippetAssert(String actual) { + super(actual, HttpResponseSnippetAssert.class); + } + + public void isHttpResponse(UnaryOperator> operator) { + HttpResponse httpResponse = operator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorHttpResponse() : new MarkdownHttpResponse()); + Assertions.assertThat(this.actual).isEqualTo(httpResponse.getLinesAsString()); + } + + } + + public abstract class HttpResponse> extends SnippetContent { + + public T ok() { + return status("200 OK"); + } + + public T badRequest() { + return status("400 Bad Request"); + } + + public T status(int status) { + return status("%d ".formatted(status)); + } + + protected abstract T status(String status); + + public abstract T header(String name, Object value); + + @SuppressWarnings("unchecked") + public T content(String content) { + addLine(-1, content); + return (T) this; + } + + } + + private final class AsciidoctorHttpResponse extends HttpResponse { + + private int headerOffset = 3; + + @Override + protected AsciidoctorHttpResponse status(String status) { + addLine("[source,http,options=\"nowrap\"]"); + addLine("----"); + addLine("HTTP/1.1 %s".formatted(status)); + addLine(""); + addLine("----"); + return this; + } + + @Override + public AsciidoctorHttpResponse header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + private final class MarkdownHttpResponse extends HttpResponse { + + private int headerOffset = 2; + + @Override + public MarkdownHttpResponse status(String status) { + addLine("```http"); + addLine("HTTP/1.1 %s".formatted(status)); + addLine(""); + addLine("```"); + return this; + } + + @Override + public MarkdownHttpResponse header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + private static class SnippetContent { + + private List lines = new ArrayList<>(); + + protected void addLine(String line) { + this.lines.add(line); + } + + protected void addLine(int index, String line) { + this.lines.add(determineIndex(index), line); + } + + private int determineIndex(int index) { + if (index >= 0) { + return index; + } + return index + this.lines.size(); + } + + protected String getLinesAsString() { + StringWriter writer = new StringWriter(); + Iterator iterator = this.lines.iterator(); + while (iterator.hasNext()) { + writer.append(String.format("%s", iterator.next())); + if (iterator.hasNext()) { + writer.append(String.format("%n")); + } + } + return writer.toString(); + } + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/CapturedOutput.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/CapturedOutput.java similarity index 77% rename from spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/CapturedOutput.java rename to spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/CapturedOutput.java index 699864f5f..d3aaed82c 100644 --- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/CapturedOutput.java +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/CapturedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,11 @@ * limitations under the License. */ -package org.springframework.restdocs.testfixtures; +package org.springframework.restdocs.testfixtures.jupiter; /** * Provides access to {@link System#out System.out} and {@link System#err System.err} - * output that has been captured by the {@link OutputCaptureRule}. Can be used to apply - * assertions using AssertJ. For example:
    - * assertThat(output).contains("started"); // Checks all output
    - * assertThat(output.getErr()).contains("failed"); // Only checks System.err
    - * assertThat(output.getOut()).contains("ok"); // Only checks System.out
    - * 
    + * output that has been captured. * * @author Madhura Bhave * @author Phillip Webb diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OperationBuilder.java similarity index 93% rename from spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java rename to spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OperationBuilder.java index e3d3d40c0..97c9ebcdd 100644 --- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.testfixtures; +package org.springframework.restdocs.testfixtures.jupiter; import java.io.File; import java.net.URI; @@ -26,8 +26,6 @@ import java.util.Map; import java.util.Set; -import org.junit.runners.model.Statement; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -59,21 +57,27 @@ * * @author Andy Wilkinson */ -public class OperationBuilder extends OperationTestRule { +public class OperationBuilder { private final Map attributes = new HashMap<>(); - private OperationResponseBuilder responseBuilder; - - private String name; + private final File outputDirectory; - private File outputDirectory; + private final String name; private final TemplateFormat templateFormat; + private OperationResponseBuilder responseBuilder; + private OperationRequestBuilder requestBuilder; - public OperationBuilder(TemplateFormat templateFormat) { + OperationBuilder(File outputDirectory, String name) { + this(outputDirectory, name, null); + } + + OperationBuilder(File outputDirectory, String name, TemplateFormat templateFormat) { + this.outputDirectory = outputDirectory; + this.name = name; this.templateFormat = templateFormat; } @@ -92,13 +96,6 @@ public OperationBuilder attribute(String name, Object value) { return this; } - private void prepare(String operationName, File outputDirectory) { - this.name = operationName; - this.outputDirectory = outputDirectory; - this.requestBuilder = null; - this.attributes.clear(); - } - public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { Map templateContext = new HashMap<>(); @@ -127,12 +124,6 @@ private RestDocumentationContext createContext() { return context; } - @Override - public Statement apply(Statement base, File outputDirectory, String operationName) { - prepare(operationName, outputDirectory); - return base; - } - /** * Basic builder API for creating an {@link OperationRequest}. */ diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCapture.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCapture.java similarity index 94% rename from spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCapture.java rename to spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCapture.java index eef8d6a39..385dd49cf 100644 --- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OutputCapture.java +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCapture.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.testfixtures; +package org.springframework.restdocs.testfixtures.jupiter; import java.io.IOException; import java.io.OutputStream; @@ -35,8 +35,6 @@ * @author Madhura Bhave * @author Phillip Webb * @author Andy Wilkinson - * @author Sam Brannen - * @see OutputCaptureRule */ class OutputCapture implements CapturedOutput { @@ -61,7 +59,7 @@ public boolean equals(Object obj) { if (obj == this) { return true; } - if (obj instanceof CapturedOutput || obj instanceof CharSequence) { + if (obj instanceof CharSequence) { return getAll().equals(obj.toString()); } return false; @@ -123,17 +121,17 @@ private String get(Predicate filter) { } /** - * A capture session that captures {@link System#out System.out} and {@link System#out + * A capture session that captures {@link System#out System.out} and {@link System#err * System.err}. */ private static class SystemCapture { - private final Object monitor = new Object(); - private final PrintStreamCapture out; private final PrintStreamCapture err; + private final Object monitor = new Object(); + private final List capturedStrings = new ArrayList<>(); SystemCapture() { @@ -195,8 +193,8 @@ PrintStream getParent() { } private static PrintStream getSystemStream(PrintStream printStream) { - while (printStream instanceof PrintStreamCapture) { - printStream = ((PrintStreamCapture) printStream).getParent(); + while (printStream instanceof PrintStreamCapture printStreamCapture) { + printStream = printStreamCapture.getParent(); } return printStream; } diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java new file mode 100644 index 000000000..4dbb2b8ed --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * JUnit Jupiter {@code @Extension} to capture {@link System#out System.out} and + * {@link System#err System.err}. Can be registered for an entire test class or for an + * individual test method through {@link ExtendWith @ExtendWith}. This extension provides + * {@linkplain ParameterResolver parameter resolution} for a {@link CapturedOutput} + * instance which can be used to assert that the correct output was written. + *

    + * To use with {@link ExtendWith @ExtendWith}, inject the {@link CapturedOutput} as an + * argument to your test class constructor, test method, or lifecycle methods: + * + *

    + * @ExtendWith(OutputCaptureExtension.class)
    + * class MyTest {
    + *
    + *     @Test
    + *     void test(CapturedOutput output) {
    + *         System.out.println("ok");
    + *         assertThat(output).contains("ok");
    + *         System.err.println("error");
    + *     }
    + *
    + *     @AfterEach
    + *     void after(CapturedOutput output) {
    + *         assertThat(output.getOut()).contains("ok");
    + *         assertThat(output.getErr()).contains("error");
    + *     }
    + *
    + * }
    + * 
    + * + * @author Madhura Bhave + * @author Phillip Webb + * @author Andy Wilkinson + * @author Sam Brannen + * @see CapturedOutput + */ +public class OutputCaptureExtension + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver { + + OutputCaptureExtension() { + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + getOutputCapture(context).push(); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + getOutputCapture(context).pop(); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + getOutputCapture(context).push(); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + getOutputCapture(context).pop(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CapturedOutput.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return getOutputCapture(extensionContext); + } + + private OutputCapture getOutputCapture(ExtensionContext context) { + return getStore(context).getOrComputeIfAbsent(OutputCapture.class); + } + + private Store getStore(ExtensionContext context) { + return context.getStore(Namespace.create(getClass())); + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java new file mode 100644 index 000000000..15eb8d39c --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; + +/** + * Signals that a method is a template for a test that renders a snippet. The test will be + * executed once for each of the two supported snippet formats (Asciidoctor and Markdown). + *

    + * A rendered snippet test method can inject the following types: + *

      + *
    • {@link OperationBuilder}
    • + *
    • {@link AssertableSnippets}
    • + *
    + * + * @author Andy Wilkinson + */ +@TestTemplate +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(RenderedSnippetTestExtension.class) +public @interface RenderedSnippetTest { + + /** + * The snippet formats to render. + * @return the formats + */ + Format[] format() default { Format.ASCIIDOCTOR, Format.MARKDOWN }; + + enum Format { + + /** + * Asciidoctor snippet format. + */ + ASCIIDOCTOR(TemplateFormats.asciidoctor()), + + /** + * Markdown snippet format. + */ + MARKDOWN(TemplateFormats.markdown()); + + private final TemplateFormat templateFormat; + + Format(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + TemplateFormat templateFormat() { + return this.templateFormat; + } + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java new file mode 100644 index 000000000..34bb83ef9 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java @@ -0,0 +1,155 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.File; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.platform.commons.util.AnnotationUtils; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest.Format; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * {@link TestTemplateInvocationContextProvider} for + * {@link RenderedSnippetTest @RenderedSnippetTest} and + * {@link SnippetTemplate @SnippetTemplate}. + * + * @author Andy Wilkinson + */ +class RenderedSnippetTestExtension implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return AnnotationUtils.findAnnotation(context.getRequiredTestMethod(), RenderedSnippetTest.class) + .map((renderedSnippetTest) -> Stream.of(renderedSnippetTest.format()) + .map(Format::templateFormat) + .map(SnippetTestInvocationContext::new) + .map(TestTemplateInvocationContext.class::cast)) + .orElseThrow(); + } + + static class SnippetTestInvocationContext implements TestTemplateInvocationContext { + + private final TemplateFormat templateFormat; + + SnippetTestInvocationContext(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + @Override + public List getAdditionalExtensions() { + return List.of(new RenderedSnippetTestParameterResolver(this.templateFormat)); + } + + @Override + public String getDisplayName(int invocationIndex) { + return this.templateFormat.getId(); + } + + } + + static class RenderedSnippetTestParameterResolver implements ParameterResolver { + + private final TemplateFormat templateFormat; + + RenderedSnippetTestParameterResolver(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Class parameterType = parameterContext.getParameter().getType(); + return AssertableSnippets.class.equals(parameterType) || OperationBuilder.class.equals(parameterType) + || TemplateFormat.class.equals(parameterType); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + Class parameterType = parameterContext.getParameter().getType(); + if (AssertableSnippets.class.equals(parameterType)) { + return getStore(extensionContext).getOrComputeIfAbsent(AssertableSnippets.class, + (key) -> new AssertableSnippets(determineOutputDirectory(extensionContext), + determineOperationName(extensionContext), this.templateFormat)); + } + if (TemplateFormat.class.equals(parameterType)) { + return this.templateFormat; + } + return getStore(extensionContext).getOrComputeIfAbsent(OperationBuilder.class, (key) -> { + OperationBuilder operationBuilder = new OperationBuilder(determineOutputDirectory(extensionContext), + determineOperationName(extensionContext), this.templateFormat); + AnnotationUtils.findAnnotation(extensionContext.getRequiredTestMethod(), SnippetTemplate.class) + .ifPresent((snippetTemplate) -> { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource(snippetTemplate.snippet())) + .willReturn(snippetResource(snippetTemplate.template(), this.templateFormat)); + operationBuilder.attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)); + }); + + return operationBuilder; + }); + } + + private Store getStore(ExtensionContext extensionContext) { + return extensionContext.getStore(Namespace.create(getClass())); + } + + private File determineOutputDirectory(ExtensionContext extensionContext) { + return new File("build/" + extensionContext.getRequiredTestClass().getSimpleName()); + } + + private String determineOperationName(ExtensionContext extensionContext) { + String operationName = extensionContext.getRequiredTestMethod().getName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + return operationName; + } + + private FileSystemResource snippetResource(String name, TemplateFormat templateFormat) { + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + templateFormat.getId() + "/" + name + ".snippet"); + } + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java new file mode 100644 index 000000000..a07e34ce7 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Customizes the template that will be used when rendering a snippet in a + * {@link RenderedSnippetTest rendered snippet test}. + * + * @author Andy Wilkinson + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface SnippetTemplate { + + /** + * The name of the snippet whose template should be customized. + * @return the snippet name + */ + String snippet(); + + /** + * The custom template to use when rendering the snippet. + * @return the custom template + */ + String template(); + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java new file mode 100644 index 000000000..291f3e6f7 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Signals that a method is a test of a {@link Snippet}. Typically used to test scenarios + * where a failure occurs before the snippet is rendered. To test snippet rendering, use + * {@link RenderedSnippetTest}. + *

    + * A snippet test method can inject the following types: + *

      + *
    • {@link OperationBuilder}
    • + *
    + * + * @author Andy Wilkinson + */ +@Test +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SnippetTestExtension.class) +public @interface SnippetTest { + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java new file mode 100644 index 000000000..1488902fc --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.File; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * {@link ParameterResolver} for {@link SnippetTest @SnippetTest}. + * + * @author Andy Wilkinson + */ +class SnippetTestExtension implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Class parameterType = parameterContext.getParameter().getType(); + return OperationBuilder.class.equals(parameterType); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return getStore(extensionContext).getOrComputeIfAbsent(OperationBuilder.class, + (key) -> new OperationBuilder(determineOutputDirectory(extensionContext), + determineOperationName(extensionContext))); + } + + private Store getStore(ExtensionContext extensionContext) { + return extensionContext.getStore(Namespace.create(getClass())); + } + + private File determineOutputDirectory(ExtensionContext extensionContext) { + return new File("build/" + extensionContext.getRequiredTestClass().getSimpleName()); + } + + private String determineOperationName(ExtensionContext extensionContext) { + String operationName = extensionContext.getRequiredTestMethod().getName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + return operationName; + } + +} diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index d163b7ddc..875d74148 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -16,8 +16,8 @@ dependencies { internal(platform(project(":spring-restdocs-platform"))) testImplementation(testFixtures(project(":spring-restdocs-core"))) - testImplementation("junit:junit") - testImplementation("org.assertj:assertj-core") - testImplementation("org.hamcrest:hamcrest-library") - testImplementation("org.mockito:mockito-core") +} + +tasks.named("test") { + useJUnitPlatform() } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 5122510a2..c0d3c1fc3 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -23,7 +23,7 @@ import java.util.Iterator; import jakarta.servlet.http.Part; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -46,19 +46,19 @@ * * @author Andy Wilkinson */ -public class MockMvcRequestConverterTests { +class MockMvcRequestConverterTests { private final MockMvcRequestConverter factory = new MockMvcRequestConverter(); @Test - public void httpRequest() { + void httpRequest() { OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void httpRequestWithCustomPort() { + void httpRequestWithCustomPort() { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); mockRequest.setServerPort(8080); OperationRequest request = this.factory.convert(mockRequest); @@ -67,14 +67,14 @@ public void httpRequestWithCustomPort() { } @Test - public void requestWithContextPath() { + void requestWithContextPath() { OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo/bar").contextPath("/foo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo/bar")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void requestWithHeaders() { + void requestWithHeaders() { OperationRequest request = createOperationRequest( MockMvcRequestBuilders.get("/foo").header("a", "alpha", "apple").header("b", "bravo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); @@ -84,7 +84,7 @@ public void requestWithHeaders() { } @Test - public void requestWithCookies() { + void requestWithCookies() { OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo") .cookie(new jakarta.servlet.http.Cookie("cookieName1", "cookieVal1"), new jakarta.servlet.http.Cookie("cookieName2", "cookieVal2"))); @@ -104,7 +104,7 @@ public void requestWithCookies() { } @Test - public void httpsRequest() { + void httpsRequest() { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(443); @@ -114,7 +114,7 @@ public void httpsRequest() { } @Test - public void httpsRequestWithCustomPort() { + void httpsRequestWithCustomPort() { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(8443); @@ -124,7 +124,7 @@ public void httpsRequestWithCustomPort() { } @Test - public void getRequestWithParametersProducesUriWithQueryString() { + void getRequestWithParametersProducesUriWithQueryString() { OperationRequest request = createOperationRequest( MockMvcRequestBuilders.get("/foo").param("a", "alpha", "apple").param("b", "br&vo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo")); @@ -132,7 +132,7 @@ public void getRequestWithParametersProducesUriWithQueryString() { } @Test - public void getRequestWithQueryString() { + void getRequestWithQueryString() { MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/foo?a=alpha&b=bravo"); OperationRequest request = createOperationRequest(builder); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&b=bravo")); @@ -140,7 +140,7 @@ public void getRequestWithQueryString() { } @Test - public void postRequestWithParametersCreatesFormUrlEncodedContent() { + void postRequestWithParametersCreatesFormUrlEncodedContent() { OperationRequest request = createOperationRequest( MockMvcRequestBuilders.post("/foo").param("a", "alpha", "apple").param("b", "br&vo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); @@ -150,7 +150,7 @@ public void postRequestWithParametersCreatesFormUrlEncodedContent() { } @Test - public void postRequestWithParametersAndQueryStringCreatesFormUrlEncodedContentWithoutDuplication() { + void postRequestWithParametersAndQueryStringCreatesFormUrlEncodedContentWithoutDuplication() { OperationRequest request = createOperationRequest( MockMvcRequestBuilders.post("/foo?a=alpha").param("a", "apple").param("b", "br&vo")); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha")); @@ -160,7 +160,7 @@ public void postRequestWithParametersAndQueryStringCreatesFormUrlEncodedContentW } @Test - public void mockMultipartFileUpload() { + void mockMultipartFileUpload() { OperationRequest request = createOperationRequest(MockMvcRequestBuilders.multipart("/foo") .file(new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); @@ -175,7 +175,7 @@ public void mockMultipartFileUpload() { } @Test - public void mockMultipartFileUploadWithContentType() { + void mockMultipartFileUploadWithContentType() { OperationRequest request = createOperationRequest(MockMvcRequestBuilders.multipart("/foo") .file(new MockMultipartFile("file", "original", "image/png", new byte[] { 1, 2, 3, 4 }))); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); @@ -189,7 +189,7 @@ public void mockMultipartFileUploadWithContentType() { } @Test - public void requestWithPart() throws IOException { + void requestWithPart() throws IOException { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); Part mockPart = mock(Part.class); given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); @@ -211,7 +211,7 @@ public void requestWithPart() throws IOException { } @Test - public void requestWithPartWithContentType() throws IOException { + void requestWithPartWithContentType() throws IOException { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); Part mockPart = mock(Part.class); given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java index 300649a2f..e13bb03be 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java @@ -20,7 +20,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -37,12 +37,12 @@ * * @author Tomasz Kopczynski */ -public class MockMvcResponseConverterTests { +class MockMvcResponseConverterTests { private final MockMvcResponseConverter factory = new MockMvcResponseConverter(); @Test - public void basicResponse() { + void basicResponse() { MockHttpServletResponse response = new MockHttpServletResponse(); response.setStatus(HttpServletResponse.SC_OK); OperationResponse operationResponse = this.factory.convert(response); @@ -50,7 +50,7 @@ public void basicResponse() { } @Test - public void responseWithCookie() { + void responseWithCookie() { MockHttpServletResponse response = new MockHttpServletResponse(); response.setStatus(HttpServletResponse.SC_OK); Cookie cookie = new Cookie("name", "value"); @@ -66,7 +66,7 @@ public void responseWithCookie() { } @Test - public void responseWithCustomStatus() { + void responseWithCustomStatus() { MockHttpServletResponse response = new MockHttpServletResponse(); response.setStatus(600); OperationResponse operationResponse = this.factory.convert(response); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index a1e3e9a0c..dfb2debc9 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,13 @@ import java.lang.reflect.Method; import java.util.Map; -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.util.ReflectionUtils; @@ -41,24 +42,22 @@ * @author Andy Wilkinson * @author Dmitriy Mayboroda */ -public class MockMvcRestDocumentationConfigurerTests { +@ExtendWith(RestDocumentationExtension.class) +class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - @Test - public void defaultConfiguration() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation) + void defaultConfiguration(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "localhost", 8080); } @Test - public void customScheme() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation).uris() + void customScheme(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() .withScheme("https") .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -66,8 +65,8 @@ public void customScheme() { } @Test - public void customHost() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation).uris() + void customHost(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() .withHost("api.example.com") .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -75,8 +74,8 @@ public void customHost() { } @Test - public void customPort() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation).uris() + void customPort(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() .withPort(8081) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -84,8 +83,8 @@ public void customPort() { } @Test - public void noContentLengthHeaderWhenRequestHasNotContent() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation).uris() + void noContentLengthHeaderWhenRequestHasNotContent(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() .withPort(8081) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -94,8 +93,8 @@ public void noContentLengthHeaderWhenRequestHasNotContent() { @Test @SuppressWarnings("unchecked") - public void uriTemplateFromRequestAttribute() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation) + void uriTemplateFromRequestAttribute(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation) .beforeMockMvcCreated(null, null); this.request.setAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); postProcessor.postProcessRequest(this.request); @@ -106,11 +105,11 @@ public void uriTemplateFromRequestAttribute() { @Test @SuppressWarnings("unchecked") - public void uriTemplateFromRequest() { + void uriTemplateFromRequest(RestDocumentationContextProvider restDocumentation) { Method setUriTemplate = ReflectionUtils.findMethod(MockHttpServletRequest.class, "setUriTemplate", String.class); - Assume.assumeNotNull(setUriTemplate); - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(this.restDocumentation) + Assumptions.assumeFalse(setUriTemplate == null); + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation) .beforeMockMvcCreated(null, null); ReflectionUtils.invokeMethod(setUriTemplate, this.request, "{a}/{b}"); postProcessor.postProcessRequest(this.request); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 2e35c9505..49460f1e3 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,11 +34,10 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.assertj.core.api.Condition; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +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.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -47,7 +46,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; @@ -56,7 +56,7 @@ import org.springframework.restdocs.testfixtures.SnippetConditions.HttpRequestCondition; import org.springframework.restdocs.testfixtures.SnippetConditions.HttpResponseCondition; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -106,29 +106,30 @@ * @author Tomasz Kopczynski * @author Filip Hrisafov */ -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @WebAppConfiguration +@ExtendWith(RestDocumentationExtension.class) @ContextConfiguration(classes = TestConfiguration.class) public class MockMvcRestDocumentationIntegrationTests { - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + private RestDocumentationContextProvider restDocumentation; @Autowired private WebApplicationContext context; - @Before - public void deleteSnippets() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.restDocumentation = restDocumentation; FileSystemUtils.deleteRecursively(new File("build/generated-snippets")); } - @After - public void clearOutputDirSystemProperty() { + @AfterEach + void clearOutputDirSystemProperty() { System.clearProperty("org.springframework.restdocs.outputDir"); } @Test - public void basicSnippetGeneration() throws Exception { + void basicSnippetGeneration() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation).snippets().withEncoding("UTF-8")) .build(); @@ -140,7 +141,19 @@ public void basicSnippetGeneration() throws Exception { } @Test - public void markdownSnippetGeneration() throws Exception { + void getRequestWithBody() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation).snippets().withEncoding("UTF-8")) + .build(); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON).content("some body content")) + .andExpect(status().isOk()) + .andDo(document("get-request-with-body")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/get-request-with-body"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } + + @Test + void markdownSnippetGeneration() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation).snippets() .withEncoding("UTF-8") @@ -154,7 +167,7 @@ public void markdownSnippetGeneration() throws Exception { } @Test - public void curlSnippetWithContent() throws Exception { + void curlSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -168,7 +181,7 @@ public void curlSnippetWithContent() throws Exception { } @Test - public void curlSnippetWithCookies() throws Exception { + void curlSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -182,7 +195,7 @@ public void curlSnippetWithCookies() throws Exception { } @Test - public void curlSnippetWithQueryStringOnPost() throws Exception { + void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -196,7 +209,7 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { } @Test - public void curlSnippetWithEmptyParameterQueryString() throws Exception { + void curlSnippetWithEmptyParameterQueryString() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -210,7 +223,7 @@ public void curlSnippetWithEmptyParameterQueryString() throws Exception { } @Test - public void curlSnippetWithContentAndParametersOnPost() throws Exception { + void curlSnippetWithContentAndParametersOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -224,7 +237,7 @@ public void curlSnippetWithContentAndParametersOnPost() throws Exception { } @Test - public void httpieSnippetWithContent() throws Exception { + void httpieSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -237,7 +250,7 @@ public void httpieSnippetWithContent() throws Exception { } @Test - public void httpieSnippetWithCookies() throws Exception { + void httpieSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -251,7 +264,7 @@ public void httpieSnippetWithCookies() throws Exception { } @Test - public void httpieSnippetWithQueryStringOnPost() throws Exception { + void httpieSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -265,7 +278,7 @@ public void httpieSnippetWithQueryStringOnPost() throws Exception { } @Test - public void httpieSnippetWithContentAndParametersOnPost() throws Exception { + void httpieSnippetWithContentAndParametersOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -280,7 +293,7 @@ public void httpieSnippetWithContentAndParametersOnPost() throws Exception { } @Test - public void linksSnippet() throws Exception { + void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -293,7 +306,7 @@ public void linksSnippet() throws Exception { } @Test - public void pathParametersSnippet() throws Exception { + void pathParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -305,7 +318,7 @@ public void pathParametersSnippet() throws Exception { } @Test - public void queryParametersSnippet() throws Exception { + void queryParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -317,7 +330,7 @@ public void queryParametersSnippet() throws Exception { } @Test - public void requestFieldsSnippet() throws Exception { + void requestFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -329,7 +342,7 @@ public void requestFieldsSnippet() throws Exception { } @Test - public void requestPartsSnippet() throws Exception { + void requestPartsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -341,7 +354,7 @@ public void requestPartsSnippet() throws Exception { } @Test - public void responseFieldsSnippet() throws Exception { + void responseFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -354,7 +367,7 @@ public void responseFieldsSnippet() throws Exception { } @Test - public void responseWithSetCookie() throws Exception { + void responseWithSetCookie() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -368,7 +381,7 @@ public void responseWithSetCookie() throws Exception { } @Test - public void parameterizedOutputDirectory() throws Exception { + void parameterizedOutputDirectory() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -380,7 +393,7 @@ public void parameterizedOutputDirectory() throws Exception { } @Test - public void multiStep() throws Exception { + void multiStep() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(document("{method-name}-{step}")) @@ -398,7 +411,7 @@ public void multiStep() throws Exception { } @Test - public void alwaysDoWithAdditionalSnippets() throws Exception { + void alwaysDoWithAdditionalSnippets() throws Exception { RestDocumentationResultHandler documentation = document("{method-name}-{step}"); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) @@ -412,7 +425,7 @@ public void alwaysDoWithAdditionalSnippets() throws Exception { } @Test - public void preprocessedRequest() throws Exception { + void preprocessedRequest() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -455,7 +468,7 @@ public void preprocessedRequest() throws Exception { } @Test - public void defaultPreprocessedRequest() throws Exception { + void defaultPreprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() @@ -486,7 +499,7 @@ public void defaultPreprocessedRequest() throws Exception { } @Test - public void preprocessedResponse() throws Exception { + void preprocessedResponse() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -515,7 +528,7 @@ public void preprocessedResponse() throws Exception { } @Test - public void defaultPreprocessedResponse() throws Exception { + void defaultPreprocessedResponse() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() @@ -537,7 +550,7 @@ public void defaultPreprocessedResponse() throws Exception { } @Test - public void customSnippetTemplate() throws Exception { + void customSnippetTemplate() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -559,7 +572,7 @@ public void customSnippetTemplate() throws Exception { } @Test - public void customContextPath() throws Exception { + void customContextPath() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); @@ -573,7 +586,7 @@ public void customContextPath() throws Exception { } @Test - public void exceptionShouldBeThrownWhenCallDocumentMockMvcNotConfigured() { + void exceptionShouldBeThrownWhenCallDocumentMockMvcNotConfigured() { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); assertThatThrownBy(() -> mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andDo(document("basic"))) .isInstanceOf(IllegalStateException.class) @@ -583,7 +596,7 @@ public void exceptionShouldBeThrownWhenCallDocumentMockMvcNotConfigured() { } @Test - public void exceptionShouldBeThrownWhenCallDocumentSnippetsMockMvcNotConfigured() { + void exceptionShouldBeThrownWhenCallDocumentSnippetsMockMvcNotConfigured() { RestDocumentationResultHandler documentation = document("{method-name}-{step}"); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); assertThatThrownBy(() -> mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) @@ -594,7 +607,7 @@ public void exceptionShouldBeThrownWhenCallDocumentSnippetsMockMvcNotConfigured( } @Test - public void multiPart() throws Exception { + void multiPart() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index b6a25d46e..f689778a6 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.net.URI; import jakarta.servlet.ServletContext; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; @@ -44,97 +44,97 @@ * @author Andy Wilkinson * */ -public class RestDocumentationRequestBuildersTests { +class RestDocumentationRequestBuildersTests { private final ServletContext servletContext = new MockServletContext(); @Test - public void getTemplate() { + void getTemplate() { assertTemplate(get("/{template}", "t"), HttpMethod.GET); } @Test - public void getUri() { + void getUri() { assertUri(get(URI.create("/uri")), HttpMethod.GET); } @Test - public void postTemplate() { + void postTemplate() { assertTemplate(post("/{template}", "t"), HttpMethod.POST); } @Test - public void postUri() { + void postUri() { assertUri(post(URI.create("/uri")), HttpMethod.POST); } @Test - public void putTemplate() { + void putTemplate() { assertTemplate(put("/{template}", "t"), HttpMethod.PUT); } @Test - public void putUri() { + void putUri() { assertUri(put(URI.create("/uri")), HttpMethod.PUT); } @Test - public void patchTemplate() { + void patchTemplate() { assertTemplate(patch("/{template}", "t"), HttpMethod.PATCH); } @Test - public void patchUri() { + void patchUri() { assertUri(patch(URI.create("/uri")), HttpMethod.PATCH); } @Test - public void deleteTemplate() { + void deleteTemplate() { assertTemplate(delete("/{template}", "t"), HttpMethod.DELETE); } @Test - public void deleteUri() { + void deleteUri() { assertUri(delete(URI.create("/uri")), HttpMethod.DELETE); } @Test - public void optionsTemplate() { + void optionsTemplate() { assertTemplate(options("/{template}", "t"), HttpMethod.OPTIONS); } @Test - public void optionsUri() { + void optionsUri() { assertUri(options(URI.create("/uri")), HttpMethod.OPTIONS); } @Test - public void headTemplate() { + void headTemplate() { assertTemplate(head("/{template}", "t"), HttpMethod.HEAD); } @Test - public void headUri() { + void headUri() { assertUri(head(URI.create("/uri")), HttpMethod.HEAD); } @Test - public void requestTemplate() { + void requestTemplate() { assertTemplate(request(HttpMethod.GET, "/{template}", "t"), HttpMethod.GET); } @Test - public void requestUri() { + void requestUri() { assertUri(request(HttpMethod.GET, URI.create("/uri")), HttpMethod.GET); } @Test - public void multipartTemplate() { + void multipartTemplate() { assertTemplate(multipart("/{template}", "t"), HttpMethod.POST); } @Test - public void multipartUri() { + void multipartUri() { assertUri(multipart(URI.create("/uri")), HttpMethod.POST); } diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 28f64f144..a4928f8e0 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -15,6 +15,7 @@ dependencies { api("org.apache.pdfbox:pdfbox:2.0.27") api("org.apache.tomcat.embed:tomcat-embed-core:11.0.2") api("org.apache.tomcat.embed:tomcat-embed-el:11.0.2") + api("org.apiguardian:apiguardian-api:1.1.2") api("org.asciidoctor:asciidoctorj:3.0.0") api("org.asciidoctor:asciidoctorj-pdf:2.3.19") api("org.assertj:assertj-core:3.23.1") @@ -22,10 +23,10 @@ dependencies { api("org.hamcrest:hamcrest-library:1.3") api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") api("org.javamoney:moneta:1.4.2") - api("org.junit.jupiter:junit-jupiter-api:5.0.0") } api(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.14.0")) api(enforcedPlatform("io.rest-assured:rest-assured-bom:5.2.1")) api(enforcedPlatform("org.mockito:mockito-bom:4.9.0")) + api(enforcedPlatform("org.junit:junit-bom:5.13.0")) api(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) } diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index a32160170..42a05a522 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -13,13 +13,14 @@ dependencies { internal(platform(project(":spring-restdocs-platform"))) + testCompileOnly("org.apiguardian:apiguardian-api") testImplementation(testFixtures(project(":spring-restdocs-core"))) testImplementation("com.fasterxml.jackson.core:jackson-databind") - testImplementation("junit:junit") testImplementation("org.apache.tomcat.embed:tomcat-embed-core") - testImplementation("org.assertj:assertj-core") - testImplementation("org.hamcrest:hamcrest-library") - testImplementation("org.mockito:mockito-core") +} + +tasks.named("test") { + useJUnitPlatform(); } compatibilityTest { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java index 4d7438a5f..1bef29294 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; import org.assertj.core.api.AbstractAssert; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -34,12 +34,12 @@ * * @author Andy Wilkinson */ -public class RestAssuredParameterBehaviorTests { +class RestAssuredParameterBehaviorTests { private static final MediaType APPLICATION_FORM_URLENCODED_ISO_8859_1 = MediaType .parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=ISO-8859-1"); - @ClassRule + @RegisterExtension public static TomcatServer tomcat = new TomcatServer(); private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); @@ -54,7 +54,7 @@ public class RestAssuredParameterBehaviorTests { }); @Test - public void queryParameterOnGet() { + void queryParameterOnGet() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .get("/query-parameter") @@ -64,7 +64,7 @@ public void queryParameterOnGet() { } @Test - public void queryParameterOnHead() { + void queryParameterOnHead() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .head("/query-parameter") @@ -74,7 +74,7 @@ public void queryParameterOnHead() { } @Test - public void queryParameterOnPost() { + void queryParameterOnPost() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .post("/query-parameter") @@ -84,7 +84,7 @@ public void queryParameterOnPost() { } @Test - public void queryParameterOnPut() { + void queryParameterOnPut() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .put("/query-parameter") @@ -94,7 +94,7 @@ public void queryParameterOnPut() { } @Test - public void queryParameterOnPatch() { + void queryParameterOnPatch() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .patch("/query-parameter") @@ -104,7 +104,7 @@ public void queryParameterOnPatch() { } @Test - public void queryParameterOnDelete() { + void queryParameterOnDelete() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .delete("/query-parameter") @@ -114,7 +114,7 @@ public void queryParameterOnDelete() { } @Test - public void queryParameterOnOptions() { + void queryParameterOnOptions() { this.spec.queryParam("a", "alpha", "apple") .queryParam("b", "bravo") .options("/query-parameter") @@ -124,49 +124,49 @@ public void queryParameterOnOptions() { } @Test - public void paramOnGet() { + void paramOnGet() { this.spec.param("a", "alpha", "apple").param("b", "bravo").get("/query-parameter").then().statusCode(200); assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.GET); } @Test - public void paramOnHead() { + void paramOnHead() { this.spec.param("a", "alpha", "apple").param("b", "bravo").head("/query-parameter").then().statusCode(200); assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.HEAD); } @Test - public void paramOnPost() { + void paramOnPost() { this.spec.param("a", "alpha", "apple").param("b", "bravo").post("/form-url-encoded").then().statusCode(200); assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.POST); } @Test - public void paramOnPut() { + void paramOnPut() { this.spec.param("a", "alpha", "apple").param("b", "bravo").put("/query-parameter").then().statusCode(200); assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PUT); } @Test - public void paramOnPatch() { + void paramOnPatch() { this.spec.param("a", "alpha", "apple").param("b", "bravo").patch("/query-parameter").then().statusCode(200); assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PATCH); } @Test - public void paramOnDelete() { + void paramOnDelete() { this.spec.param("a", "alpha", "apple").param("b", "bravo").delete("/query-parameter").then().statusCode(200); assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.DELETE); } @Test - public void paramOnOptions() { + void paramOnOptions() { this.spec.param("a", "alpha", "apple").param("b", "bravo").options("/query-parameter").then().statusCode(200); assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.OPTIONS); } @Test - public void formParamOnGet() { + void formParamOnGet() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .get("/query-parameter") @@ -176,7 +176,7 @@ public void formParamOnGet() { } @Test - public void formParamOnHead() { + void formParamOnHead() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .head("/form-url-encoded") @@ -186,7 +186,7 @@ public void formParamOnHead() { } @Test - public void formParamOnPost() { + void formParamOnPost() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .post("/form-url-encoded") @@ -196,7 +196,7 @@ public void formParamOnPost() { } @Test - public void formParamOnPut() { + void formParamOnPut() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .put("/form-url-encoded") @@ -206,7 +206,7 @@ public void formParamOnPut() { } @Test - public void formParamOnPatch() { + void formParamOnPatch() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .patch("/form-url-encoded") @@ -216,7 +216,7 @@ public void formParamOnPatch() { } @Test - public void formParamOnDelete() { + void formParamOnDelete() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .delete("/form-url-encoded") @@ -226,7 +226,7 @@ public void formParamOnDelete() { } @Test - public void formParamOnOptions() { + void formParamOnOptions() { this.spec.formParam("a", "alpha", "apple") .formParam("b", "bravo") .options("/form-url-encoded") diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index e7abf05d3..09d411ae2 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -28,8 +28,8 @@ import io.restassured.RestAssured; import io.restassured.specification.FilterableRequestSpecification; import io.restassured.specification.RequestSpecification; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -47,15 +47,15 @@ * * @author Andy Wilkinson */ -public class RestAssuredRequestConverterTests { +class RestAssuredRequestConverterTests { - @ClassRule + @RegisterExtension public static TomcatServer tomcat = new TomcatServer(); private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); @Test - public void requestUri() { + void requestUri() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/foo/bar"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -63,7 +63,7 @@ public void requestUri() { } @Test - public void requestMethod() { + void requestMethod() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.head("/foo/bar"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -71,7 +71,7 @@ public void requestMethod() { } @Test - public void queryStringParameters() { + void queryStringParameters() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).queryParam("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -79,7 +79,7 @@ public void queryStringParameters() { } @Test - public void queryStringFromUrlParameters() { + void queryStringFromUrlParameters() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/?foo=bar&foo=qix"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -87,7 +87,7 @@ public void queryStringFromUrlParameters() { } @Test - public void paramOnGetRequestIsMappedToQueryString() { + void paramOnGetRequestIsMappedToQueryString() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).param("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -95,7 +95,7 @@ public void paramOnGetRequestIsMappedToQueryString() { } @Test - public void headers() { + void headers() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).header("Foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -104,7 +104,7 @@ public void headers() { } @Test - public void headersWithCustomAccept() { + void headersWithCustomAccept() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .header("Foo", "bar") @@ -117,7 +117,7 @@ public void headersWithCustomAccept() { } @Test - public void cookies() { + void cookies() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .cookie("cookie1", "cookieVal1") @@ -138,7 +138,7 @@ public void cookies() { } @Test - public void multipart() { + void multipart() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .multiPart("a", "a.txt", "alpha", null) @@ -156,14 +156,14 @@ public void multipart() { } @Test - public void byteArrayBody() { + void byteArrayBody() { RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()).port(tomcat.getPort()); requestSpec.post(); this.factory.convert((FilterableRequestSpecification) requestSpec); } @Test - public void stringBody() { + void stringBody() { RequestSpecification requestSpec = RestAssured.given().body("body").port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -171,7 +171,7 @@ public void stringBody() { } @Test - public void objectBody() { + void objectBody() { RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")).port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -179,7 +179,7 @@ public void objectBody() { } @Test - public void byteArrayInputStreamBody() { + void byteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) .port(tomcat.getPort()); @@ -189,7 +189,7 @@ public void byteArrayInputStreamBody() { } @Test - public void fileBody() { + void fileBody() { RequestSpecification requestSpec = RestAssured.given() .body(new File("src/test/resources/body.txt")) .port(tomcat.getPort()); @@ -199,7 +199,7 @@ public void fileBody() { } @Test - public void fileInputStreamBody() throws FileNotFoundException { + void fileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); RequestSpecification requestSpec = RestAssured.given().body(inputStream).port(tomcat.getPort()); requestSpec.post(); @@ -209,7 +209,7 @@ public void fileInputStreamBody() throws FileNotFoundException { } @Test - public void multipartWithByteArrayInputStreamBody() { + void multipartWithByteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); @@ -219,7 +219,7 @@ public void multipartWithByteArrayInputStreamBody() { } @Test - public void multipartWithStringBody() { + void multipartWithStringBody() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", "foo"); requestSpec.post(); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); @@ -227,7 +227,7 @@ public void multipartWithStringBody() { } @Test - public void multipartWithByteArrayBody() { + void multipartWithByteArrayBody() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .multiPart("control", "file", "foo".getBytes()); @@ -237,7 +237,7 @@ public void multipartWithByteArrayBody() { } @Test - public void multipartWithFileBody() { + void multipartWithFileBody() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .multiPart(new File("src/test/resources/body.txt")); @@ -247,7 +247,7 @@ public void multipartWithFileBody() { } @Test - public void multipartWithFileInputStreamBody() throws FileNotFoundException { + void multipartWithFileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) @@ -259,7 +259,7 @@ public void multipartWithFileInputStreamBody() throws FileNotFoundException { } @Test - public void multipartWithObjectBody() { + void multipartWithObjectBody() { RequestSpecification requestSpec = RestAssured.given() .port(tomcat.getPort()) .multiPart("control", new ObjectBody("bar")); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java index 3760c6567..22b17601b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import io.restassured.http.Headers; import io.restassured.response.Response; import io.restassured.response.ResponseBody; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.operation.OperationResponse; @@ -33,12 +33,12 @@ * * @author Andy Wilkinson */ -public class RestAssuredResponseConverterTests { +class RestAssuredResponseConverterTests { private final RestAssuredResponseConverter converter = new RestAssuredResponseConverter(); @Test - public void responseWithCustomStatus() { + void responseWithCustomStatus() { Response response = mock(Response.class); given(response.getStatusCode()).willReturn(600); given(response.getHeaders()).willReturn(new Headers()); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index af44ae4cd..8dbf59354 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,11 +22,13 @@ import io.restassured.filter.FilterContext; import io.restassured.specification.FilterableRequestSpecification; import io.restassured.specification.FilterableResponseSpecification; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; @@ -45,10 +47,8 @@ * @author Andy Wilkinson * @author Filip Hrisafov */ -public class RestAssuredRestDocumentationConfigurerTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class RestAssuredRestDocumentationConfigurerTests { private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); @@ -56,17 +56,21 @@ public class RestAssuredRestDocumentationConfigurerTests { private final FilterContext filterContext = mock(FilterContext.class); - private final RestAssuredRestDocumentationConfigurer configurer = new RestAssuredRestDocumentationConfigurer( - this.restDocumentation); + private RestAssuredRestDocumentationConfigurer configurer; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.configurer = new RestAssuredRestDocumentationConfigurer(restDocumentation); + } @Test - public void nextFilterIsCalled() { + void nextFilterIsCalled() { this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); verify(this.filterContext).next(this.requestSpec, this.responseSpec); } @Test - public void configurationIsAddedToTheContext() { + void configurationIsAddedToTheContext() { this.configurer.operationPreprocessors() .withRequestDefaults(Preprocessors.prettyPrint()) .withResponseDefaults(Preprocessors.modifyHeaders().remove("Foo")) diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 740f886c6..89d9c3516 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,14 +29,15 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; import org.assertj.core.api.Condition; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.testfixtures.SnippetConditions; @@ -80,18 +81,16 @@ * @author Tomasz Kopczynski * @author Filip Hrisafov */ -public class RestAssuredRestDocumentationIntegrationTests { +@ExtendWith(RestDocumentationExtension.class) +class RestAssuredRestDocumentationIntegrationTests { - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - @ClassRule - public static TomcatServer tomcat = new TomcatServer(); + @RegisterExtension + private static TomcatServer tomcat = new TomcatServer(); @Test - public void defaultSnippetGeneration() { + void defaultSnippetGeneration(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("default")) .get("/") .then() @@ -101,10 +100,10 @@ public void defaultSnippetGeneration() { } @Test - public void curlSnippetWithContent() { + void curlSnippetWithContent(RestDocumentationContextProvider restDocumentation) { String contentType = "text/plain; charset=UTF-8"; given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("curl-snippet-with-content")) .accept("application/json") .body("content") @@ -120,10 +119,10 @@ public void curlSnippetWithContent() { } @Test - public void curlSnippetWithCookies() { + void curlSnippetWithCookies(RestDocumentationContextProvider restDocumentation) { String contentType = "text/plain; charset=UTF-8"; given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("curl-snippet-with-cookies")) .accept("application/json") .contentType(contentType) @@ -138,9 +137,9 @@ public void curlSnippetWithCookies() { } @Test - public void curlSnippetWithEmptyParameterQueryString() { + void curlSnippetWithEmptyParameterQueryString(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("curl-snippet-with-empty-parameter-query-string")) .accept("application/json") .param("a", "") @@ -155,9 +154,9 @@ public void curlSnippetWithEmptyParameterQueryString() { } @Test - public void curlSnippetWithQueryStringOnPost() { + void curlSnippetWithQueryStringOnPost(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("curl-snippet-with-query-string")) .accept("application/json") .param("foo", "bar") @@ -174,9 +173,9 @@ public void curlSnippetWithQueryStringOnPost() { } @Test - public void linksSnippet() { + void linksSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("links", links(linkWithRel("rel").description("The description")))) .accept("application/json") .get("/") @@ -187,9 +186,9 @@ public void linksSnippet() { } @Test - public void pathParametersSnippet() { + void pathParametersSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("path-parameters", pathParameters(parameterWithName("foo").description("The description")))) .accept("application/json") @@ -201,9 +200,9 @@ public void pathParametersSnippet() { } @Test - public void queryParametersSnippet() { + void queryParametersSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("query-parameters", queryParameters(parameterWithName("foo").description("The description")))) .accept("application/json") @@ -216,9 +215,9 @@ public void queryParametersSnippet() { } @Test - public void requestFieldsSnippet() { + void requestFieldsSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("request-fields", requestFields(fieldWithPath("a").description("The description")))) .accept("application/json") .body("{\"a\":\"alpha\"}") @@ -230,9 +229,9 @@ public void requestFieldsSnippet() { } @Test - public void requestPartsSnippet() { + void requestPartsSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("request-parts", requestParts(partWithName("a").description("The description")))) .multiPart("a", "foo") .post("/upload") @@ -243,9 +242,9 @@ public void requestPartsSnippet() { } @Test - public void responseFieldsSnippet() { + void responseFieldsSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("response-fields", responseFields(fieldWithPath("a").description("The description"), subsectionWithPath("links").description("Links to other resources")))) @@ -258,9 +257,9 @@ public void responseFieldsSnippet() { } @Test - public void parameterizedOutputDirectory() { + void parameterizedOutputDirectory(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("{method-name}")) .get("/") .then() @@ -270,9 +269,9 @@ public void parameterizedOutputDirectory() { } @Test - public void multiStep() { + void multiStep(RestDocumentationContextProvider restDocumentation) { RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) - .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(documentationConfiguration(restDocumentation)) .addFilter(document("{method-name}-{step}")) .build(); given(spec).get("/").then().statusCode(200); @@ -287,10 +286,10 @@ public void multiStep() { } @Test - public void additionalSnippets() { + void additionalSnippets(RestDocumentationContextProvider restDocumentation) { RestDocumentationFilter documentation = document("{method-name}-{step}"); RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) - .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(documentationConfiguration(restDocumentation)) .addFilter(documentation) .build(); given(spec) @@ -304,9 +303,9 @@ public void additionalSnippets() { } @Test - public void responseWithCookie() { + void responseWithCookie(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("set-cookie", preprocessResponse(modifyHeaders().remove(HttpHeaders.DATE).remove(HttpHeaders.CONTENT_TYPE)))) .get("/set-cookie") @@ -322,10 +321,10 @@ public void responseWithCookie() { } @Test - public void preprocessedRequest() { + void preprocessedRequest(RestDocumentationContextProvider restDocumentation) { Pattern pattern = Pattern.compile("(\"alpha\")"); given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .header("a", "alpha") .header("b", "bravo") .contentType("application/json") @@ -356,10 +355,10 @@ public void preprocessedRequest() { } @Test - public void defaultPreprocessedRequest() { + void defaultPreprocessedRequest(RestDocumentationContextProvider restDocumentation) { Pattern pattern = Pattern.compile("(\"alpha\")"); given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .filter(documentationConfiguration(restDocumentation).operationPreprocessors() .withRequestDefaults(prettyPrint(), replacePattern(pattern, "\"<>\""), modifyUris().removePort(), modifyHeaders().remove("a").remove(HttpHeaders.CONTENT_LENGTH))) .header("a", "alpha") @@ -381,10 +380,10 @@ public void defaultPreprocessedRequest() { } @Test - public void preprocessedResponse() { + void preprocessedResponse(RestDocumentationContextProvider restDocumentation) { Pattern pattern = Pattern.compile("(\"alpha\")"); given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("original-response")) .filter(document("preprocessed-response", preprocessResponse(prettyPrint(), maskLinks(), @@ -407,10 +406,10 @@ public void preprocessedResponse() { } @Test - public void defaultPreprocessedResponse() { + void defaultPreprocessedResponse(RestDocumentationContextProvider restDocumentation) { Pattern pattern = Pattern.compile("(\"alpha\")"); given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .filter(documentationConfiguration(restDocumentation).operationPreprocessors() .withResponseDefaults(prettyPrint(), maskLinks(), modifyHeaders().remove("a").remove("Transfer-Encoding").remove("Date").remove("Server"), replacePattern(pattern, "\"<>\""), @@ -432,7 +431,7 @@ public void defaultPreprocessedResponse() { } @Test - public void customSnippetTemplate() throws MalformedURLException { + void customSnippetTemplate(RestDocumentationContextProvider restDocumentation) throws MalformedURLException { ClassLoader classLoader = new URLClassLoader( new URL[] { new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, getClass().getClassLoader()); @@ -441,7 +440,7 @@ public void customSnippetTemplate() throws MalformedURLException { try { given().port(tomcat.getPort()) .accept("application/json") - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .filter(document("custom-snippet-template")) .get("/") .then() @@ -455,7 +454,7 @@ public void customSnippetTemplate() throws MalformedURLException { } @Test - public void exceptionShouldBeThrownWhenCallDocumentRequestSpecificationNotConfigured() { + void exceptionShouldBeThrownWhenCallDocumentRequestSpecificationNotConfigured() { assertThatThrownBy(() -> given().port(tomcat.getPort()).filter(document("default")).get("/")) .isInstanceOf(IllegalStateException.class) .hasMessage("REST Docs configuration not found. Did you forget to add a " @@ -463,7 +462,7 @@ public void exceptionShouldBeThrownWhenCallDocumentRequestSpecificationNotConfig } @Test - public void exceptionShouldBeThrownWhenCallDocumentSnippetsRequestSpecificationNotConfigured() { + void exceptionShouldBeThrownWhenCallDocumentSnippetsRequestSpecificationNotConfigured() { RestDocumentationFilter documentation = document("{method-name}-{step}"); assertThatThrownBy(() -> given().port(tomcat.getPort()) .filter(documentation.document(responseHeaders(headerWithName("a").description("one")))) diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java index e2da4c8f8..f91e1943c 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,46 +32,57 @@ import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; -import org.junit.rules.ExternalResource; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; /** - * {@link ExternalResource} that starts and stops a Tomcat server. + * {@link Extension} that starts and stops a Tomcat server. * * @author Andy Wilkinson */ -class TomcatServer extends ExternalResource { - - private Tomcat tomcat; +class TomcatServer implements BeforeAllCallback, AfterAllCallback { private int port; @Override - protected void before() throws LifecycleException { - this.tomcat = new Tomcat(); - this.tomcat.getConnector().setPort(0); - Context context = this.tomcat.addContext("/", null); - this.tomcat.addServlet("/", "test", new TestServlet()); - context.addServletMappingDecoded("/", "test"); - this.tomcat.addServlet("/", "set-cookie", new CookiesServlet()); - context.addServletMappingDecoded("/set-cookie", "set-cookie"); - this.tomcat.addServlet("/", "query-parameter", new QueryParameterServlet()); - context.addServletMappingDecoded("/query-parameter", "query-parameter"); - this.tomcat.addServlet("/", "form-url-encoded", new FormUrlEncodedServlet()); - context.addServletMappingDecoded("/form-url-encoded", "form-url-encoded"); - this.tomcat.start(); - this.port = this.tomcat.getConnector().getLocalPort(); + public void beforeAll(ExtensionContext extensionContext) { + Store store = extensionContext.getStore(Namespace.create(TomcatServer.class)); + store.getOrComputeIfAbsent(Tomcat.class, (key) -> { + Tomcat tomcat = new Tomcat(); + tomcat.getConnector().setPort(0); + Context context = tomcat.addContext("/", null); + tomcat.addServlet("/", "test", new TestServlet()); + context.addServletMappingDecoded("/", "test"); + tomcat.addServlet("/", "set-cookie", new CookiesServlet()); + context.addServletMappingDecoded("/set-cookie", "set-cookie"); + tomcat.addServlet("/", "query-parameter", new QueryParameterServlet()); + context.addServletMappingDecoded("/query-parameter", "query-parameter"); + tomcat.addServlet("/", "form-url-encoded", new FormUrlEncodedServlet()); + context.addServletMappingDecoded("/form-url-encoded", "form-url-encoded"); + try { + tomcat.start(); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + this.port = tomcat.getConnector().getLocalPort(); + return tomcat; + }); } @Override - protected void after() { - try { - this.tomcat.stop(); - } - catch (LifecycleException ex) { - throw new RuntimeException(ex); + public void afterAll(ExtensionContext extensionContext) throws LifecycleException { + Store store = extensionContext.getStore(Namespace.create(TomcatServer.class)); + Tomcat tomcat = store.get(Tomcat.class, Tomcat.class); + if (tomcat != null) { + tomcat.stop(); } } diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index eadcc88c8..951e41f55 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -13,10 +13,10 @@ dependencies { internal(platform(project(":spring-restdocs-platform"))) testImplementation(testFixtures(project(":spring-restdocs-core"))) - testImplementation("junit:junit") - testImplementation("org.assertj:assertj-core") - testImplementation("org.hamcrest:hamcrest-library") - testImplementation("org.mockito:mockito-core") testRuntimeOnly("org.springframework:spring-context") } + +tasks.named("test") { + useJUnitPlatform(); +} diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java index 5c8782e3d..1087ea411 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java @@ -20,7 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.ContentDisposition; @@ -48,12 +48,12 @@ * * @author Andy Wilkinson */ -public class WebTestClientRequestConverterTests { +class WebTestClientRequestConverterTests { private final WebTestClientRequestConverter converter = new WebTestClientRequestConverter(); @Test - public void httpRequest() { + void httpRequest() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient() .baseUrl("http://localhost/") @@ -69,7 +69,7 @@ public void httpRequest() { } @Test - public void httpRequestWithCustomPort() { + void httpRequestWithCustomPort() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient() .baseUrl("http://localhost:8080/") @@ -85,7 +85,7 @@ public void httpRequestWithCustomPort() { } @Test - public void requestWithHeaders() { + void requestWithHeaders() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/"), (req) -> null)) .configureClient() .baseUrl("http://localhost/") @@ -105,7 +105,7 @@ public void requestWithHeaders() { } @Test - public void httpsRequest() { + void httpsRequest() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient() .baseUrl("https://localhost/") @@ -121,7 +121,7 @@ public void httpsRequest() { } @Test - public void httpsRequestWithCustomPort() { + void httpsRequestWithCustomPort() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient() .baseUrl("https://localhost:8443/") @@ -137,7 +137,7 @@ public void httpsRequestWithCustomPort() { } @Test - public void getRequestWithQueryString() { + void getRequestWithQueryString() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient() .baseUrl("http://localhost/") @@ -153,7 +153,7 @@ public void getRequestWithQueryString() { } @Test - public void postRequestWithFormDataParameters() { + void postRequestWithFormDataParameters() { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.addAll("a", Arrays.asList("alpha", "apple")); parameters.addAll("b", Arrays.asList("br&vo")); @@ -181,7 +181,7 @@ public void postRequestWithFormDataParameters() { } @Test - public void postRequestWithQueryStringParameters() { + void postRequestWithQueryStringParameters() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> { req.body(BodyExtractors.toFormData()).block(); return null; @@ -200,7 +200,7 @@ public void postRequestWithQueryStringParameters() { } @Test - public void postRequestWithQueryStringAndFormDataParameters() { + void postRequestWithQueryStringAndFormDataParameters() { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.addAll("a", Arrays.asList("apple")); ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> { @@ -227,7 +227,7 @@ public void postRequestWithQueryStringAndFormDataParameters() { } @Test - public void postRequestWithNoContentType() { + void postRequestWithNoContentType() { ExchangeResult result = WebTestClient .bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> ServerResponse.ok().build())) .configureClient() @@ -244,7 +244,7 @@ public void postRequestWithNoContentType() { } @Test - public void multipartUpload() { + void multipartUpload() { MultiValueMap multipartData = new LinkedMultiValueMap<>(); multipartData.add("file", new byte[] { 1, 2, 3, 4 }); ExchangeResult result = WebTestClient @@ -274,7 +274,7 @@ public void multipartUpload() { } @Test - public void multipartUploadFromResource() { + void multipartUploadFromResource() { MultiValueMap multipartData = new LinkedMultiValueMap<>(); multipartData.add("file", new ByteArrayResource(new byte[] { 1, 2, 3, 4 }) { @@ -314,7 +314,7 @@ public String getFilename() { } @Test - public void requestWithCookies() { + void requestWithCookies() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient() .baseUrl("http://localhost/") diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java index 474c6feba..e50fc5a5d 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java @@ -18,7 +18,7 @@ import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -40,12 +40,12 @@ * * @author Andy Wilkinson */ -public class WebTestClientResponseConverterTests { +class WebTestClientResponseConverterTests { private final WebTestClientResponseConverter converter = new WebTestClientResponseConverter(); @Test - public void basicResponse() { + void basicResponse() { ExchangeResult result = WebTestClient .bindToRouterFunction( RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.ok().bodyValue("Hello, World!"))) @@ -66,7 +66,7 @@ public void basicResponse() { } @Test - public void responseWithCookie() { + void responseWithCookie() { ExchangeResult result = WebTestClient .bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.ok() @@ -92,7 +92,7 @@ public void responseWithCookie() { } @Test - public void responseWithNonStandardStatusCode() { + void responseWithNonStandardStatusCode() { ExchangeResult result = WebTestClient .bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.status(210).build())) .configureClient() diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java index aaf873166..efa22ebfb 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,14 @@ import java.net.URI; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpMethod; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ExchangeFunction; @@ -38,16 +40,19 @@ * * @author Andy Wilkinson */ -public class WebTestClientRestDocumentationConfigurerTests { +@ExtendWith(RestDocumentationExtension.class) +class WebTestClientRestDocumentationConfigurerTests { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + private WebTestClientRestDocumentationConfigurer configurer; - private final WebTestClientRestDocumentationConfigurer configurer = new WebTestClientRestDocumentationConfigurer( - this.restDocumentation); + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.configurer = new WebTestClientRestDocumentationConfigurer(restDocumentation); + + } @Test - public void configurationCanBeRetrievedButOnlyOnce() { + void configurationCanBeRetrievedButOnlyOnce() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1") .build(); @@ -58,7 +63,7 @@ public void configurationCanBeRetrievedButOnlyOnce() { } @Test - public void requestUriHasDefaultsAppliedWhenItHasNoHost() { + void requestUriHasDefaultsAppliedWhenItHasNoHost() { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test?foo=bar#baz")) .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1") .build(); @@ -70,7 +75,7 @@ public void requestUriHasDefaultsAppliedWhenItHasNoHost() { } @Test - public void requestUriIsNotChangedWhenItHasAHost() { + void requestUriIsNotChangedWhenItHasAHost() { ClientRequest request = ClientRequest .create(HttpMethod.GET, URI.create("https://api.example.com:4567/test?foo=bar#baz")) .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1") diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java index 863e95b5c..30db5c4a6 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,15 +30,16 @@ import java.util.stream.Stream; import org.assertj.core.api.Condition; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.testfixtures.SnippetConditions; @@ -75,15 +76,13 @@ * * @author Andy Wilkinson */ +@ExtendWith(RestDocumentationExtension.class) public class WebTestClientRestDocumentationIntegrationTests { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - private WebTestClient webTestClient; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { RouterFunction route = RouterFunctions .route(RequestPredicates.GET("/"), (request) -> ServerResponse.status(HttpStatus.OK).body(fromValue(new Person("Jane", "Doe")))) @@ -99,12 +98,12 @@ public void setUp() { this.webTestClient = WebTestClient.bindToRouterFunction(route) .configureClient() .baseUrl("https://api.example.com/") - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .build(); } @Test - public void defaultSnippetGeneration() { + void defaultSnippetGeneration() { File outputDir = new File("build/generated-snippets/default-snippets"); FileSystemUtils.deleteRecursively(outputDir); this.webTestClient.get() @@ -119,7 +118,7 @@ public void defaultSnippetGeneration() { } @Test - public void pathParametersSnippet() { + void pathParametersSnippet() { this.webTestClient.get() .uri("/{foo}/{bar}", "1", "2") .exchange() @@ -136,7 +135,7 @@ public void pathParametersSnippet() { } @Test - public void queryParametersSnippet() { + void queryParametersSnippet() { this.webTestClient.get() .uri("/?a=alpha&b=bravo") .exchange() @@ -153,7 +152,7 @@ public void queryParametersSnippet() { } @Test - public void multipart() { + void multipart() { MultiValueMap multipartData = new LinkedMultiValueMap<>(); multipartData.add("a", "alpha"); multipartData.add("b", "bravo"); @@ -173,7 +172,7 @@ public void multipart() { } @Test - public void responseWithSetCookie() { + void responseWithSetCookie() { this.webTestClient.get() .uri("/set-cookie") .exchange() @@ -187,7 +186,7 @@ public void responseWithSetCookie() { } @Test - public void curlSnippetWithCookies() { + void curlSnippetWithCookies() { this.webTestClient.get() .uri("/") .cookie("cookieName", "cookieVal") @@ -204,7 +203,7 @@ public void curlSnippetWithCookies() { } @Test - public void curlSnippetWithEmptyParameterQueryString() { + void curlSnippetWithEmptyParameterQueryString() { this.webTestClient.get() .uri("/?a=") .accept(MediaType.APPLICATION_JSON) @@ -220,7 +219,7 @@ public void curlSnippetWithEmptyParameterQueryString() { } @Test - public void httpieSnippetWithCookies() { + void httpieSnippetWithCookies() { this.webTestClient.get() .uri("/") .cookie("cookieName", "cookieVal") @@ -237,7 +236,7 @@ public void httpieSnippetWithCookies() { } @Test - public void illegalStateExceptionShouldBeThrownWhenCallDocumentWebClientNotConfigured() { + void illegalStateExceptionShouldBeThrownWhenCallDocumentWebClientNotConfigured() { assertThatThrownBy(() -> this.webTestClient .mutateWith((builder, httpHandlerBuilder, connector) -> builder.filters(List::clear).build()) .get() From b82451e52701743369ca055365ef869f2d97a542 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 17:35:46 +0100 Subject: [PATCH 51/60] Remove support for JUnit 4 Closes gh-958 --- docs/build.gradle | 1 - docs/src/docs/asciidoc/getting-started.adoc | 82 ++----------------- .../CustomDefaultOperationPreprocessors.java | 21 +++-- .../mockmvc/CustomDefaultSnippets.java | 21 +++-- .../com/example/mockmvc/CustomEncoding.java | 21 +++-- .../com/example/mockmvc/CustomFormat.java | 21 +++-- .../mockmvc/CustomUriConfiguration.java | 21 +++-- .../mockmvc/EveryTestPreprocessing.java | 19 ++--- .../ExampleApplicationJUnit5Tests.java | 45 ---------- .../mockmvc/ExampleApplicationTests.java | 27 +++--- .../example/mockmvc/ParameterizedOutput.java | 21 +++-- .../CustomDefaultOperationPreprocessors.java | 21 +++-- .../restassured/CustomDefaultSnippets.java | 21 +++-- .../example/restassured/CustomEncoding.java | 21 +++-- .../com/example/restassured/CustomFormat.java | 21 +++-- .../restassured/EveryTestPreprocessing.java | 23 +++--- .../ExampleApplicationJUnit5Tests.java | 43 ---------- .../restassured/ExampleApplicationTests.java | 21 +++-- .../restassured/ParameterizedOutput.java | 19 ++--- .../CustomDefaultOperationPreprocessors.java | 21 +++-- .../webtestclient/CustomDefaultSnippets.java | 21 +++-- .../example/webtestclient/CustomEncoding.java | 21 +++-- .../example/webtestclient/CustomFormat.java | 21 +++-- .../webtestclient/CustomUriConfiguration.java | 21 +++-- .../webtestclient/EveryTestPreprocessing.java | 23 +++--- .../ExampleApplicationJUnit5Tests.java | 45 ---------- .../ExampleApplicationTests.java | 27 +++--- .../webtestclient/ParameterizedOutput.java | 21 +++-- spring-restdocs-core/build.gradle | 1 - .../restdocs/JUnitRestDocumentation.java | 77 ----------------- .../restdocs/ManualRestDocumentation.java | 8 +- spring-restdocs-platform/build.gradle | 1 - 32 files changed, 244 insertions(+), 554 deletions(-) delete mode 100644 docs/src/test/java/com/example/mockmvc/ExampleApplicationJUnit5Tests.java delete mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java delete mode 100644 docs/src/test/java/com/example/webtestclient/ExampleApplicationJUnit5Tests.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java diff --git a/docs/build.gradle b/docs/build.gradle index 8c08f1800..fefe9a5fa 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -18,7 +18,6 @@ dependencies { testImplementation(project(":spring-restdocs-webtestclient")) testImplementation("jakarta.servlet:jakarta.servlet-api") testImplementation("jakarta.validation:jakarta.validation-api") - testImplementation("junit:junit") testImplementation("org.testng:testng:6.9.10") testImplementation("org.junit.jupiter:junit-jupiter-api") } diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index afa220a01..11be6100d 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -203,8 +203,7 @@ It then produces documentation snippets for the request and the resulting respon ==== Setting up Your Tests Exactly how you set up your tests depends on the test framework that you use. -Spring REST Docs provides first-class support for JUnit 5 and JUnit 4. -JUnit 5 is recommended. +Spring REST Docs provides first-class support for JUnit 5. Other frameworks, such as TestNG, are also supported, although slightly more setup is required. @@ -258,72 +257,6 @@ public class JUnit5ExampleTests { Next, you must provide a `@BeforeEach` method to configure MockMvc or WebTestClient, or REST Assured. The following listings show how to do so: -[source,java,indent=0,role="primary"] -.MockMvc ----- -include::{examples-dir}/com/example/mockmvc/ExampleApplicationJUnit5Tests.java[tags=setup] ----- -<1> The `MockMvc` instance is configured by using a `MockMvcRestDocumentationConfigurer`. -You can obtain an instance of this class from the static `documentationConfiguration()` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. - -[source,java,indent=0,role="secondary"] -.WebTestClient ----- -include::{examples-dir}/com/example/webtestclient/ExampleApplicationJUnit5Tests.java[tags=setup] ----- -<1> The `WebTestClient` instance is configured by adding a `WebTestClientRestDocumentationConfigurer` as an `ExchangeFilterFunction`. -You can obtain an instance of this class from the static `documentationConfiguration()` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`. - -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/ExampleApplicationJUnit5Tests.java[tags=setup] ----- -<1> REST Assured is configured by adding a `RestAssuredRestDocumentationConfigurer` as a `Filter`. -You can obtain an instance of this class from the static `documentationConfiguration()` method on `RestAssuredRestDocumentation` in the `org.springframework.restdocs.restassured` package. - -The configurer applies sensible defaults and also provides an API for customizing the configuration. -See the <> for more information. - - - -[[getting-started-documentation-snippets-setup-junit]] -===== Setting up Your JUnit 4 Tests - -When using JUnit 4, the first step in generating documentation snippets is to declare a `public` `JUnitRestDocumentation` field that is annotated as a JUnit `@Rule`. -The following example shows how to do so: - -[source,java,indent=0] ----- -@Rule -public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); ----- - -By default, the `JUnitRestDocumentation` rule is automatically configured with an output directory based on your project's build tool: - -[cols="2,5"] -|=== -| Build tool | Output directory - -| Maven -| `target/generated-snippets` - -| Gradle -| `build/generated-snippets` -|=== - -You can override the default by providing an output directory when you create the `JUnitRestDocumentation` instance. -The following example shows how to do so: - -[source,java,indent=0] ----- -@Rule -public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom"); ----- - -Next, you must provide an `@Before` method to configure MockMvc or WebTestClient, or REST Assured. -The following examples show how to do so: - [source,java,indent=0,role="primary"] .MockMvc ---- @@ -337,7 +270,7 @@ You can obtain an instance of this class from the static `documentationConfigura ---- include::{examples-dir}/com/example/webtestclient/ExampleApplicationTests.java[tags=setup] ---- -<1> The `WebTestClient` instance is configured by adding a `WebTestclientRestDocumentationConfigurer` as an `ExchangeFilterFunction`. +<1> The `WebTestClient` instance is configured by adding a `WebTestClientRestDocumentationConfigurer` as an `ExchangeFilterFunction`. You can obtain an instance of this class from the static `documentationConfiguration()` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`. [source,java,indent=0,role="secondary"] @@ -353,16 +286,15 @@ See the <> for more information. + [[getting-started-documentation-snippets-setup-manual]] ===== Setting up your tests without JUnit -The configuration when JUnit is not being used is largely similar to when it is being used. -This section describes the key differences. -The {samples}/testng[TestNG sample] also illustrates the approach. +The configuration when JUnit is not being used is a little more involved as the test class must perform some lifecycle management. +The {samples}/testng[TestNG sample] illustrates the approach. -The first difference is that you should use `ManualRestDocumentation` in place of `JUnitRestDocumentation`. -Also, you do not need the `@Rule` annotation. -The following example shows how to use `ManualRestDocumentation`: +First, you need a `ManualRestDocumentation` field. +The following example shows how to define it: [source,java,indent=0] ---- diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java index c83660770..5aec4248d 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -28,21 +29,19 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -public class CustomDefaultOperationPreprocessors { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultOperationPreprocessors { private WebApplicationContext context; @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setup() { + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-operation-preprocessors[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .apply(documentationConfiguration(restDocumentation).operationPreprocessors() .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index e5905a6eb..860e8e1e2 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,12 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -28,10 +29,8 @@ import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomDefaultSnippets { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultSnippets { @Autowired private WebApplicationContext context; @@ -39,11 +38,11 @@ public class CustomDefaultSnippets { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets().withDefaults(curlRequest())) + .apply(documentationConfiguration(restDocumentation).snippets().withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index fbca4c617..7b6d1a42d 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,20 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomEncoding { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomEncoding { @Autowired private WebApplicationContext context; @@ -38,11 +37,11 @@ public class CustomEncoding { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-encoding[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets().withEncoding("ISO-8859-1")) + .apply(documentationConfiguration(restDocumentation).snippets().withEncoding("ISO-8859-1")) .build(); // end::custom-encoding[] } diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index 7427491f0..75a304654 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,12 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -28,10 +29,8 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomFormat { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomFormat { @Autowired private WebApplicationContext context; @@ -39,11 +38,11 @@ public class CustomFormat { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-format[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() + .apply(documentationConfiguration(restDocumentation).snippets() .withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index 7b3a1ef97..7af440b08 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,20 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomUriConfiguration { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomUriConfiguration { @Autowired private WebApplicationContext context; @@ -38,11 +37,11 @@ public class CustomUriConfiguration { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-uri-configuration[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).uris() + .apply(documentationConfiguration(restDocumentation).uris() .withScheme("https") .withHost("example.com") .withPort(443)) diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 7c27c2352..dcf4d44af 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -33,20 +34,18 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@ExtendWith(RestDocumentationExtension.class) public class EveryTestPreprocessing { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - private WebApplicationContext context; // tag::setup[] private MockMvc mockMvc; - @Before - public void setup() { + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .apply(documentationConfiguration(restDocumentation).operationPreprocessors() .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationJUnit5Tests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationJUnit5Tests.java deleted file mode 100644 index c012699e7..000000000 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationJUnit5Tests.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.mockmvc; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - -@ExtendWith(RestDocumentationExtension.class) -class ExampleApplicationJUnit5Tests { - - @SuppressWarnings("unused") - // tag::setup[] - private MockMvc mockMvc; - - @BeforeEach - void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) - .apply(documentationConfiguration(restDocumentation)) // <1> - .build(); - } - // end::setup[] - -} diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 3e6647853..24c0a3f4b 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,33 +16,28 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class ExampleApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class ExampleApplicationTests { @SuppressWarnings("unused") // tag::setup[] private MockMvc mockMvc; - @Autowired - private WebApplicationContext context; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) // <1> + @BeforeEach + void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) // <1> .build(); } // end::setup[] diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index 436643222..831c9a4ae 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -27,10 +28,8 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class ParameterizedOutput { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class ParameterizedOutput { @SuppressWarnings("unused") private MockMvc mockMvc; @@ -38,10 +37,10 @@ public class ParameterizedOutput { private WebApplicationContext context; // tag::parameterized-output[] - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) + .apply(documentationConfiguration(restDocumentation)) .alwaysDo(document("{method-name}/{step}/")) .build(); } diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java index 171723339..2d0fa772d 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,28 +18,27 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomDefaultOperationPreprocessors { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultOperationPreprocessors { @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setup() { + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-operation-preprocessors[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .addFilter(documentationConfiguration(restDocumentation).operationPreprocessors() .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 2edf5a20c..3b6d175f1 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,27 +18,26 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomDefaultSnippets { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultSnippets { @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-snippets[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).snippets().withDefaults(curlRequest())) + .addFilter(documentationConfiguration(restDocumentation).snippets().withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 2beaa7bbe..316b19a21 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,26 +18,25 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomEncoding { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomEncoding { @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-encoding[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).snippets().withEncoding("ISO-8859-1")) + .addFilter(documentationConfiguration(restDocumentation).snippets().withEncoding("ISO-8859-1")) .build(); // end::custom-encoding[] } diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index 33fd0661e..5f690f1b3 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,27 +18,26 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.templates.TemplateFormats; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomFormat { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomFormat { @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-format[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).snippets() + .addFilter(documentationConfiguration(restDocumentation).snippets() .withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index 3257de4f3..741cfdfe9 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,11 @@ import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; @@ -32,25 +33,23 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class EveryTestPreprocessing { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class EveryTestPreprocessing { // tag::setup[] private RequestSpecification spec; - @Before - public void setup() { + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .addFilter(documentationConfiguration(restDocumentation).operationPreprocessors() .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); } // end::setup[] - public void use() { + void use() { // tag::use[] RestAssured.given(this.spec) .filter(document("index", links(linkWithRel("self").description("Canonical self link")))) diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java deleted file mode 100644 index fe83e2a75..000000000 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; - -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -@ExtendWith(RestDocumentationExtension.class) -class ExampleApplicationJUnit5Tests { - - @SuppressWarnings("unused") - // tag::setup[] - private RequestSpecification spec; - - @BeforeEach - void setUp(RestDocumentationContextProvider restDocumentation) { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) // <1> - .build(); - } - // end::setup[] - -} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index 377df1ad5..7affb1ef2 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,25 +18,24 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class ExampleApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class ExampleApplicationTests { @SuppressWarnings("unused") // tag::setup[] private RequestSpecification spec; - @Before - public void setUp() { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) // <1> + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) // <1> .build(); } // end::setup[] diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 228e6188e..7471d1772 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,26 +18,25 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +@ExtendWith(RestDocumentationExtension.class) public class ParameterizedOutput { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - @SuppressWarnings("unused") private RequestSpecification spec; // tag::parameterized-output[] - @Before - public void setUp() { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) + @BeforeEach + public void setUp(RestDocumentationContextProvider restDocumentation) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) .addFilter(document("{method-name}/{step}")) .build(); } diff --git a/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java index cb6d69744..17ee06b44 100644 --- a/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java +++ b/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,34 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class CustomDefaultOperationPreprocessors { +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultOperationPreprocessors { // @formatter:off - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - private ApplicationContext context; @SuppressWarnings("unused") private WebTestClient webTestClient; - @Before - public void setup() { + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-operation-preprocessors[] this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() - .filter(documentationConfiguration(this.restDocumentation) + .filter(documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> diff --git a/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java b/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java index 5910ac713..420a52f49 100644 --- a/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,36 +16,35 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class CustomDefaultSnippets { +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultSnippets { // @formatter:off - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - @Autowired private ApplicationContext context; @SuppressWarnings("unused") private WebTestClient webTestClient; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-snippets[] this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient().filter( - documentationConfiguration(this.restDocumentation) + documentationConfiguration(restDocumentation) .snippets().withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] diff --git a/docs/src/test/java/com/example/webtestclient/CustomEncoding.java b/docs/src/test/java/com/example/webtestclient/CustomEncoding.java index fd8176713..42800d797 100644 --- a/docs/src/test/java/com/example/webtestclient/CustomEncoding.java +++ b/docs/src/test/java/com/example/webtestclient/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,34 +16,33 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class CustomEncoding { +@ExtendWith(RestDocumentationExtension.class) +class CustomEncoding { // @formatter:off - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - @Autowired private ApplicationContext context; @SuppressWarnings("unused") private WebTestClient webTestClient; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-encoding[] this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() - .filter(documentationConfiguration(this.restDocumentation) + .filter(documentationConfiguration(restDocumentation) .snippets().withEncoding("ISO-8859-1")) .build(); // end::custom-encoding[] diff --git a/docs/src/test/java/com/example/webtestclient/CustomFormat.java b/docs/src/test/java/com/example/webtestclient/CustomFormat.java index 9021a39ac..ab57f1846 100644 --- a/docs/src/test/java/com/example/webtestclient/CustomFormat.java +++ b/docs/src/test/java/com/example/webtestclient/CustomFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,34 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class CustomFormat { +@ExtendWith(RestDocumentationExtension.class) +class CustomFormat { // @formatter:off - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - @Autowired private ApplicationContext context; @SuppressWarnings("unused") private WebTestClient webTestClient; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-format[] this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() - .filter(documentationConfiguration(this.restDocumentation) + .filter(documentationConfiguration(restDocumentation) .snippets().withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] diff --git a/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java b/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java index 94d842477..9db88cded 100644 --- a/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,19 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class CustomUriConfiguration { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class CustomUriConfiguration { @SuppressWarnings("unused") private WebTestClient webTestClient; @@ -38,12 +37,12 @@ public class CustomUriConfiguration { private ApplicationContext context; // tag::custom-uri-configuration[] - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl("https://api.example.com/") // <1> - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .build(); } // end::custom-uri-configuration[] diff --git a/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java b/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java index 97240a95e..e6ff7ceb9 100644 --- a/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,12 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; @@ -30,23 +31,21 @@ import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class EveryTestPreprocessing { +@ExtendWith(RestDocumentationExtension.class) +class EveryTestPreprocessing { // @formatter:off - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - private ApplicationContext context; // tag::setup[] private WebTestClient webTestClient; - @Before - public void setup() { + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() - .filter(documentationConfiguration(this.restDocumentation) + .filter(documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> @@ -54,7 +53,7 @@ public void setup() { } // end::setup[] - public void use() { + void use() { // tag::use[] this.webTestClient.get().uri("/").exchange().expectStatus().isOk() .expectBody().consumeWith(document("index", diff --git a/docs/src/test/java/com/example/webtestclient/ExampleApplicationJUnit5Tests.java b/docs/src/test/java/com/example/webtestclient/ExampleApplicationJUnit5Tests.java deleted file mode 100644 index 4739440f5..000000000 --- a/docs/src/test/java/com/example/webtestclient/ExampleApplicationJUnit5Tests.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.webtestclient; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.test.web.reactive.server.WebTestClient; - -import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; - -@ExtendWith(RestDocumentationExtension.class) -class ExampleApplicationJUnit5Tests { - - @SuppressWarnings("unused") - // tag::setup[] - private WebTestClient webTestClient; - - @BeforeEach - void setUp(ApplicationContext applicationContext, RestDocumentationContextProvider restDocumentation) { - this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext) - .configureClient() - .filter(documentationConfiguration(restDocumentation)) // <1> - .build(); - } - // end::setup[] - -} diff --git a/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java b/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java index 0ec72a4ea..3126487ad 100644 --- a/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,33 +16,28 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class ExampleApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class ExampleApplicationTests { @SuppressWarnings("unused") // tag::setup[] private WebTestClient webTestClient; - @Autowired - private ApplicationContext context; - - @Before - public void setUp() { - this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + @BeforeEach + void setUp(ApplicationContext applicationContext, RestDocumentationContextProvider restDocumentation) { + this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext) .configureClient() - .filter(documentationConfiguration(this.restDocumentation)) // <1> + .filter(documentationConfiguration(restDocumentation)) // <1> .build(); } // end::setup[] diff --git a/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java b/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java index 6d1dae319..f37702746 100644 --- a/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,20 @@ package com.example.webtestclient; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -public class ParameterizedOutput { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); +@ExtendWith(RestDocumentationExtension.class) +class ParameterizedOutput { @SuppressWarnings("unused") private WebTestClient webTestClient; @@ -39,11 +38,11 @@ public class ParameterizedOutput { private ApplicationContext context; // tag::parameterized-output[] - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() - .filter(documentationConfiguration(this.restDocumentation)) + .filter(documentationConfiguration(restDocumentation)) .entityExchangeResultConsumer(document("{method-name}/{step}")) .build(); } diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 6832649b4..496bb1a39 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -47,7 +47,6 @@ dependencies { optional(platform(project(":spring-restdocs-platform"))) optional("jakarta.validation:jakarta.validation-api") - optional("junit:junit") optional("org.hibernate.validator:hibernate-validator") optional("org.junit.jupiter:junit-jupiter-api") diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java deleted file mode 100644 index bbf99d94d..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * A JUnit {@link TestRule} used to automatically manage the - * {@link RestDocumentationContext}. - * - * @author Andy Wilkinson - * @since 1.1.0 - */ -public class JUnitRestDocumentation implements RestDocumentationContextProvider, TestRule { - - private final ManualRestDocumentation delegate; - - /** - * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets - * to <gradle/maven build path>/generated-snippet. - */ - public JUnitRestDocumentation() { - this.delegate = new ManualRestDocumentation(); - } - - /** - * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets - * to the given {@code outputDirectory}. - * @param outputDirectory the output directory - */ - public JUnitRestDocumentation(String outputDirectory) { - this.delegate = new ManualRestDocumentation(outputDirectory); - } - - @Override - public Statement apply(final Statement base, final Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - Class testClass = description.getTestClass(); - String methodName = description.getMethodName(); - JUnitRestDocumentation.this.delegate.beforeTest(testClass, methodName); - try { - base.evaluate(); - } - finally { - JUnitRestDocumentation.this.delegate.afterTest(); - } - } - - }; - - } - - @Override - public RestDocumentationContext beforeOperation() { - return this.delegate.beforeOperation(); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index cb56676e5..037189541 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,15 @@ import java.io.File; +import org.junit.jupiter.api.extension.Extension; + /** * {@code ManualRestDocumentation} is used to manually manage the * {@link RestDocumentationContext}. Primarly intended for use with TestNG, but suitable * for use in any environment where manual management of the context is required. *

    - * Users of JUnit should use {@link JUnitRestDocumentation} and take advantage of its - * Rule-based support for automatic management of the context. + * Users of JUnit should use {@link RestDocumentationExtension} and take advantage of its + * {@link Extension}-based support for automatic management of the context. * * @author Andy Wilkinson * @since 1.1.0 diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index a4928f8e0..5e0404db0 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -11,7 +11,6 @@ dependencies { api("com.samskivert:jmustache:$jmustacheVersion") api("jakarta.servlet:jakarta.servlet-api:6.1.0") api("jakarta.validation:jakarta.validation-api:3.1.0") - api("junit:junit:4.13.1") api("org.apache.pdfbox:pdfbox:2.0.27") api("org.apache.tomcat.embed:tomcat-embed-core:11.0.2") api("org.apache.tomcat.embed:tomcat-embed-el:11.0.2") From 7c7daf6c0413ecde6947eb3c10cb51114fee1f7c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 17:58:13 +0100 Subject: [PATCH 52/60] Close Writers in StandardWriterResolverTests The unclosed writers were preventing clean up of the JUnit-managed temporary directory. See gh-959 --- .../restdocs/snippet/StandardWriterResolverTests.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 13f3b6cd2..0df5118d1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -78,8 +78,9 @@ void placeholdersAreResolvedInOperationName() throws IOException { PlaceholderResolver resolver = mock(PlaceholderResolver.class); given(resolver.resolvePlaceholder("a")).willReturn("alpha"); given(this.placeholderResolverFactory.create(context)).willReturn(resolver); - Writer writer = this.resolver.resolve("{a}", "bravo", context); - assertSnippetLocation(writer, new File(outputDirectory, "alpha/bravo.adoc")); + try (Writer writer = this.resolver.resolve("{a}", "bravo", context)) { + assertSnippetLocation(writer, new File(outputDirectory, "alpha/bravo.adoc")); + } } @Test @@ -89,8 +90,9 @@ void placeholdersAreResolvedInSnippetName() throws IOException { PlaceholderResolver resolver = mock(PlaceholderResolver.class); given(resolver.resolvePlaceholder("b")).willReturn("bravo"); given(this.placeholderResolverFactory.create(context)).willReturn(resolver); - Writer writer = this.resolver.resolve("alpha", "{b}", context); - assertSnippetLocation(writer, new File(outputDirectory, "alpha/bravo.adoc")); + try (Writer writer = this.resolver.resolve("alpha", "{b}", context)) { + assertSnippetLocation(writer, new File(outputDirectory, "alpha/bravo.adoc")); + } } private RestDocumentationContext createContext(String outputDir) { From 2fb84a29ac465469919475048014c368c8e3ad2f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 19:08:41 +0100 Subject: [PATCH 53/60] Run CI against latest and LTS Java versions --- .github/workflows/ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b966e5d2..b290208a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,11 +22,9 @@ jobs: - version: 17 toolchain: false - version: 21 - toolchain: true - - version: 22 - toolchain: true - - version: 23 - toolchain: true + toolchain: false + - version: 24 + toolchain: false exclude: - os: name: Linux From 91446847681a78eb1c7a19aa663b7e50fba3ce2d Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 29 May 2025 22:24:43 +0200 Subject: [PATCH 54/60] Support link extraction with official HAL and HAL-FORMS media types Register the HalLinkExtractor for both the official HAL media type (application/vnd.hal+json) and the HAL-FORMS media type (application/prs.hal-forms+json) See gh-965 --- .../hypermedia/ContentTypeLinkExtractor.java | 7 +++++- .../restdocs/hypermedia/HalLinkExtractor.java | 2 ++ .../ContentTypeLinkExtractorTests.java | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java index ef2a9e8ec..20a281de7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -30,6 +30,7 @@ * content type. * * @author Andy Wilkinson + * @author Oliver Drotbohm */ class ContentTypeLinkExtractor implements LinkExtractor { @@ -37,7 +38,11 @@ class ContentTypeLinkExtractor implements LinkExtractor { ContentTypeLinkExtractor() { this.linkExtractors.put(MediaType.APPLICATION_JSON, new AtomLinkExtractor()); - this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, new HalLinkExtractor()); + + LinkExtractor halLinkExtractor = new HalLinkExtractor(); + this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, halLinkExtractor); + this.linkExtractors.put(HalLinkExtractor.VND_HAL_MEDIA_TYPE, halLinkExtractor); + this.linkExtractors.put(HalLinkExtractor.HAL_FORMS_MEDIA_TYPE, halLinkExtractor); } ContentTypeLinkExtractor(Map linkExtractors) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index f93c572a7..82d47f0ba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -34,6 +34,8 @@ class HalLinkExtractor extends AbstractJsonLinkExtractor { static final MediaType HAL_MEDIA_TYPE = new MediaType("application", "hal+json"); + static final MediaType VND_HAL_MEDIA_TYPE = new MediaType("application", "vnd.hal+json"); + static final MediaType HAL_FORMS_MEDIA_TYPE = new MediaType("application", "prs.hal-forms+json"); @Override public Map> extractLinks(Map json) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 2e51b0ac9..f48bcbe43 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -41,6 +43,8 @@ class ContentTypeLinkExtractorTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final String halBody = "{ \"_links\" : { \"someRel\" : { \"href\" : \"someHref\" }} }"; + @Test void extractionFailsWithNullContentType() { assertThatIllegalStateException().isThrownBy(() -> new ContentTypeLinkExtractor() @@ -71,4 +75,22 @@ void extractorCalledWithCompatibleContextType() throws IOException { verify(extractor).extractLinks(response); } + @Test + public void extractsLinksFromVndHalMediaType() throws IOException { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.parseMediaType("application/vnd.hal+json")); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, this.halBody.getBytes()); + Map> links = new ContentTypeLinkExtractor().extractLinks(response); + assertThat(links).containsKey("someRel"); + } + + @Test + public void extractsLinksFromHalFormsMediaType() throws IOException { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.parseMediaType("application/prs.hal-forms+json")); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, this.halBody.getBytes()); + Map> links = new ContentTypeLinkExtractor().extractLinks(response); + assertThat(links).containsKey("someRel"); + } + } From cd16feba50652ad408786d99ea02286d850e72e3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 19:24:18 +0100 Subject: [PATCH 55/60] Polish "Support link extraction with official HAL and HAL-FORMS media types" See gh-965 --- .../restdocs/hypermedia/ContentTypeLinkExtractor.java | 3 +-- .../restdocs/hypermedia/HalLinkExtractor.java | 5 ++++- .../restdocs/hypermedia/ContentTypeLinkExtractorTests.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java index 20a281de7..bdf5772cb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ class ContentTypeLinkExtractor implements LinkExtractor { ContentTypeLinkExtractor() { this.linkExtractors.put(MediaType.APPLICATION_JSON, new AtomLinkExtractor()); - LinkExtractor halLinkExtractor = new HalLinkExtractor(); this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, halLinkExtractor); this.linkExtractors.put(HalLinkExtractor.VND_HAL_MEDIA_TYPE, halLinkExtractor); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index 82d47f0ba..e56f32ba8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,14 @@ * format. * * @author Andy Wilkinson + * @author Oliver Drotbohm */ class HalLinkExtractor extends AbstractJsonLinkExtractor { static final MediaType HAL_MEDIA_TYPE = new MediaType("application", "hal+json"); + static final MediaType VND_HAL_MEDIA_TYPE = new MediaType("application", "vnd.hal+json"); + static final MediaType HAL_FORMS_MEDIA_TYPE = new MediaType("application", "prs.hal-forms+json"); @Override diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index f48bcbe43..eb5875f8b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -76,7 +76,7 @@ void extractorCalledWithCompatibleContextType() throws IOException { } @Test - public void extractsLinksFromVndHalMediaType() throws IOException { + void extractsLinksFromVndHalMediaType() throws IOException { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.parseMediaType("application/vnd.hal+json")); OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, this.halBody.getBytes()); @@ -85,7 +85,7 @@ public void extractsLinksFromVndHalMediaType() throws IOException { } @Test - public void extractsLinksFromHalFormsMediaType() throws IOException { + void extractsLinksFromHalFormsMediaType() throws IOException { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.parseMediaType("application/prs.hal-forms+json")); OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, this.halBody.getBytes()); From 4e363762eb66328ae401a14e5444bb147c8adc0e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 3 Jun 2025 19:28:10 +0100 Subject: [PATCH 56/60] Remove Concourse CI configuration --- ci/README.adoc | 17 -- ci/config/changelog-generator.yml | 23 -- ci/config/release-scripts.yml | 10 - ci/images/README.adoc | 21 -- ci/images/ci-image/Dockerfile | 8 - ci/images/get-jdk-url.sh | 11 - ci/images/setup.sh | 32 --- ci/parameters.yml | 12 - ci/pipeline.yml | 402 --------------------------- ci/scripts/build-project-windows.bat | 4 - ci/scripts/build-project.sh | 9 - ci/scripts/common.sh | 4 - ci/scripts/generate-changelog.sh | 12 - ci/scripts/promote.sh | 17 -- ci/scripts/stage.sh | 50 ---- ci/tasks/build-ci-image.yml | 31 --- ci/tasks/build-project-windows.yml | 15 - ci/tasks/build-project.yml | 19 -- ci/tasks/generate-changelog.yml | 22 -- ci/tasks/promote.yml | 25 -- ci/tasks/stage.yml | 17 -- 21 files changed, 761 deletions(-) delete mode 100644 ci/README.adoc delete mode 100644 ci/config/changelog-generator.yml delete mode 100644 ci/config/release-scripts.yml delete mode 100644 ci/images/README.adoc delete mode 100644 ci/images/ci-image/Dockerfile delete mode 100755 ci/images/get-jdk-url.sh delete mode 100755 ci/images/setup.sh delete mode 100644 ci/parameters.yml delete mode 100644 ci/pipeline.yml delete mode 100755 ci/scripts/build-project-windows.bat delete mode 100755 ci/scripts/build-project.sh delete mode 100644 ci/scripts/common.sh delete mode 100755 ci/scripts/generate-changelog.sh delete mode 100755 ci/scripts/promote.sh delete mode 100755 ci/scripts/stage.sh delete mode 100644 ci/tasks/build-ci-image.yml delete mode 100644 ci/tasks/build-project-windows.yml delete mode 100644 ci/tasks/build-project.yml delete mode 100755 ci/tasks/generate-changelog.yml delete mode 100644 ci/tasks/promote.yml delete mode 100644 ci/tasks/stage.yml diff --git a/ci/README.adoc b/ci/README.adoc deleted file mode 100644 index 691582d42..000000000 --- a/ci/README.adoc +++ /dev/null @@ -1,17 +0,0 @@ -== Concourse pipeline - -Ensure that you've setup the spring-restdocs target and can login - -[source] ----- -$ fly -t spring-restdocs login -n spring-restdocs -c https://ci.spring.io ----- - -The pipeline can be deployed using the following command: - -[source] ----- -$ fly -t spring-restdocs set-pipeline -p spring-restdocs-3.0.x -c ci/pipeline.yml -l ci/parameters.yml ----- - -NOTE: This assumes that you have Vault integration configured with the appropriate secrets. diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml deleted file mode 100644 index 9c7fa3589..000000000 --- a/ci/config/changelog-generator.yml +++ /dev/null @@ -1,23 +0,0 @@ -changelog: - repository: spring-projects/spring-restdocs - sections: - - title: ":star: New Features" - labels: - - "type: enhancement" - - title: ":lady_beetle: Bug Fixes" - labels: - - "type: bug" - - "type: regression" - - title: ":notebook_with_decorative_cover: Documentation" - labels: - - "type: documentation" - - title: ":hammer: Dependency Upgrades" - sort: "title" - labels: - - "type: dependency-upgrade" - issues: - ports: - - label: "status: forward-port" - bodyExpression: 'Forward port of issue #(\d+).*' - - label: "status: back-port" - bodyExpression: 'Back port of issue #(\d+).*' diff --git a/ci/config/release-scripts.yml b/ci/config/release-scripts.yml deleted file mode 100644 index 5199d1818..000000000 --- a/ci/config/release-scripts.yml +++ /dev/null @@ -1,10 +0,0 @@ -logging: - level: - io.spring.concourse: DEBUG -spring: - main: - banner-mode: off -sonatype: - exclude: - - 'build-info\.json' - - 'org/springframework/restdocs/spring-restdocs/.*' diff --git a/ci/images/README.adoc b/ci/images/README.adoc deleted file mode 100644 index 92fa3c2ec..000000000 --- a/ci/images/README.adoc +++ /dev/null @@ -1,21 +0,0 @@ -== CI Images - -These images are used by CI to run the actual builds. - -To build the image locally run the following from this directory: - ----- -$ docker build --no-cache -f /Dockerfile . ----- - -For example - ----- -$ docker build --no-cache -f ci-image/Dockerfile . ----- - -To test run: - ----- -$ docker run -it --entrypoint /bin/bash ----- diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile deleted file mode 100644 index 36d3a5ae2..000000000 --- a/ci/images/ci-image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:focal-20220531 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java17 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh deleted file mode 100755 index 907444426..000000000 --- a/ci/images/get-jdk-url.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -e - -case "$1" in - java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.3.1+2/bellsoft-jdk17.0.3.1+2-linux-amd64.tar.gz" - ;; - *) - echo $"Unknown java version" - exit 1 -esac diff --git a/ci/images/setup.sh b/ci/images/setup.sh deleted file mode 100755 index 6661b0587..000000000 --- a/ci/images/setup.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -set -ex - -########################################################### -# UTILS -########################################################### - -export DEBIAN_FRONTEND=noninteractive -apt-get update -apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq unzip -ln -fs /usr/share/zoneinfo/UTC /etc/localtime -dpkg-reconfigure --frontend noninteractive tzdata -rm -rf /var/lib/apt/lists/* - -curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh - -########################################################### -# JAVA -########################################################### -JDK_URL=$( ./get-jdk-url.sh $1 ) - -mkdir -p /opt/openjdk -cd /opt/openjdk -curl -L ${JDK_URL} | tar zx --strip-components=1 -test -f /opt/openjdk/bin/java -test -f /opt/openjdk/bin/javac - -########################################################### -# GRADLE ENTERPRISE -########################################################### -mkdir ~/.gradle -echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties \ No newline at end of file diff --git a/ci/parameters.yml b/ci/parameters.yml deleted file mode 100644 index e2c41b5ec..000000000 --- a/ci/parameters.yml +++ /dev/null @@ -1,12 +0,0 @@ -github-organization: "spring-projects" -github-repository: "spring-restdocs" -docker-hub-organization: "springci" -artifactory-server: "https://repo.spring.io/" -branch: "main" -milestone: "3.0.x" -build-name: "spring-restdocs" -concourse-url: "https://ci.spring.io/" -task-timeout: 1h00m -registry-mirror-host: docker.repo.spring.io -registry-mirror-username: ((artifactory-username)) -registry-mirror-password: ((artifactory-password)) \ No newline at end of file diff --git a/ci/pipeline.yml b/ci/pipeline.yml deleted file mode 100644 index c98f02243..000000000 --- a/ci/pipeline.yml +++ /dev/null @@ -1,402 +0,0 @@ -anchors: - git-repo-resource-source: &git-repo-resource-source - uri: "https://github.com/((github-organization))/((github-repository)).git" - username: ((github-username)) - password: ((github-password)) - branch: ((branch)) - registry-image-resource-source: ®istry-image-resource-source - username: ((docker-hub-username)) - password: ((docker-hub-password)) - tag: ((milestone)) - gradle-enterprise-task-params: &gradle-enterprise-task-params - GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) - GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) - docker-hub-task-params: &docker-hub-task-params - DOCKER_HUB_USERNAME: ((docker-hub-username)) - DOCKER_HUB_PASSWORD: ((docker-hub-password)) - github-task-params: &github-task-params - GITHUB_REPO: ((github-repository)) - GITHUB_ORGANIZATION: ((github-organization)) - GITHUB_PASSWORD: ((github-ci-release-token)) - GITHUB_USERNAME: ((github-username)) - MILESTONE: ((milestone)) - sontatype-task-params: &sonatype-task-params - SONATYPE_USERNAME: ((s01-user-token)) - SONATYPE_PASSWORD: ((s01-user-token-password)) - SONATYPE_URL: ((sonatype-url)) - SONATYPE_STAGING_PROFILE_ID: ((sonatype-staging-profile-id)) - artifactory-task-params: &artifactory-task-params - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - build-project-task-params: &build-project-task-params - privileged: true - timeout: ((task-timeout)) - file: git-repo/ci/tasks/build-project.yml - params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - <<: *docker-hub-task-params - artifactory-repo-put-params: &artifactory-repo-put-params - signing_key: ((signing-key)) - signing_passphrase: ((signing-passphrase)) - repo: libs-snapshot-local - folder: distribution-repository - build_uri: "https://ci.spring.io/teams/$%7BBUILD_TEAM_NAME%7D/pipelines/$%7BBUILD_PIPELINE_NAME%7D/jobs/$%7BBUILD_JOB_NAME%7D/builds/$%7BBUILD_NAME%7D" - build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" - disable_checksum_uploads: true - threads: 8 - artifact_set: - - include: - - "/**/spring-restdocs-*.zip" - properties: - "zip.type": "docs" - "zip.deployed": "false" - slack-fail-params: &slack-fail-params - text: > - :concourse-failed: - [$TEXT_FILE_CONTENT] - text_file: git-repo/build/build-scan-uri.txt - silent: true - icon_emoji: ":concourse:" - username: concourse-ci - slack-success-params: &slack-success-params - text: > - :concourse-succeeded: - [$TEXT_FILE_CONTENT] - text_file: git-repo/build/build-scan-uri.txt - silent: true - icon_emoji: ":concourse:" - username: concourse-ci - registry-mirror-vars: ®istry-mirror-vars - registry-mirror-host: ((registry-mirror-host)) - registry-mirror-username: ((registry-mirror-username)) - registry-mirror-password: ((registry-mirror-password)) -resource_types: -- name: artifactory-resource - type: registry-image - source: - <<: *registry-image-resource-source - repository: springio/artifactory-resource - tag: 0.0.17 -- name: github-status-resource - type: registry-image - source: - <<: *registry-image-resource-source - repository: dpb587/github-status-resource - tag: master -- name: slack-notification - type: registry-image - source: - <<: *registry-image-resource-source - repository: cfcommunity/slack-notification-resource - tag: latest -- name: github-release - type: registry-image - source: - <<: *registry-image-resource-source - repository: concourse/github-release-resource - tag: 1.7.2 -resources: -- name: git-repo - type: git - icon: github - source: - <<: *git-repo-resource-source -- name: git-repo-windows - type: git - icon: github - source: - <<: *git-repo-resource-source - git_config: - - name: core.autocrlf - value: true -- name: github-pre-release - type: github-release - icon: briefcase-download-outline - source: - owner: ((github-organization)) - repository: ((github-repository)) - access_token: ((github-ci-release-token)) - pre_release: true - release: false -- name: github-release - type: github-release - icon: briefcase-download - source: - owner: ((github-organization)) - repository: ((github-repository)) - access_token: ((github-ci-release-token)) - pre_release: false -- name: ci-images-git-repo - type: git - icon: github - source: - uri: https://github.com/((github-organization))/((github-repository)).git - branch: ((branch)) - paths: ["ci/images/*"] -- name: ci-image - type: registry-image - icon: docker - source: - <<: *registry-image-resource-source - repository: ((docker-hub-organization))/((github-repository))-ci -- name: artifactory-repo - type: artifactory-resource - icon: package-variant - source: - uri: ((artifactory-server)) - username: ((artifactory-username)) - password: ((artifactory-password)) - build_name: ((build-name)) -- name: repo-status-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-organization))/((github-repository)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: build -- name: slack-alert - type: slack-notification - icon: slack - source: - url: ((slack-webhook-url)) -- name: daily - type: time - icon: clock-outline - source: { interval: "24h" } -jobs: -- name: build-ci-images - plan: - - get: ci-images-git-repo - trigger: true - - get: git-repo - - task: build-ci-image - privileged: true - file: git-repo/ci/tasks/build-ci-image.yml - output_mapping: - image: ci-image - vars: - ci-image-name: ci-image - <<: *registry-mirror-vars - - put: ci-image - params: - image: ci-image/image.tar -- name: build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - trigger: true - - put: repo-status-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: build-project - image: ci-image - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-build - params: { state: "success", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-success-params -- name: windows-build - serial: true - plan: - - get: git-repo - resource: git-repo-windows - - get: daily - trigger: true - - do: - - task: build-project - privileged: true - file: git-repo/ci/tasks/build-project-windows.yml - tags: - - WIN64 - timeout: ((task-timeout)) - params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - on_failure: - do: - - put: slack-alert - params: - <<: *slack-fail-params - - put: slack-alert - params: - <<: *slack-success-params -- name: stage-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage.yml - params: - RELEASE_TYPE: M - <<: *gradle-enterprise-task-params - <<: *docker-hub-task-params - - put: artifactory-repo - params: - <<: *artifactory-repo-put-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: stage-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage.yml - params: - RELEASE_TYPE: RC - <<: *gradle-enterprise-task-params - <<: *docker-hub-task-params - - put: artifactory-repo - params: - <<: *artifactory-repo-put-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: stage-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage.yml - params: - RELEASE_TYPE: RELEASE - <<: *gradle-enterprise-task-params - <<: *docker-hub-task-params - - put: artifactory-repo - params: - <<: *artifactory-repo-put-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo -- name: promote-milestone - serial: true - plan: - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-milestone] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote.yml - params: - RELEASE_TYPE: M - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: M - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - vars: - <<: *registry-mirror-vars - - put: github-pre-release - params: - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md -- name: promote-rc - serial: true - plan: - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-rc] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote.yml - params: - RELEASE_TYPE: RC - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RC - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - vars: - <<: *registry-mirror-vars - - put: github-pre-release - params: - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md -- name: promote-release - serial: true - plan: - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-release] - params: - download_artifacts: true - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote.yml - params: - RELEASE_TYPE: RELEASE - <<: *artifactory-task-params - <<: *sonatype-task-params -- name: create-github-release - serial: true - plan: - - get: ci-image - - get: git-repo - - get: artifactory-repo - trigger: true - passed: [promote-release] - params: - download_artifacts: false - save_build_info: true - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RELEASE - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - vars: - <<: *registry-mirror-vars - - put: github-release - params: - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md -groups: -- name: "builds" - jobs: ["build", "windows-build"] -- name: "releases" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] -- name: "ci-images" - jobs: ["build-ci-images"] diff --git a/ci/scripts/build-project-windows.bat b/ci/scripts/build-project-windows.bat deleted file mode 100755 index 3e9e87272..000000000 --- a/ci/scripts/build-project-windows.bat +++ /dev/null @@ -1,4 +0,0 @@ -SET "JAVA_HOME=C:\opt\jdk-17" -SET PATH=%PATH%;C:\Program Files\Git\usr\bin -cd git-repo -.\gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 build diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh deleted file mode 100755 index 3844d1a3d..000000000 --- a/ci/scripts/build-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository -popd > /dev/null diff --git a/ci/scripts/common.sh b/ci/scripts/common.sh deleted file mode 100644 index 85bd12338..000000000 --- a/ci/scripts/common.sh +++ /dev/null @@ -1,4 +0,0 @@ -source /opt/concourse-java.sh - -setup_symlinks -cleanup_maven_repo "org.springframework.restdocs" diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh deleted file mode 100755 index d3d2b97e5..000000000 --- a/ci/scripts/generate-changelog.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -CONFIG_DIR=git-repo/ci/config -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) - -java -jar /github-changelog-generator.jar \ - --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ - ${version} generated-changelog/changelog.md - -echo ${version} > generated-changelog/version -echo v${version} > generated-changelog/tag diff --git a/ci/scripts/promote.sh b/ci/scripts/promote.sh deleted file mode 100755 index bd1600191..000000000 --- a/ci/scripts/promote.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -CONFIG_DIR=git-repo/ci/config - -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } - -echo "Promotion complete" -echo $version > version/version diff --git a/ci/scripts/stage.sh b/ci/scripts/stage.sh deleted file mode 100755 index bfc269019..000000000 --- a/ci/scripts/stage.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -git fetch --tags --all > /dev/null -popd > /dev/null - -git clone git-repo stage-git-repo > /dev/null - -pushd stage-git-repo > /dev/null - -snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties ) -if [[ $RELEASE_TYPE = "M" ]]; then - stageVersion=$( get_next_milestone_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RC" ]]; then - stageVersion=$( get_next_rc_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RELEASE" ]]; then - stageVersion=$( get_next_release $snapshotVersion) - nextVersion=$( bump_version_number $snapshotVersion) -else - echo "Unknown release type $RELEASE_TYPE" >&2; exit 1; -fi - -echo "Staging $stageVersion (next version will be $nextVersion)" -sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties - -git config user.name "Spring Builds" > /dev/null -git config user.email "spring-builds@users.noreply.github.com" > /dev/null -git add gradle.properties > /dev/null -git commit -m"Release v$stageVersion" > /dev/null -git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null - -./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository - -git reset --hard HEAD^ > /dev/null -if [[ $nextVersion != $snapshotVersion ]]; then - echo "Setting next development version (v$nextVersion)" - sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties - git add gradle.properties > /dev/null - git commit -m"Next development version (v$nextVersion)" > /dev/null -fi - -echo "DONE" - -popd > /dev/null diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml deleted file mode 100644 index 4d588c31c..000000000 --- a/ci/tasks/build-ci-image.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -platform: linux -image_resource: - type: registry-image - source: - repository: concourse/oci-build-task - tag: 0.9.0 - registry_mirror: - host: ((registry-mirror-host)) - username: ((registry-mirror-username)) - password: ((registry-mirror-password)) -inputs: -- name: ci-images-git-repo -outputs: -- name: image -caches: -- path: ci-image-cache -params: - CONTEXT: ci-images-git-repo/ci/images - DOCKERFILE: ci-images-git-repo/ci/images/((ci-image-name))/Dockerfile - DOCKER_HUB_AUTH: ((docker-hub-auth)) -run: - path: /bin/sh - args: - - "-c" - - | - mkdir -p /root/.docker - cat > /root/.docker/config.json < Date: Tue, 3 Jun 2025 19:30:44 +0100 Subject: [PATCH 57/60] Remove deprecated APIs Closes gh-967 --- .../operation/preprocess/Preprocessors.java | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index ba741ff14..6a36ed81e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -68,44 +68,6 @@ public static OperationPreprocessor prettyPrint() { return new ContentModifyingOperationPreprocessor(new PrettyPrintingContentModifier()); } - /** - * Returns an {@code OperationPreprocessor} that will remove any header from the - * request or response with a name that is equal to one of the given - * {@code headersToRemove}. - * @param headerNames the header names - * @return the preprocessor - * @deprecated since 3.0.0 in favor of {@link #modifyHeaders()} and - * {@link HeadersModifyingOperationPreprocessor#remove(String)} - * @see String#equals(Object) - */ - @Deprecated - public static OperationPreprocessor removeHeaders(String... headerNames) { - HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); - for (String headerName : headerNames) { - preprocessor.remove(headerName); - } - return preprocessor; - } - - /** - * Returns an {@code OperationPreprocessor} that will remove any headers from the - * request or response with a name that matches one of the given - * {@code headerNamePatterns} regular expressions. - * @param headerNamePatterns the header name patterns - * @return the preprocessor - * @deprecated since 3.0.0 in favor of {@link #modifyHeaders()} and - * {@link HeadersModifyingOperationPreprocessor#removeMatching(String)} - * @see java.util.regex.Matcher#matches() - */ - @Deprecated - public static OperationPreprocessor removeMatchingHeaders(String... headerNamePatterns) { - HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); - for (String headerNamePattern : headerNamePatterns) { - preprocessor.removeMatching(headerNamePattern); - } - return preprocessor; - } - /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. From dfef03e4d41de9862f4a7376645afdf5547583f1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Jun 2025 07:01:31 +0100 Subject: [PATCH 58/60] Publish releases using Central Portal Closes gh-968 --- .../actions/sync-to-maven-central/action.yml | 17 +++++++---------- .github/workflows/release.yml | 5 ++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml index ec3b20d36..bacda7762 100644 --- a/.github/actions/sync-to-maven-central/action.yml +++ b/.github/actions/sync-to-maven-central/action.yml @@ -1,17 +1,14 @@ name: Sync to Maven Central description: 'Syncs a release to Maven Central and waits for it to be available for use' inputs: - jfrog-cli-config-token: - description: 'Config token for the JFrog CLI' - required: true - ossrh-s01-staging-profile: - description: 'Staging profile to use when syncing to Central' + central-token-password: + description: 'Password for authentication with central.sonatype.com' required: true - ossrh-s01-token-password: - description: 'Password for authentication with s01.oss.sonatype.org' + central-token-username: + description: 'Username for authentication with central.sonatype.com' required: true - ossrh-s01-token-username: - description: 'Username for authentication with s01.oss.sonatype.org' + jfrog-cli-config-token: + description: 'Config token for the JFrog CLI' required: true spring-restdocs-version: description: 'Version of Spring REST Docs that is being synced to Central' @@ -27,7 +24,7 @@ runs: shell: bash run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-restdocs-{0}', inputs.spring-restdocs-version) }};buildNumber=${{ github.run_number }}' - name: Sync - uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 + uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0 with: close: true create: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef92b5cb7..5b69c73b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,10 +46,9 @@ jobs: - name: Sync to Maven Central uses: ./.github/actions/sync-to-maven-central with: + central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} + central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }} jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }} - ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }} - ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} spring-restdocs-version: ${{ needs.build-and-stage-release.outputs.version }} promote-release: name: Promote Release From 65c80809d2f581542899c933df5f56f59f3ee42a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Jun 2025 07:27:06 +0100 Subject: [PATCH 59/60] Correct configuration of central-publish-action See gh-968 --- .github/actions/sync-to-maven-central/action.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml index bacda7762..5a264aa2c 100644 --- a/.github/actions/sync-to-maven-central/action.yml +++ b/.github/actions/sync-to-maven-central/action.yml @@ -26,14 +26,8 @@ runs: - name: Sync uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0 with: - close: true - create: true - generate-checksums: true - password: ${{ inputs.ossrh-s01-token-password }} - release: true - staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} - upload: true - username: ${{ inputs.ossrh-s01-token-username }} + token: ${{ inputs.central-token-password }} + token-name: ${{ inputs.central-token-username }} - name: Await uses: ./.github/actions/await-http-resource with: From 7e02c125f197cdd3299a8fa2ff945a73fb71f89c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Jun 2025 07:09:01 +0100 Subject: [PATCH 60/60] Next development version (v3.0.5-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fbd1b3322..e945e0938 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.4-SNAPSHOT +version=3.0.5-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8