From c182630092ad4fd1c14a44bb49bf8e9a4b767bd8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Sep 2021 11:58:56 +0100 Subject: [PATCH 001/198] Start work on 3.0 --- ci/README.adoc | 2 +- gradle.properties | 2 +- samples/junit5/build.gradle | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- samples/web-test-client/build.gradle | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ci/README.adoc b/ci/README.adoc index 396bbcf9..691582d4 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -11,7 +11,7 @@ The pipeline can be deployed using the following command: [source] ---- -$ fly -t spring-restdocs set-pipeline -p spring-restdocs-2.0.x -c ci/pipeline.yml -l ci/parameters.yml +$ 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/gradle.properties b/gradle.properties index 369bc90b..de5af153 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.0.6.BUILD-SNAPSHOT +version=3.0.0-SNAPSHOT org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 org.gradle.parallel=true diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index d25c71bf..edce9ca1 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -23,7 +23,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '2.0.6.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' configurations { asciidoctorExtensions diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 0ab61172..a6db4f1d 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -23,7 +23,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '2.0.6.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' configurations { asciidoctorExtensions diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 5ad049a4..96953f3a 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -22,7 +22,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '2.0.6.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 3ed3bf35..82453e87 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.8 - 2.0.6.BUILD-SNAPSHOT + 3.0.0-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 449997d2..1cb6e7fd 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -23,7 +23,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '2.0.6.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' configurations { asciidoctorExtensions diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index f59974a0..1b981581 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -23,7 +23,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '2.0.6.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' configurations { asciidoctorExtensions diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index 3580fe66..27cbc93b 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -21,7 +21,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '2.0.6.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' configurations { asciidoctorExtensions From d742ad1cec03e782a9686564a80926f12ed574df Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 27 Sep 2021 15:13:53 +0100 Subject: [PATCH 002/198] Upgrade to Gradle 7.2 Closes gh-753 --- gradle/wrapper/gradle-wrapper.properties | 2 +- .../junit5/gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/junit5/gradlew | 2 +- samples/junit5/gradlew.bat | 21 +------ .../gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/rest-assured/gradlew | 2 +- samples/rest-assured/gradlew.bat | 21 +------ .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/web-test-client/gradlew | 53 +++++++++++------- samples/web-test-client/gradlew.bat | 43 +++++++------- 16 files changed, 72 insertions(+), 84 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/junit5/gradle/wrapper/gradle-wrapper.jar b/samples/junit5/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 6656 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 Yte!%o?r4GTrVtxD61Ym!|5fq-1K$0e!T1w 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 diff --git a/samples/junit5/gradle/wrapper/gradle-wrapper.properties b/samples/junit5/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/samples/junit5/gradle/wrapper/gradle-wrapper.properties +++ b/samples/junit5/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/junit5/gradlew b/samples/junit5/gradlew index fbd7c515..4f906e0c 100755 --- a/samples/junit5/gradlew +++ b/samples/junit5/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/samples/junit5/gradlew.bat b/samples/junit5/gradlew.bat index 5093609d..107acd32 100644 --- a/samples/junit5/gradlew.bat +++ b/samples/junit5/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 6656 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 Yte!%o?r4GTrVtxD61Ym!|5fq-1K$0e!T1w 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 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index fbd7c515..4f906e0c 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat index 5093609d..107acd32 100644 --- a/samples/rest-assured/gradlew.bat +++ b/samples/rest-assured/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/web-test-client/gradle/wrapper/gradle-wrapper.jar b/samples/web-test-client/gradle/wrapper/gradle-wrapper.jar index 0d4a9516871afd710a9d84d89e31ba77745607bd..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 54413 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 diff --git a/samples/web-test-client/gradle/wrapper/gradle-wrapper.properties b/samples/web-test-client/gradle/wrapper/gradle-wrapper.properties index 442d9132..ffed3a25 100644 --- a/samples/web-test-client/gradle/wrapper/gradle-wrapper.properties +++ b/samples/web-test-client/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/web-test-client/gradlew b/samples/web-test-client/gradlew index cccdd3d5..4f906e0c 100755 --- a/samples/web-test-client/gradlew +++ b/samples/web-test-client/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 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. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/samples/web-test-client/gradlew.bat b/samples/web-test-client/gradlew.bat index f9553162..107acd32 100644 --- a/samples/web-test-client/gradlew.bat +++ b/samples/web-test-client/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell From 774bed30b98abf01da9d981a80be88fb021fd3e2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Sep 2021 09:20:09 +0100 Subject: [PATCH 003/198] Raise the minimum version of REST Assured to 4.4 Closes gh-752 --- build.gradle | 1 + docs/src/docs/asciidoc/getting-started.adoc | 8 +++---- docs/src/docs/asciidoc/introduction.adoc | 2 +- .../CustomDefaultOperationPreprocessors.java | 2 +- .../restassured/CustomDefaultSnippets.java | 2 +- .../example/restassured/CustomEncoding.java | 2 +- .../com/example/restassured/CustomFormat.java | 2 +- .../restassured/EveryTestPreprocessing.java | 4 ++-- .../ExampleApplicationJUnit5Tests.java | 2 +- .../ExampleApplicationTestNgTests.java | 2 +- .../restassured/ExampleApplicationTests.java | 2 +- .../com/example/restassured/HttpHeaders.java | 2 +- .../com/example/restassured/Hypermedia.java | 2 +- .../example/restassured/InvokeService.java | 2 +- .../restassured/ParameterizedOutput.java | 4 ++-- .../example/restassured/PathParameters.java | 2 +- .../java/com/example/restassured/Payload.java | 2 +- .../restassured/PerTestPreprocessing.java | 2 +- .../restassured/RequestParameters.java | 2 +- .../restassured/RequestPartPayload.java | 2 +- .../com/example/restassured/RequestParts.java | 2 +- .../restassured/RestAssuredSnippetReuse.java | 2 +- samples/rest-assured/build.gradle | 4 +++- .../SampleRestAssuredApplicationTests.java | 6 +++--- settings.gradle | 9 ++++++++ spring-restdocs-platform/build.gradle | 2 +- spring-restdocs-restassured/build.gradle | 8 ------- ...suredOperationPreprocessorsConfigurer.java | 4 ++-- .../RestAssuredRequestConverter.java | 4 ++-- .../RestAssuredResponseConverter.java | 4 ++-- .../RestAssuredRestDocumentation.java | 4 ++-- ...estAssuredRestDocumentationConfigurer.java | 4 ++-- .../RestAssuredSnippetConfigurer.java | 4 ++-- .../RestDocumentationFilter.java | 4 ++-- .../preprocess/RestAssuredPreprocessors.java | 4 ++-- .../UriModifyingOperationPreprocessor.java | 4 ++-- .../operation/preprocess/package-info.java | 4 ++-- .../package-info.java | 4 ++-- .../RestAssuredRequestConverterTests.java | 4 ++-- .../RestAssuredResponseConverterTests.java | 4 ++-- ...suredRestDocumentationConfigurerTests.java | 4 ++-- ...uredRestDocumentationIntegrationTests.java | 21 ++++++++----------- .../TomcatServer.java | 4 ++-- ...riModifyingOperationPreprocessorTests.java | 4 ++-- 44 files changed, 84 insertions(+), 83 deletions(-) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredOperationPreprocessorsConfigurer.java (93%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredRequestConverter.java (98%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredResponseConverter.java (93%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredRestDocumentation.java (97%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredRestDocumentationConfigurer.java (96%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredSnippetConfigurer.java (93%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/RestDocumentationFilter.java (97%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/operation/preprocess/RestAssuredPreprocessors.java (93%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/operation/preprocess/UriModifyingOperationPreprocessor.java (93%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/operation/preprocess/package-info.java (84%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/{restassured3 => restassured}/package-info.java (85%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredRequestConverterTests.java (99%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredResponseConverterTests.java (94%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredRestDocumentationConfigurerTests.java (97%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/{restassured3 => restassured}/RestAssuredRestDocumentationIntegrationTests.java (96%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/{restassured3 => restassured}/TomcatServer.java (97%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/{restassured3 => restassured}/operation/preprocess/UriModifyingOperationPreprocessorTests.java (99%) diff --git a/build.gradle b/build.gradle index 1795f6f2..d40731cc 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ plugins { allprojects { group = "org.springframework.restdocs" repositories { + mavenLocal() mavenCentral() maven { url "https://repo.spring.io/snapshot" } } diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 7332344f..7e586aba 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -79,7 +79,7 @@ Spring REST Docs has the following minimum requirements: * Java 8 * Spring Framework 5 (5.0.2 or later) -Additionally, the `spring-restdocs-restassured` module requires REST Assured 3.0. +Additionally, the `spring-restdocs-restassured` module requires REST Assured 4 (4.4 or later). [[getting-started-build-configuration]] === Build configuration @@ -345,7 +345,7 @@ include::{examples-dir}/com/example/restassured/ExampleApplicationTests.java[tag <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.restassured3` package. +`org.springframework.restdocs.restassured` package. ==== The configurer applies sensible defaults and also provides an API for customizing the @@ -438,7 +438,7 @@ include::{examples-dir}/com/example/restassured/ExampleApplicationJUnit5Tests.ja <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.restassured3` package. +`org.springframework.restdocs.restassured` package. ==== The configurer applies sensible defaults and also provides an API for customizing the @@ -545,7 +545,7 @@ include::{examples-dir}/com/example/restassured/InvokeService.java[tags=invoke-s (which is located beneath the configured output directory). The snippets are written by a `RestDocumentationFilter`. You can obtain an instance of this class from the static `document` method on `RestAssuredRestDocumentation` in the -`org.springframework.restdocs.restassured3` package. +`org.springframework.restdocs.restassured` package. <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. ==== diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 29faddfe..f20d7738 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -13,7 +13,7 @@ configure Spring REST Docs to use Markdown. Spring REST Docs uses snippets produced by tests written with Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or -http://rest-assured.io[REST Assured 3]. This test-driven approach helps to guarantee +http://rest-assured.io[REST Assured 4]. This test-driven approach helps to guarantee the accuracy of your service's documentation. If a snippet is incorrect, the test that produces it fails. diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java index cfcdc90d..cb392c91 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java @@ -25,7 +25,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultOperationPreprocessors { diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index d236d109..3eeb870d 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -24,7 +24,7 @@ import org.springframework.restdocs.JUnitRestDocumentation; import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 8c503086..bcfd0caa 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -23,7 +23,7 @@ import org.springframework.restdocs.JUnitRestDocumentation; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomEncoding { diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index 63bcb6a6..3b6f6482 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -24,7 +24,7 @@ import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomFormat { diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index f92d6e36..6757da31 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -29,8 +29,8 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class EveryTestPreprocessing { diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java index 275e591e..7fa9bf1f 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java @@ -24,7 +24,7 @@ import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; @ExtendWith(RestDocumentationExtension.class) public class ExampleApplicationJUnit5Tests { diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java index 22af139a..d1e07b21 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -25,7 +25,7 @@ import org.springframework.restdocs.ManualRestDocumentation; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class ExampleApplicationTestNgTests { diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index 1df00b12..26c08c59 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -23,7 +23,7 @@ import org.springframework.restdocs.JUnitRestDocumentation; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class ExampleApplicationTests { diff --git a/docs/src/test/java/com/example/restassured/HttpHeaders.java b/docs/src/test/java/com/example/restassured/HttpHeaders.java index 09bcaf68..697acbaf 100644 --- a/docs/src/test/java/com/example/restassured/HttpHeaders.java +++ b/docs/src/test/java/com/example/restassured/HttpHeaders.java @@ -23,7 +23,7 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class HttpHeaders { diff --git a/docs/src/test/java/com/example/restassured/Hypermedia.java b/docs/src/test/java/com/example/restassured/Hypermedia.java index 1d013702..346411b3 100644 --- a/docs/src/test/java/com/example/restassured/Hypermedia.java +++ b/docs/src/test/java/com/example/restassured/Hypermedia.java @@ -23,7 +23,7 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class Hypermedia { diff --git a/docs/src/test/java/com/example/restassured/InvokeService.java b/docs/src/test/java/com/example/restassured/InvokeService.java index 5937b15a..70b884bd 100644 --- a/docs/src/test/java/com/example/restassured/InvokeService.java +++ b/docs/src/test/java/com/example/restassured/InvokeService.java @@ -20,7 +20,7 @@ import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class InvokeService { diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 8626345d..6859417b 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -23,8 +23,8 @@ import org.springframework.restdocs.JUnitRestDocumentation; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class ParameterizedOutput { diff --git a/docs/src/test/java/com/example/restassured/PathParameters.java b/docs/src/test/java/com/example/restassured/PathParameters.java index 0da95f00..c6ca7e44 100644 --- a/docs/src/test/java/com/example/restassured/PathParameters.java +++ b/docs/src/test/java/com/example/restassured/PathParameters.java @@ -22,7 +22,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class PathParameters { diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 22dfb5d7..4c7b9f28 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -29,7 +29,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java index 2f63dfc9..02974491 100644 --- a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java @@ -24,7 +24,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class PerTestPreprocessing { diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/RequestParameters.java index 40a48fa7..229c62be 100644 --- a/docs/src/test/java/com/example/restassured/RequestParameters.java +++ b/docs/src/test/java/com/example/restassured/RequestParameters.java @@ -22,7 +22,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class RequestParameters { diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java index 5dcfc7fc..f7eb05a6 100644 --- a/docs/src/test/java/com/example/restassured/RequestPartPayload.java +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -27,7 +27,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class RequestPartPayload { diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java index 70059b12..32b2e0e9 100644 --- a/docs/src/test/java/com/example/restassured/RequestParts.java +++ b/docs/src/test/java/com/example/restassured/RequestParts.java @@ -22,7 +22,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class RequestParts { diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java index 1668cc5e..c2267c1c 100644 --- a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -22,7 +22,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class RestAssuredSnippetReuse extends SnippetReuse { diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index a6db4f1d..a2afef45 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -24,6 +24,8 @@ ext { } ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' +ext['rest-assured.version'] = '4.4.0' +ext['groovy.version'] = '3.0.8' configurations { asciidoctorExtensions @@ -34,7 +36,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'io.rest-assured:rest-assured:3.0.2' + testImplementation 'io.rest-assured:rest-assured:4.4.0' testImplementation('org.junit.vintage:junit-vintage-engine') { exclude group: 'org.hamcrest', module: 'hamcrest-core' } diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java index 32e7044a..8e1dc7c5 100644 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 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. @@ -20,8 +20,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import org.junit.Before; import org.junit.Rule; diff --git a/settings.gradle b/settings.gradle index 6ed71662..45a29672 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,18 @@ pluginManagement { repositories { + mavenLocal() mavenCentral() maven { url "https://repo.spring.io/plugins-release-local" } + maven { url "https://repo.spring.io/snapshot" } gradlePluginPortal() } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "io.spring.javaformat") { + useModule "io.spring.javaformat:spring-javaformat-gradle-plugin:${requested.version}" + } + } + } } plugins { diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 59fb644d..529dae93 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -14,7 +14,7 @@ dependencies { api("javax.servlet:javax.servlet-api:3.1.0") api("javax.validation:validation-api:2.0.0.Final") api("junit:junit:4.12") - api("io.rest-assured:rest-assured:3.0.7") + api("io.rest-assured:rest-assured:4.4.0") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.assertj:assertj-core:3.11.1") api("org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.18") diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 6b3546d1..26eee409 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,5 +1,4 @@ plugins { - id "io.spring.compatibility-test" version "0.0.1" id "java-library" id "maven-publish" } @@ -22,10 +21,3 @@ dependencies { testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.mockito:mockito-core") } - -compatibilityTest { - dependency("REST Assured") { restAssured -> - restAssured.groupId = "io.rest-assured" - restAssured.versions = ["4.0.0", "4.1.2", "4.2.0", "4.3.1"] - } -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredOperationPreprocessorsConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java similarity index 93% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredOperationPreprocessorsConfigurer.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java index e5d539ec..2b738858 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredOperationPreprocessorsConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import io.restassured.filter.Filter; import io.restassured.filter.FilterContext; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java similarity index 98% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRequestConverter.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 6356e437..8da8f911 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.io.File; import java.io.IOException; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java similarity index 93% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverter.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index e1e31aec..d3888e03 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import io.restassured.http.Header; import io.restassured.response.Response; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java similarity index 97% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentation.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index b2e899b4..fac2b825 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java similarity index 96% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationConfigurer.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 581911b7..3e4f4370 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.util.HashMap; import java.util.Map; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java similarity index 93% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredSnippetConfigurer.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java index 0dbebbdb..5d4bff9b 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredSnippetConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import io.restassured.filter.Filter; import io.restassured.filter.FilterContext; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java similarity index 97% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestDocumentationFilter.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 342d69eb..63539187 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.util.HashMap; import java.util.Map; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java similarity index 93% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/RestAssuredPreprocessors.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java index 0605d76d..86f7213d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/RestAssuredPreprocessors.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3.operation.preprocess; +package org.springframework.restdocs.restassured.operation.preprocess; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java similarity index 93% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/UriModifyingOperationPreprocessor.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index a1ace08d..b7aec38d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3.operation.preprocess; +package org.springframework.restdocs.restassured.operation.preprocess; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java similarity index 84% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/package-info.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java index cdcf9a3a..a0e80251 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/operation/preprocess/package-info.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 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. @@ -18,4 +18,4 @@ * REST Assured-specific support for preprocessing an operation prior to it being * documented. */ -package org.springframework.restdocs.restassured3.operation.preprocess; +package org.springframework.restdocs.restassured.operation.preprocess; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java similarity index 85% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/package-info.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java index e8ee89e0..b4c02f36 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/package-info.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 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. @@ -17,4 +17,4 @@ /** * Core classes for using Spring REST Docs with REST Assured 3. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java similarity index 99% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRequestConverterTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 72f9a94d..f7590fbd 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.io.ByteArrayInputStream; import java.io.File; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java similarity index 94% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverterTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java index 927cf67b..f7db8684 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import io.restassured.http.Headers; import io.restassured.response.Response; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java similarity index 97% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationConfigurerTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 53e02b50..40d1fee5 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.util.List; import java.util.Map; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java similarity index 96% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationIntegrationTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index a6c77d4c..7ff46f28 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.io.File; import java.io.FileInputStream; @@ -69,8 +69,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; /** * Integration tests for using Spring REST Docs with REST Assured. @@ -265,15 +265,13 @@ public void preprocessedRequest() throws Exception { assertThat(new File("build/generated-snippets/original-request/http-request.adoc")) .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("a", "alpha") .header("b", "bravo").header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost:" + tomcat.getPort()).header("Content-Length", "13") - .content("{\"a\":\"alpha\"}"))); + .header("Content-Type", "application/json").header("Host", "localhost:" + tomcat.getPort()) + .header("Content-Length", "13").content("{\"a\":\"alpha\"}"))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat(new File("build/generated-snippets/preprocessed-request/http-request.adoc")) .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Type", "application/json; charset=UTF-8").header("Host", "localhost") - .content(prettyPrinted))); + .header("Accept", MediaType.APPLICATION_JSON_VALUE).header("Content-Type", "application/json") + .header("Host", "localhost").content(prettyPrinted))); } @Test @@ -289,9 +287,8 @@ public void defaultPreprocessedRequest() throws Exception { String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat(new File("build/generated-snippets/default-preprocessed-request/http-request.adoc")) .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Type", "application/json; charset=UTF-8").header("Host", "localhost") - .content(prettyPrinted))); + .header("Accept", MediaType.APPLICATION_JSON_VALUE).header("Content-Type", "application/json") + .header("Host", "localhost").content(prettyPrinted))); } @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/TomcatServer.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java similarity index 97% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/TomcatServer.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java index 3f252bd3..f52618b0 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/TomcatServer.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3; +package org.springframework.restdocs.restassured; import java.io.IOException; import java.util.Arrays; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java similarity index 99% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/operation/preprocess/UriModifyingOperationPreprocessorTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 69705f61..96e7efa5 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured3/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured3.operation.preprocess; +package org.springframework.restdocs.restassured.operation.preprocess; import java.net.URI; import java.util.Arrays; From 1e96e3e71a7be17a560a37530d030e34bb297047 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Sep 2021 10:36:30 +0100 Subject: [PATCH 004/198] Raise the minimum supported version of AsciidoctorJ to 2.5 Closes gh-751 --- docs/src/docs/asciidoc/getting-started.adoc | 2 +- gradle.properties | 1 + samples/rest-notes-spring-data-rest/pom.xml | 5 +- settings.gradle | 4 -- spring-restdocs-asciidoctor-1.5/build.gradle | 19 ------ ...tAttributesAsciidoctorJ15Preprocessor.java | 40 ----------- ...stDocsAsciidoctorJ15ExtensionRegistry.java | 50 -------------- ...ibutesAsciidoctorJ15PreprocessorTests.java | 67 ------------------- spring-restdocs-asciidoctor-1.6/build.gradle | 19 ------ ...stDocsAsciidoctorJ16ExtensionRegistry.java | 50 -------------- ...ibutesAsciidoctorJ16PreprocessorTests.java | 67 ------------------- spring-restdocs-asciidoctor-2.x/build.gradle | 18 ----- ...tAttributesAsciidoctorJ2xPreprocessor.java | 39 ----------- .../build.gradle | 12 ---- spring-restdocs-asciidoctor/build.gradle | 36 +--------- .../DefaultAttributesPreprocessor.java | 4 +- .../RestDocsExtensionRegistry.java | 11 ++- .../SnippetsDirectoryResolver.java | 2 +- ...sciidoctor.extension.spi.ExtensionRegistry | 2 - ...ctor.jruby.extension.spi.ExtensionRegistry | 2 +- .../extensions/operation_block_macro.rb | 0 .../AbstractOperationBlockMacroTests.java | 5 +- .../DefaultAttributesPreprocessorTests.java | 12 ++-- .../GradleOperationBlockMacroTests.java | 12 ++-- .../MavenOperationBlockMacroTests.java | 10 +-- .../SnippetsDirectoryResolverTests.java | 2 +- spring-restdocs-platform/build.gradle | 3 +- 27 files changed, 37 insertions(+), 457 deletions(-) delete mode 100644 spring-restdocs-asciidoctor-1.5/build.gradle delete mode 100644 spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15Preprocessor.java delete mode 100644 spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ15ExtensionRegistry.java delete mode 100644 spring-restdocs-asciidoctor-1.5/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15PreprocessorTests.java delete mode 100644 spring-restdocs-asciidoctor-1.6/build.gradle delete mode 100644 spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ16ExtensionRegistry.java delete mode 100644 spring-restdocs-asciidoctor-1.6/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16PreprocessorTests.java delete mode 100644 spring-restdocs-asciidoctor-2.x/build.gradle delete mode 100644 spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessor.java delete mode 100644 spring-restdocs-asciidoctor-support/build.gradle rename spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16Preprocessor.java => spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java (90%) rename spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ2xExtensionRegistry.java => spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java (71%) rename {spring-restdocs-asciidoctor-support => spring-restdocs-asciidoctor}/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java (97%) delete mode 100644 spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry rename {spring-restdocs-asciidoctor-support => spring-restdocs-asciidoctor}/src/main/resources/extensions/operation_block_macro.rb (100%) rename spring-restdocs-asciidoctor-2.x/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessorTests.java => spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java (85%) rename {spring-restdocs-asciidoctor-support => spring-restdocs-asciidoctor}/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java (98%) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 7e586aba..9e39dce2 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -106,7 +106,7 @@ the configuration are described in the following listings: <2> org.asciidoctor asciidoctor-maven-plugin - 1.5.8 + 2.2.1 generate-docs diff --git a/gradle.properties b/gradle.properties index de5af153..934f07db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,4 +13,5 @@ asciidoctorj24Version=2.4.3 asciidoctorj25Version=2.5.1 javaFormatVersion=0.0.27 + jmustacheVersion=1.12 diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 82453e87..71d008cd 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -83,7 +83,7 @@ org.asciidoctor asciidoctor-maven-plugin - 1.5.8 + 2.2.1 generate-docs @@ -96,9 +96,6 @@ book true - - DEBUG - diff --git a/settings.gradle b/settings.gradle index 45a29672..2e81bf85 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,10 +36,6 @@ settings.gradle.projectsLoaded { include "docs" include "spring-restdocs-asciidoctor" -include "spring-restdocs-asciidoctor-1.5" -include "spring-restdocs-asciidoctor-1.6" -include "spring-restdocs-asciidoctor-2.x" -include "spring-restdocs-asciidoctor-support" include "spring-restdocs-core" include "spring-restdocs-mockmvc" include "spring-restdocs-platform" diff --git a/spring-restdocs-asciidoctor-1.5/build.gradle b/spring-restdocs-asciidoctor-1.5/build.gradle deleted file mode 100644 index 05dfb19a..00000000 --- a/spring-restdocs-asciidoctor-1.5/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id "java" -} - -description = "AsciidoctorJ 1.5 extensions for Spring REST Docs" - -dependencies { - compileOnly("org.asciidoctor:asciidoctorj:$asciidoctorj15Version") - - implementation(project(":spring-restdocs-asciidoctor-support")) - - internal(platform(project(":spring-restdocs-platform"))) - - testImplementation("junit:junit") - testImplementation("org.asciidoctor:asciidoctorj:$asciidoctorj15Version") - testImplementation("org.assertj:assertj-core") - - testRuntimeOnly(project(":spring-restdocs-asciidoctor-support")) -} diff --git a/spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15Preprocessor.java b/spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15Preprocessor.java deleted file mode 100644 index d1dd4018..00000000 --- a/spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15Preprocessor.java +++ /dev/null @@ -1,40 +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.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 DefaultAttributesAsciidoctorJ15Preprocessor extends Preprocessor { - - private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); - - @Override - public PreprocessorReader process(Document document, PreprocessorReader reader) { - document.setAttr("snippets", this.snippetsDirectoryResolver.getSnippetsDirectory(document.getAttributes()), - false); - return reader; - } - -} diff --git a/spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ15ExtensionRegistry.java b/spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ15ExtensionRegistry.java deleted file mode 100644 index 8c0734c0..00000000 --- a/spring-restdocs-asciidoctor-1.5/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ15ExtensionRegistry.java +++ /dev/null @@ -1,50 +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.asciidoctor; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.extension.spi.ExtensionRegistry; - -/** - * AsciidoctorJ 1.5 {@link ExtensionRegistry} for Spring REST Docs. - * - * @author Andy Wilkinson - */ -public final class RestDocsAsciidoctorJ15ExtensionRegistry implements ExtensionRegistry { - - @Override - public void register(Asciidoctor asciidoctor) { - if (!asciidoctorJ15()) { - return; - } - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesAsciidoctorJ15Preprocessor()); - asciidoctor.rubyExtensionRegistry() - .loadClass(RestDocsAsciidoctorJ15ExtensionRegistry.class - .getResourceAsStream("/extensions/operation_block_macro.rb")) - .blockMacro("operation", "OperationBlockMacro"); - } - - private boolean asciidoctorJ15() { - try { - return !Class.forName("org.asciidoctor.extension.JavaExtensionRegistry").isInterface(); - } - catch (Throwable ex) { - return false; - } - } - -} diff --git a/spring-restdocs-asciidoctor-1.5/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15PreprocessorTests.java b/spring-restdocs-asciidoctor-1.5/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15PreprocessorTests.java deleted file mode 100644 index 1dcd9153..00000000 --- a/spring-restdocs-asciidoctor-1.5/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ15PreprocessorTests.java +++ /dev/null @@ -1,67 +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.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 DefaultAttributesAsciidoctorJ15Preprocessor}. - * - * @author Andy Wilkinson - */ -public class DefaultAttributesAsciidoctorJ15PreprocessorTests { - - @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 = new Options(); - options.setAttributes(new Attributes(attributes)); - return options; - } - - private Asciidoctor createAsciidoctor() { - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesAsciidoctorJ15Preprocessor()); - return asciidoctor; - } - -} diff --git a/spring-restdocs-asciidoctor-1.6/build.gradle b/spring-restdocs-asciidoctor-1.6/build.gradle deleted file mode 100644 index 7a9695a6..00000000 --- a/spring-restdocs-asciidoctor-1.6/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id "java" -} - -description = "AsciidoctorJ 1.6 extensions for Spring REST Docs" - -dependencies { - compileOnly("org.asciidoctor:asciidoctorj:$asciidoctorj16Version") - - implementation(project(":spring-restdocs-asciidoctor-support")) - - internal(platform(project(":spring-restdocs-platform"))) - - testImplementation("junit:junit") - testImplementation("org.asciidoctor:asciidoctorj:$asciidoctorj16Version") - testImplementation("org.assertj:assertj-core") - - testRuntimeOnly(project(":spring-restdocs-asciidoctor-support")) -} diff --git a/spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ16ExtensionRegistry.java b/spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ16ExtensionRegistry.java deleted file mode 100644 index b115bd98..00000000 --- a/spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ16ExtensionRegistry.java +++ /dev/null @@ -1,50 +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.asciidoctor; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.extension.spi.ExtensionRegistry; - -/** - * AsciidoctorJ 1.6 {@link ExtensionRegistry} for Spring REST Docs. - * - * @author Andy Wilkinson - */ -public final class RestDocsAsciidoctorJ16ExtensionRegistry implements ExtensionRegistry { - - @Override - public void register(Asciidoctor asciidoctor) { - if (!asciidoctorJ16()) { - return; - } - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesAsciidoctorJ16Preprocessor()); - asciidoctor.rubyExtensionRegistry() - .loadClass(RestDocsAsciidoctorJ16ExtensionRegistry.class - .getResourceAsStream("/extensions/operation_block_macro.rb")) - .blockMacro("operation", "OperationBlockMacro"); - } - - private boolean asciidoctorJ16() { - try { - return Class.forName("org.asciidoctor.extension.JavaExtensionRegistry").isInterface(); - } - catch (Throwable ex) { - return false; - } - } - -} diff --git a/spring-restdocs-asciidoctor-1.6/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16PreprocessorTests.java b/spring-restdocs-asciidoctor-1.6/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16PreprocessorTests.java deleted file mode 100644 index ec91a339..00000000 --- a/spring-restdocs-asciidoctor-1.6/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16PreprocessorTests.java +++ /dev/null @@ -1,67 +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.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 DefaultAttributesAsciidoctorJ16Preprocessor}. - * - * @author Andy Wilkinson - */ -public class DefaultAttributesAsciidoctorJ16PreprocessorTests { - - @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 = new Options(); - options.setAttributes(new Attributes(attributes)); - return options; - } - - private Asciidoctor createAsciidoctor() { - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesAsciidoctorJ16Preprocessor()); - return asciidoctor; - } - -} diff --git a/spring-restdocs-asciidoctor-2.x/build.gradle b/spring-restdocs-asciidoctor-2.x/build.gradle deleted file mode 100644 index bc6361e7..00000000 --- a/spring-restdocs-asciidoctor-2.x/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id "java" -} -description = "AsciidoctorJ 2.x extensions for Spring REST Docs" - -dependencies { - compileOnly("org.asciidoctor:asciidoctorj:$asciidoctorj20Version") - - implementation(project(":spring-restdocs-asciidoctor-support")) - - internal(platform(project(":spring-restdocs-platform"))) - - testImplementation("junit:junit") - testImplementation("org.asciidoctor:asciidoctorj:$asciidoctorj20Version") - testImplementation("org.assertj:assertj-core") - - testRuntimeOnly(project(":spring-restdocs-asciidoctor-support")) -} diff --git a/spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessor.java b/spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessor.java deleted file mode 100644 index 3ed0a62c..00000000 --- a/spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessor.java +++ /dev/null @@ -1,39 +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.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 DefaultAttributesAsciidoctorJ2xPreprocessor 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); - } - -} diff --git a/spring-restdocs-asciidoctor-support/build.gradle b/spring-restdocs-asciidoctor-support/build.gradle deleted file mode 100644 index a6ef58c4..00000000 --- a/spring-restdocs-asciidoctor-support/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id "java" -} - -description = "Support code for AsciidoctorJ extensions for Spring REST Docs" - -dependencies { - internal(platform(project(":spring-restdocs-platform"))) - - testImplementation("junit:junit") - testImplementation("org.assertj:assertj-core") -} diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle index 1a6d429b..149c81e2 100644 --- a/spring-restdocs-asciidoctor/build.gradle +++ b/spring-restdocs-asciidoctor/build.gradle @@ -1,51 +1,19 @@ plugins { - id "io.spring.compatibility-test" version "0.0.1" id "java-library" id "maven-publish" } description = "Spring REST Docs Asciidoctor Extension" -configurations { - merge - testRuntimeOnly { extendsFrom merge } -} - dependencies { - internal(platform(project(":spring-restdocs-platform"))) + implementation("org.asciidoctor:asciidoctorj") - merge project(":spring-restdocs-asciidoctor-1.5") - merge project(":spring-restdocs-asciidoctor-1.6") - merge project(":spring-restdocs-asciidoctor-2.x") + internal(platform(project(":spring-restdocs-platform"))) testImplementation("junit:junit") testImplementation("org.apache.pdfbox:pdfbox") - testImplementation("org.asciidoctor:asciidoctorj:1.5.8.1") testImplementation("org.assertj:assertj-core") testImplementation("org.springframework:spring-core") testRuntimeOnly("org.asciidoctor:asciidoctorj-pdf") } - -jar { - dependsOn(":spring-restdocs-asciidoctor-1.5:jar") - dependsOn(":spring-restdocs-asciidoctor-1.6:jar") - dependsOn(":spring-restdocs-asciidoctor-2.x:jar") - from configurations.merge.collect { file -> zipTree(file) } -} - -compatibilityTest { - dependency('AsciidoctorJ') { asciidoctorJ -> - asciidoctorJ.groupId = "org.asciidoctor" - asciidoctorJ.artifactId = "asciidoctorj" - asciidoctorJ.versions = [ - asciidoctorj16Version, - asciidoctorj20Version, - asciidoctorj21Version, - asciidoctorj22Version, - asciidoctorj23Version, - asciidoctorj24Version, - asciidoctorj25Version, - ] - } -} diff --git a/spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16Preprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java similarity index 90% rename from spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16Preprocessor.java rename to spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java index 409cf658..1d1be53b 100644 --- a/spring-restdocs-asciidoctor-1.6/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ16Preprocessor.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -26,7 +26,7 @@ * * @author Andy Wilkinson */ -final class DefaultAttributesAsciidoctorJ16Preprocessor extends Preprocessor { +final class DefaultAttributesPreprocessor extends Preprocessor { private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); diff --git a/spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ2xExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java similarity index 71% rename from spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ2xExtensionRegistry.java rename to spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java index 973b1b8c..f2ad6551 100644 --- a/spring-restdocs-asciidoctor-2.x/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsAsciidoctorJ2xExtensionRegistry.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -20,18 +20,17 @@ import org.asciidoctor.jruby.extension.spi.ExtensionRegistry; /** - * AsciidoctorJ 2.x {@link ExtensionRegistry} for Spring REST Docs. + * {@link ExtensionRegistry} for Spring REST Docs. * * @author Andy Wilkinson */ -public final class RestDocsAsciidoctorJ2xExtensionRegistry implements ExtensionRegistry { +public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesAsciidoctorJ2xPreprocessor()); + asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); asciidoctor.rubyExtensionRegistry() - .loadClass(RestDocsAsciidoctorJ2xExtensionRegistry.class - .getResourceAsStream("/extensions/operation_block_macro.rb")) + .loadClass(RestDocsExtensionRegistry.class.getResourceAsStream("/extensions/operation_block_macro.rb")) .blockMacro("operation", "OperationBlockMacro"); } diff --git a/spring-restdocs-asciidoctor-support/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java similarity index 97% rename from spring-restdocs-asciidoctor-support/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java rename to spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java index 87c83cad..af2e9482 100644 --- a/spring-restdocs-asciidoctor-support/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-2019 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. diff --git a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry deleted file mode 100644 index ccdd8d45..00000000 --- a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.restdocs.asciidoctor.RestDocsAsciidoctorJ15ExtensionRegistry -org.springframework.restdocs.asciidoctor.RestDocsAsciidoctorJ16ExtensionRegistry diff --git a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry index 249b935a..7123d5b5 100644 --- a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry +++ b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry @@ -1 +1 @@ -org.springframework.restdocs.asciidoctor.RestDocsAsciidoctorJ2xExtensionRegistry +org.springframework.restdocs.asciidoctor.RestDocsExtensionRegistry diff --git a/spring-restdocs-asciidoctor-support/src/main/resources/extensions/operation_block_macro.rb b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb similarity index 100% rename from spring-restdocs-asciidoctor-support/src/main/resources/extensions/operation_block_macro.rb rename to spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb 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 e44b3b79..52445aa5 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-2019 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. @@ -34,7 +34,6 @@ import org.asciidoctor.Asciidoctor; import org.asciidoctor.Attributes; import org.asciidoctor.Options; -import org.asciidoctor.OptionsBuilder; import org.asciidoctor.SafeMode; import org.junit.After; import org.junit.Before; @@ -64,7 +63,7 @@ public abstract class AbstractOperationBlockMacroTests { @Before public void setUp() throws IOException { prepareOperationSnippets(getBuildOutputLocation()); - this.options = OptionsBuilder.options().safe(SafeMode.UNSAFE).baseDir(getSourceLocation()).get(); + this.options = Options.builder().safe(SafeMode.UNSAFE).baseDir(getSourceLocation()).build(); this.options.setAttributes(getAttributes()); CapturingLogHandler.clear(); } diff --git a/spring-restdocs-asciidoctor-2.x/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java similarity index 85% rename from spring-restdocs-asciidoctor-2.x/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessorTests.java rename to spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java index 4bb84aab..154cd73f 100644 --- a/spring-restdocs-asciidoctor-2.x/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesAsciidoctorJ2xPreprocessorTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -26,11 +26,11 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link DefaultAttributesAsciidoctorJ2xPreprocessor}. + * Tests for {@link DefaultAttributesPreprocessor}. * * @author Andy Wilkinson */ -public class DefaultAttributesAsciidoctorJ2xPreprocessorTests { +public class DefaultAttributesPreprocessorTests { @Test public void snippetsAttributeIsSet() { @@ -53,14 +53,14 @@ public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { } private Options createOptions(String attributes) { - Options options = new Options(); - options.setAttributes(new Attributes(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 DefaultAttributesAsciidoctorJ2xPreprocessor()); + asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); return asciidoctor; } 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 04eddf35..df594537 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-2019 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. @@ -42,22 +42,24 @@ public static Object[] parameters() { return new Object[] { "projectdir", "gradle-projectdir" }; } + @Override protected Attributes getAttributes() { - Attributes attributes = new Attributes(); - attributes.setAttribute(this.attributeName, new File(temp.getRoot(), "gradle-project").getAbsolutePath()); + Attributes attributes = Attributes.builder() + .attribute(this.attributeName, new File(this.temp.getRoot(), "gradle-project").getAbsolutePath()) + .build(); return attributes; } @Override protected File getBuildOutputLocation() { - File outputLocation = new File(temp.getRoot(), "gradle-project/build"); + File outputLocation = new File(this.temp.getRoot(), "gradle-project/build"); outputLocation.mkdirs(); return outputLocation; } @Override protected File getSourceLocation() { - File sourceLocation = new File(temp.getRoot(), "gradle-project/src/docs/asciidoc"); + File sourceLocation = new File(this.temp.getRoot(), "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 0738144c..702ed06b 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-2019 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. @@ -40,12 +40,12 @@ public void clearMavenHome() { System.clearProperty("maven.home"); } + @Override protected Attributes getAttributes() { try { File sourceLocation = getSourceLocation(); new File(sourceLocation.getParentFile().getParentFile().getParentFile(), "pom.xml").createNewFile(); - Attributes attributes = new Attributes(); - attributes.setAttribute("docdir", sourceLocation.getAbsolutePath()); + Attributes attributes = Attributes.builder().attribute("docdir", sourceLocation.getAbsolutePath()).build(); return attributes; } catch (IOException ex) { @@ -55,14 +55,14 @@ protected Attributes getAttributes() { @Override protected File getBuildOutputLocation() { - File outputLocation = new File(temp.getRoot(), "maven-project/target"); + File outputLocation = new File(this.temp.getRoot(), "maven-project/target"); outputLocation.mkdirs(); return outputLocation; } @Override protected File getSourceLocation() { - File sourceLocation = new File(temp.getRoot(), "maven-project/src/main/asciidoc"); + File sourceLocation = new File(this.temp.getRoot(), "maven-project/src/main/asciidoc"); if (!sourceLocation.exists()) { sourceLocation.mkdirs(); } diff --git a/spring-restdocs-asciidoctor-support/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java similarity index 98% rename from spring-restdocs-asciidoctor-support/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java rename to spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java index abf59eca..dafeb467 100644 --- a/spring-restdocs-asciidoctor-support/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-2019 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. diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 529dae93..4d04aa5f 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -16,8 +16,9 @@ dependencies { api("junit:junit:4.12") api("io.rest-assured:rest-assured:4.4.0") api("org.apache.pdfbox:pdfbox:2.0.7") + api("org.asciidoctor:asciidoctorj:2.5.2") + api("org.asciidoctor:asciidoctorj-pdf:1.6.0") api("org.assertj:assertj-core:3.11.1") - api("org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.18") api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") api("org.hibernate.validator:hibernate-validator:6.0.9.Final") From 68ff043fb328e9db3067cc54c5ec65cce6b1df44 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Sep 2021 12:12:50 +0100 Subject: [PATCH 005/198] Raise the minimum supported version of Java to 17 Closes gh-749 --- build.gradle | 4 ++-- ci/images/ci-image/Dockerfile | 2 +- ci/images/get-jdk-url.sh | 4 ++-- docs/src/docs/asciidoc/getting-started.adoc | 2 +- samples/junit5/build.gradle | 4 ++-- samples/rest-assured/build.gradle | 4 ++-- samples/rest-notes-slate/build.gradle | 4 ++-- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 4 ++-- samples/testng/build.gradle | 4 ++-- samples/web-test-client/build.gradle | 4 ++-- .../restdocs/operation/OperationResponseFactory.java | 11 ++++++++++- .../webtestclient/WebTestClientRequestConverter.java | 5 +++-- 13 files changed, 32 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index d40731cc..7ce90aab 100644 --- a/build.gradle +++ b/build.gradle @@ -39,8 +39,8 @@ subprojects { subproject -> subproject.apply plugin: "io.spring.javaformat" subproject.apply plugin: "checkstyle" - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 17 + targetCompatibility = 17 configurations { internal { diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index a73b78fc..4e675734 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -2,7 +2,7 @@ FROM ubuntu:focal-20210401 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java8 +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 index 14469df1..a3babd3a 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -2,8 +2,8 @@ set -e case "$1" in - java8) - echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jdk_x64_linux_hotspot_8u292b10.tar.gz" + java17) + echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x64_linux_hotspot_17_35.tar.gz" ;; *) echo $"Unknown java version" diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 9e39dce2..8227b94e 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -76,7 +76,7 @@ If you want to jump straight in, a number of sample applications are available: Spring REST Docs has the following minimum requirements: -* Java 8 +* Java 17 * Spring Framework 5 (5.0.2 or later) Additionally, the `spring-restdocs-restassured` module requires REST Assured 4 (4.4 or later). diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index edce9ca1..cb3c7044 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -16,8 +16,8 @@ repositories { group = 'com.example' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 17 +targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index a2afef45..f7237456 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -16,8 +16,8 @@ repositories { group = 'com.example' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 17 +targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 96953f3a..7b7648c1 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -15,8 +15,8 @@ repositories { group = 'com.example' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 17 +targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 71d008cd..67d13cff 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -17,7 +17,7 @@ UTF-8 - 1.8 + 17 3.0.0-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 1cb6e7fd..52fb3093 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -16,8 +16,8 @@ repositories { group = 'com.example' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 17 +targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 1b981581..5fd0cc4c 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -16,8 +16,8 @@ repositories { group = 'com.example' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 17 +targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index 27cbc93b..a4dcddd7 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -14,8 +14,8 @@ repositories { group = 'com.example' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 17 +targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index 2768451b..dd658ce1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -41,6 +41,15 @@ public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] c return this.create(status.value(), headers, content); } + /** + * Creates a new {@link OperationResponse}. If the response has any content, the given + * {@code headers} will be augmented to ensure that they include a + * {@code Content-Length} header. + * @param status the status of the response + * @param headers the request's headers + * @param content the content of the request + * @return the {@code OperationResponse} + */ public OperationResponse create(int status, HttpHeaders headers, byte[] content) { return new StandardOperationResponse(status, augmentHeaders(headers, content), content); } diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java index fb5be55f..84bf6370 100644 --- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 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. @@ -110,7 +110,8 @@ private HttpMessageReader findPartHttpMessageReader() { if (ClassUtils.isPresent(DEFAULT_PART_HTTP_MESSAGE_READER, getClass().getClassLoader())) { try { return (HttpMessageReader) Class - .forName(DEFAULT_PART_HTTP_MESSAGE_READER, true, getClass().getClassLoader()).newInstance(); + .forName(DEFAULT_PART_HTTP_MESSAGE_READER, true, getClass().getClassLoader()) + .getDeclaredConstructor().newInstance(); } catch (Exception ex) { // Continue From 4223f70102f3b70cc55f03770352f6b80c2d624c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Sep 2021 15:01:55 +0100 Subject: [PATCH 006/198] Upgrade to Spring Framework 6 and Jakarta EE 9 Closes gh-750 Closes gh-748 --- build.gradle | 4 +- docs/build.gradle | 4 +- .../docs/asciidoc/documenting-your-api.adoc | 4 +- docs/src/docs/asciidoc/getting-started.adoc | 2 +- .../test/java/com/example/Constraints.java | 4 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../ExceptionSupressingErrorAttributes.java | 2 +- .../java/com/example/notes/NoteInput.java | 2 +- .../com/example/notes/NullOrNotBlank.java | 8 +-- .../notes/RestNotesControllerAdvice.java | 4 +- .../main/java/com/example/notes/TagInput.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../example/notes/NullOrNotBlankTests.java | 6 +-- spring-restdocs-core/build.gradle | 13 ++--- ...ceBundleConstraintDescriptionResolver.java | 51 +++++++++--------- .../ValidatorConstraintResolver.java | 16 +++--- .../DefaultConstraintDescriptions.properties | 45 ++++++++-------- ...dleConstraintDescriptionResolverTests.java | 53 ++++++++----------- .../ValidatorConstraintResolverTests.java | 13 +++-- .../TestConstraintDescriptions.properties | 2 +- spring-restdocs-mockmvc/build.gradle | 9 +--- .../mockmvc/MockMvcRequestConverter.java | 6 +-- .../mockmvc/MockMvcResponseConverter.java | 2 +- .../mockmvc/MockMvcRequestConverterTests.java | 7 ++- .../MockMvcResponseConverterTests.java | 5 +- ...kMvcRestDocumentationIntegrationTests.java | 5 +- ...RestDocumentationRequestBuildersTests.java | 3 +- spring-restdocs-platform/build.gradle | 9 ++-- spring-restdocs-restassured/build.gradle | 2 +- ...uredRestDocumentationIntegrationTests.java | 11 ++-- .../restdocs/restassured/TomcatServer.java | 11 ++-- spring-restdocs-webtestclient/build.gradle | 14 ----- .../WebTestClientRequestConverter.java | 29 +--------- ...TestClientRestDocumentationConfigurer.java | 2 +- .../WebTestClientResponseConverterTests.java | 2 +- ...ientRestDocumentationIntegrationTests.java | 6 +-- 37 files changed, 147 insertions(+), 217 deletions(-) diff --git a/build.gradle b/build.gradle index 7ce90aab..54c88ef6 100644 --- a/build.gradle +++ b/build.gradle @@ -25,10 +25,10 @@ nohttp { } ext { - springVersion = "5.0.15.RELEASE" + springFrameworkVersion = "6.0.0-SNAPSHOT" javadocLinks = [ "https://docs.oracle.com/javase/8/docs/api/", - "https://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", + "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", "https://docs.jboss.org/hibernate/stable/beanvalidation/api/", "https://docs.jboss.org/hibernate/stable/validator/api/" ] as String[] diff --git a/docs/build.gradle b/docs/build.gradle index 152468dd..f4367407 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -16,13 +16,13 @@ dependencies { asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.5.0") internal(platform(project(":spring-restdocs-platform"))) - internal(enforcedPlatform("org.springframework:spring-framework-bom:5.3.8")) + internal(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) testImplementation(project(":spring-restdocs-mockmvc")) testImplementation(project(":spring-restdocs-restassured")) testImplementation(project(":spring-restdocs-webtestclient")) testImplementation("io.rest-assured:rest-assured") - testImplementation("javax.validation:validation-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/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 306382f8..c4a68fab 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -1252,7 +1252,7 @@ example of such a resource bundle]. Each key in the resource bundle is the fully-qualified name of a constraint plus a `.description`. For example, the key for the standard `@NotNull` constraint is -`javax.validation.constraints.NotNull.description`. +`jakarta.validation.constraints.NotNull.description`. You can use a property placeholder referring to a constraint's attributes in its description. For example, the default description of the `@Min` constraint, @@ -1318,7 +1318,7 @@ You can configure which snippets are produced by default. See the === Using Parameterized Output Directories When using MockMvc, REST Assured, or `WebTestClient` you can parameterize the output directory used by -`document`. Parameterizing output with `WebTestClient` requires Spring Framework 5.3.5 or later. +`document`. The following parameters are supported: diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8227b94e..8ae2279a 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -77,7 +77,7 @@ If you want to jump straight in, a number of sample applications are available: Spring REST Docs has the following minimum requirements: * Java 17 -* Spring Framework 5 (5.0.2 or later) +* Spring Framework 6 Additionally, the `spring-restdocs-restassured` module requires REST Assured 4 (4.4 or later). diff --git a/docs/src/test/java/com/example/Constraints.java b/docs/src/test/java/com/example/Constraints.java index 64912874..6d8ef1fc 100644 --- a/docs/src/test/java/com/example/Constraints.java +++ b/docs/src/test/java/com/example/Constraints.java @@ -18,8 +18,8 @@ import java.util.List; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import org.springframework.restdocs.constraints.ConstraintDescriptions; diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index 5955934b..0416f6a8 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -38,7 +38,7 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.RequestDispatcher; +import jakarta.servlet.RequestDispatcher; import org.junit.Before; import org.junit.Rule; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index bbcc9a3f..746f3c51 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -37,7 +37,7 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.RequestDispatcher; +import jakarta.servlet.RequestDispatcher; import org.junit.Before; import org.junit.Rule; diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java index d7725a51..8da9b419 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java @@ -32,7 +32,7 @@ public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map errorAttributes = super.getErrorAttributes(webRequest, options); errorAttributes.remove("exception"); - Object message = webRequest.getAttribute("javax.servlet.error.message", RequestAttributes.SCOPE_REQUEST); + Object message = webRequest.getAttribute("jakarta.servlet.error.message", RequestAttributes.SCOPE_REQUEST); if (message != null) { errorAttributes.put("message", message); } diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java index d5fd5c86..48975e02 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.List; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java index 23dbbd45..661f6fae 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java @@ -21,10 +21,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.validation.Constraint; -import javax.validation.Payload; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Null; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Null; import org.hibernate.validator.constraints.CompositionType; import org.hibernate.validator.constraints.ConstraintComposition; diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java index bef998e2..750e21ca 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java @@ -18,8 +18,8 @@ import java.io.IOException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java index bc9b397a..d10f25fa 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java @@ -16,7 +16,7 @@ package com.example.notes; -import javax.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 17cebdb1..c3e96b51 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -42,7 +42,7 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.RequestDispatcher; +import jakarta.servlet.RequestDispatcher; import org.junit.Before; import org.junit.Rule; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java index a7086e72..8b9f1c9a 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java @@ -20,9 +20,9 @@ import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import org.junit.Test; diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 3b67df29..9cbfd184 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -46,11 +46,11 @@ dependencies { optional(platform(project(":spring-restdocs-platform"))) optional("commons-codec:commons-codec") - optional("javax.validation:validation-api") + optional("jakarta.validation:jakarta.validation-api") optional("junit:junit") optional("org.hibernate.validator:hibernate-validator") optional("org.junit.jupiter:junit-jupiter-api") - + testFixturesApi(platform(project(":spring-restdocs-platform"))) testFixturesApi("junit:junit") testFixturesApi("org.assertj:assertj-core") @@ -68,7 +68,7 @@ dependencies { testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.springframework:spring-test") - testRuntimeOnly("org.glassfish:javax.el:3.0.0") + testRuntimeOnly("org.glassfish:jakarta.el:4.0.2") } jar { @@ -85,10 +85,3 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesApiElem components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } - -compatibilityTest { - dependency("Spring Framework") { springFramework -> - springFramework.groupId = "org.springframework" - springFramework.versions = ["5.1.+", "5.2.+", "5.3.+"] - } -} 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 46fcfe92..e7fff063 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 @@ -20,29 +20,28 @@ import java.util.MissingResourceException; import java.util.ResourceBundle; -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.DecimalMax; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Email; -import javax.validation.constraints.Future; -import javax.validation.constraints.FutureOrPresent; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.Negative; -import javax.validation.constraints.NegativeOrZero; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Past; -import javax.validation.constraints.PastOrPresent; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Positive; -import javax.validation.constraints.PositiveOrZero; -import javax.validation.constraints.Size; - +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; import org.hibernate.validator.constraints.CodePointLength; import org.hibernate.validator.constraints.CreditCardNumber; import org.hibernate.validator.constraints.Currency; @@ -52,7 +51,6 @@ import org.hibernate.validator.constraints.Mod10Check; import org.hibernate.validator.constraints.Mod11Check; import org.hibernate.validator.constraints.Range; -import org.hibernate.validator.constraints.SafeHtml; import org.hibernate.validator.constraints.URL; import org.springframework.util.PropertyPlaceholderHelper; @@ -63,8 +61,8 @@ * A {@link ConstraintDescriptionResolver} that resolves constraint descriptions from a * {@link ResourceBundle}. The resource bundle's keys are the name of the constraint with * {@code .description} appended. For example, the key for the constraint named - * {@code javax.validation.constraints.NotNull} is - * {@code javax.validation.constraints.NotNull.description}. + * {@code jakarta.validation.constraints.NotNull} is + * {@code jakarta.validation.constraints.NotNull.description}. *

* Default descriptions are provided for Bean Validation 2.0's constraints: * @@ -109,7 +107,6 @@ *

  • {@link org.hibernate.validator.constraints.NotBlank} *
  • {@link org.hibernate.validator.constraints.NotEmpty} *
  • {@link Range} - *
  • {@link SafeHtml} *
  • {@link URL} * * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java index e8e9fecb..28910a93 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -19,19 +19,19 @@ import java.util.ArrayList; import java.util.List; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import javax.validation.constraints.NotNull; -import javax.validation.metadata.BeanDescriptor; -import javax.validation.metadata.ConstraintDescriptor; -import javax.validation.metadata.PropertyDescriptor; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.metadata.BeanDescriptor; +import jakarta.validation.metadata.ConstraintDescriptor; +import jakarta.validation.metadata.PropertyDescriptor; /** * A {@link ConstraintResolver} that uses a Bean Validation {@link Validator} to resolve * constraints. The name of the constraint is the fully-qualified class name of the * constraint annotation. For example, a {@link NotNull} constraint will be named - * {@code javax.validation.constraints.NotNull}. + * {@code jakarta.validation.constraints.NotNull}. * * @author Andy Wilkinson * 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 ce699e09..248cb238 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 @@ -1,25 +1,25 @@ -javax.validation.constraints.AssertFalse.description=Must be false -javax.validation.constraints.AssertTrue.description=Must be true -javax.validation.constraints.DecimalMax.description=Must be at most ${value} -javax.validation.constraints.DecimalMin.description=Must be at least ${value} -javax.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits -javax.validation.constraints.Email.description=Must be a well-formed email address -javax.validation.constraints.Future.description=Must be in the future -javax.validation.constraints.FutureOrPresent.description=Must be in the future or the present -javax.validation.constraints.Max.description=Must be at most ${value} -javax.validation.constraints.Min.description=Must be at least ${value} -javax.validation.constraints.Negative.description=Must be negative -javax.validation.constraints.NegativeOrZero.description=Must be negative or zero -javax.validation.constraints.NotBlank.description=Must not be blank -javax.validation.constraints.NotEmpty.description=Must not be empty -javax.validation.constraints.NotNull.description=Must not be null -javax.validation.constraints.Null.description=Must be null -javax.validation.constraints.Past.description=Must be in the past -javax.validation.constraints.PastOrPresent.description=Must be in the past or the present -javax.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` -javax.validation.constraints.Positive.description=Must be positive -javax.validation.constraints.PositiveOrZero.description=Must be positive or zero -javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive +jakarta.validation.constraints.AssertFalse.description=Must be false +jakarta.validation.constraints.AssertTrue.description=Must be true +jakarta.validation.constraints.DecimalMax.description=Must be at most ${value} +jakarta.validation.constraints.DecimalMin.description=Must be at least ${value} +jakarta.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits +jakarta.validation.constraints.Email.description=Must be a well-formed email address +jakarta.validation.constraints.Future.description=Must be in the future +jakarta.validation.constraints.FutureOrPresent.description=Must be in the future or the present +jakarta.validation.constraints.Max.description=Must be at most ${value} +jakarta.validation.constraints.Min.description=Must be at least ${value} +jakarta.validation.constraints.Negative.description=Must be negative +jakarta.validation.constraints.NegativeOrZero.description=Must be negative or zero +jakarta.validation.constraints.NotBlank.description=Must not be blank +jakarta.validation.constraints.NotEmpty.description=Must not be empty +jakarta.validation.constraints.NotNull.description=Must not be null +jakarta.validation.constraints.Null.description=Must be null +jakarta.validation.constraints.Past.description=Must be in the past +jakarta.validation.constraints.PastOrPresent.description=Must be in the past or the present +jakarta.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` +jakarta.validation.constraints.Positive.description=Must be positive +jakarta.validation.constraints.PositiveOrZero.description=Must be positive or zero +jakarta.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.CodePointLength.description=Code point length must be between ${min} and ${max} inclusive 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}) @@ -32,5 +32,4 @@ org.hibernate.validator.constraints.Mod11Check.description=Must pass the Mod11 c 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.SafeHtml.description=Must be safe HTML 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 f5003061..e3dfd56e 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 @@ -26,29 +26,29 @@ import java.util.ResourceBundle; import javax.money.MonetaryAmount; -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.DecimalMax; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Email; -import javax.validation.constraints.Future; -import javax.validation.constraints.FutureOrPresent; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.Negative; -import javax.validation.constraints.NegativeOrZero; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Past; -import javax.validation.constraints.PastOrPresent; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Positive; -import javax.validation.constraints.PositiveOrZero; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; import org.hibernate.validator.constraints.CodePointLength; import org.hibernate.validator.constraints.CreditCardNumber; import org.hibernate.validator.constraints.Currency; @@ -58,7 +58,6 @@ import org.hibernate.validator.constraints.Mod10Check; import org.hibernate.validator.constraints.Mod11Check; import org.hibernate.validator.constraints.Range; -import org.hibernate.validator.constraints.SafeHtml; import org.junit.Test; import org.springframework.core.annotation.AnnotationUtils; @@ -253,11 +252,6 @@ public void defaultMessageRange() { assertThat(constraintDescriptionForField("range")).isEqualTo("Must be at least 10 and at most 100"); } - @Test - public void defaultMessageSafeHtml() { - assertThat(constraintDescriptionForField("safeHtml")).isEqualTo("Must be safe HTML"); - } - @Test public void defaultMessageUrl() { assertThat(constraintDescriptionForField("url")).isEqualTo("Must be a well-formed URL"); @@ -423,9 +417,6 @@ private static class Constrained { @Range(min = 10, max = 100) private int range; - @SafeHtml - private String safeHtml; - @org.hibernate.validator.constraints.URL private String url; 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 1106f842..3794a6c4 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 @@ -26,12 +26,11 @@ import java.util.Map; import java.util.Map.Entry; -import javax.validation.Payload; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Size; - +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Size; import org.assertj.core.api.Condition; import org.assertj.core.description.TextDescription; import org.hibernate.validator.constraints.CompositionType; @@ -102,7 +101,7 @@ private static class ConstrainedFields { @NotBlank @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) - @javax.validation.Constraint(validatedBy = {}) + @jakarta.validation.Constraint(validatedBy = {}) private @interface CompositeConstraint { String message() default "Must be null or not blank"; diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties index ea0f3a91..b7150793 100644 --- a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties +++ b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties @@ -1 +1 @@ -javax.validation.constraints.NotNull.description=Should not be null \ No newline at end of file +jakarta.validation.constraints.NotNull.description=Should not be null \ No newline at end of file diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index e6c1f35d..a0381c8a 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -12,7 +12,7 @@ dependencies { api("org.springframework:spring-webmvc") api("org.springframework:spring-test") - implementation("javax.servlet:javax.servlet-api") + implementation("jakarta.servlet:jakarta.servlet-api") internal(platform(project(":spring-restdocs-platform"))) @@ -25,10 +25,3 @@ dependencies { testRuntimeOnly("commons-logging:commons-logging:1.2") } - -compatibilityTest { - dependency("Spring Framework") { springFramework -> - springFramework.groupId = "org.springframework" - springFramework.versions = ["5.1.+", "5.2.+", "5.3.+"] - } -} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index a456e252..2ab8fcd8 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -26,8 +26,8 @@ import java.util.List; import java.util.Map.Entry; -import javax.servlet.ServletException; -import javax.servlet.http.Part; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Part; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -89,7 +89,7 @@ private Collection extractCookies(MockHttpServletRequest mockRequ return Collections.emptyList(); } List cookies = new ArrayList<>(); - for (javax.servlet.http.Cookie servletCookie : mockRequest.getCookies()) { + for (jakarta.servlet.http.Cookie servletCookie : mockRequest.getCookies()) { cookies.add(new RequestCookie(servletCookie.getName(), servletCookie.getValue())); } headers.remove(HttpHeaders.COOKIE); 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 93e6b1f4..db0adc71 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 @@ -16,7 +16,7 @@ package org.springframework.restdocs.mockmvc; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; 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 ca2028c1..78bb536a 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 @@ -21,8 +21,7 @@ import java.util.Arrays; import java.util.Iterator; -import javax.servlet.http.Part; - +import jakarta.servlet.http.Part; import org.junit.Test; import org.springframework.http.HttpMethod; @@ -85,8 +84,8 @@ public void requestWithHeaders() throws Exception { @Test public void requestWithCookies() throws Exception { OperationRequest request = createOperationRequest( - MockMvcRequestBuilders.get("/foo").cookie(new javax.servlet.http.Cookie("cookieName1", "cookieVal1"), - new javax.servlet.http.Cookie("cookieName2", "cookieVal2"))); + MockMvcRequestBuilders.get("/foo").cookie(new jakarta.servlet.http.Cookie("cookieName1", "cookieVal1"), + new jakarta.servlet.http.Cookie("cookieName2", "cookieVal2"))); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); assertThat(request.getCookies().size()).isEqualTo(2); 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 ef9b85c2..4928a3fe 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 @@ -18,9 +18,8 @@ import java.util.Collections; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; - +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import org.junit.Test; import org.springframework.http.HttpHeaders; 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 c970a0ac..9b425dbd 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 @@ -31,9 +31,8 @@ import java.util.Set; import java.util.regex.Pattern; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; - +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; 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 53a12c11..53caf0ed 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 @@ -18,8 +18,7 @@ import java.net.URI; -import javax.servlet.ServletContext; - +import jakarta.servlet.ServletContext; import org.junit.Test; import org.springframework.http.HttpMethod; diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 4d04aa5f..7c411813 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -11,8 +11,8 @@ dependencies { api("com.fasterxml.jackson.core:jackson-databind:2.9.5") api("com.samskivert:jmustache:$jmustacheVersion") api("commons-codec:commons-codec:1.10") - api("javax.servlet:javax.servlet-api:3.1.0") - api("javax.validation:validation-api:2.0.0.Final") + api("jakarta.servlet:jakarta.servlet-api:5.0.0") + api("jakarta.validation:jakarta.validation-api:3.0.0") api("junit:junit:4.12") api("io.rest-assured:rest-assured:4.4.0") api("org.apache.pdfbox:pdfbox:2.0.7") @@ -21,14 +21,13 @@ dependencies { api("org.assertj:assertj-core:3.11.1") api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") - api("org.hibernate.validator:hibernate-validator:6.0.9.Final") + api("org.hibernate.validator:hibernate-validator:7.0.0.Final") api("org.jacoco:org.jacoco.agent:0.7.9") api("org.javamoney:moneta:1.1") api("org.jruby:jruby-complete:9.1.13.0") api("org.junit.jupiter:junit-jupiter-api:5.0.0") api("org.mockito:mockito-core:1.10.19") api("org.springframework.hateoas:spring-hateoas:0.23.0.RELEASE") - api("org.synchronoss.cloud:nio-multipart-parser:1.1.0") } - api(enforcedPlatform("org.springframework:spring-framework-bom:$springVersion")) + api(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) } diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 26eee409..bd817465 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -16,7 +16,7 @@ dependencies { testImplementation(testFixtures(project(":spring-restdocs-core"))) testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("junit:junit") - testImplementation("org.apache.tomcat.embed:tomcat-embed-core:8.5.13") + testImplementation("org.apache.tomcat.embed:tomcat-embed-core:10.0.11") testImplementation("org.assertj:assertj-core") testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.mockito:mockito-core") 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 7ff46f28..760884c7 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 @@ -248,8 +248,9 @@ public void responseWithCookie() { assertExpectedSnippetFilesExist(new File("build/generated-snippets/set-cookie"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc")) - .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK).header(HttpHeaders.SET_COOKIE, - "name=value; Domain=localhost; HttpOnly"))); + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header(HttpHeaders.SET_COOKIE, "name=value; Domain=localhost; HttpOnly") + .header("Keep-Alive", "timeout=60").header("Connection", "keep-alive"))); } @Test @@ -307,7 +308,8 @@ public void preprocessedResponse() throws Exception { assertThat(new File("build/generated-snippets/preprocessed-response/http-response.adoc")) .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) .header("Foo", "https://api.example.com/foo/bar") - .header("Content-Type", "application/json;charset=UTF-8") + .header("Content-Type", "application/json;charset=UTF-8").header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive") .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length).content(prettyPrinted))); } @@ -326,7 +328,8 @@ public void defaultPreprocessedResponse() throws Exception { assertThat(new File("build/generated-snippets/default-preprocessed-response/http-response.adoc")) .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) .header("Foo", "https://api.example.com/foo/bar") - .header("Content-Type", "application/json;charset=UTF-8") + .header("Content-Type", "application/json;charset=UTF-8").header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive") .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length).content(prettyPrinted))); } 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 f52618b0..83f9b15a 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 @@ -21,14 +21,13 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle index 676e3792..0cc8851b 100644 --- a/spring-restdocs-webtestclient/build.gradle +++ b/spring-restdocs-webtestclient/build.gradle @@ -20,18 +20,4 @@ dependencies { testImplementation("org.mockito:mockito-core") testRuntimeOnly("org.springframework:spring-context") - testRuntimeOnly("org.synchronoss.cloud:nio-multipart-parser") -} - -compatibilityTest { - dependency("Spring Framework") { springFramework -> - springFramework.groupId = "org.springframework" - springFramework.versions = ["5.1.+", "5.2.+", "5.3.+"] - } -} - -project.afterEvaluate { - configurations.getByName("testRuntimeClasspath_spring_framework_5.3.+") { - exclude group: "org.synchronoss.cloud", module: "nio-multipart-parser" - } } diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java index 84bf6370..4afabb82 100644 --- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java @@ -34,10 +34,10 @@ import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.codec.FormHttpMessageReader; import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.Part; -import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; @@ -48,7 +48,6 @@ import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -60,8 +59,6 @@ */ class WebTestClientRequestConverter implements RequestConverter { - private static final String DEFAULT_PART_HTTP_MESSAGE_READER = "org.springframework.http.codec.multipart.DefaultPartHttpMessageReader"; - private static final ResolvableType FORM_DATA_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class); @@ -94,10 +91,7 @@ private Parameters extractParameters(ExchangeResult result) { } private List extractRequestParts(ExchangeResult result) { - HttpMessageReader partHttpMessageReader = findPartHttpMessageReader(); - if (partHttpMessageReader == null) { - return Collections.emptyList(); - } + HttpMessageReader partHttpMessageReader = new DefaultPartHttpMessageReader(); return new MultipartHttpMessageReader(partHttpMessageReader) .readMono(ResolvableType.forClass(Part.class), new ExchangeResultReactiveHttpInputMessage(result), Collections.emptyMap()) @@ -105,25 +99,6 @@ private List extractRequestParts(ExchangeResult result) { .flatMap((parts) -> parts.stream().map(this::createOperationRequestPart)).collect(Collectors.toList()); } - @SuppressWarnings("unchecked") - private HttpMessageReader findPartHttpMessageReader() { - if (ClassUtils.isPresent(DEFAULT_PART_HTTP_MESSAGE_READER, getClass().getClassLoader())) { - try { - return (HttpMessageReader) Class - .forName(DEFAULT_PART_HTTP_MESSAGE_READER, true, getClass().getClassLoader()) - .getDeclaredConstructor().newInstance(); - } - catch (Exception ex) { - // Continue - } - } - if (ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParserListener", - getClass().getClassLoader())) { - return new SynchronossPartHttpMessageReader(); - } - return null; - } - private OperationRequestPart createOperationRequestPart(Part part) { ByteArrayOutputStream content = readPartBodyContent(part); return new OperationRequestPartFactory().create(part.name(), diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java index e6d5299d..fb3f4d50 100644 --- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java @@ -94,7 +94,7 @@ public Mono filter(ClientRequest request, ExchangeFunction next) private ClientRequest applyUriDefaults(ClientRequest request) { URI requestUri = request.url(); - if (!StringUtils.isEmpty(requestUri.getHost())) { + if (StringUtils.hasLength(requestUri.getHost())) { return request; } try { 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 18e04004..ce3dfdf6 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 @@ -46,7 +46,7 @@ public class WebTestClientResponseConverterTests { public void basicResponse() { ExchangeResult result = WebTestClient .bindToRouterFunction( - RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.ok().syncBody("Hello, World!"))) + RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.ok().bodyValue("Hello, World!"))) .configureClient().baseUrl("http://localhost").build().get().uri("/foo").exchange().expectBody() .returnResult(); OperationResponse response = this.converter.convert(result); 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 b7a653d2..89efdcc9 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 @@ -68,7 +68,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; -import static org.springframework.web.reactive.function.BodyInserters.fromObject; +import static org.springframework.web.reactive.function.BodyInserters.fromValue; /** * Integration tests for using Spring REST Docs with Spring Framework's WebTestClient. @@ -86,9 +86,9 @@ public class WebTestClientRestDocumentationIntegrationTests { public void setUp() { RouterFunction route = RouterFunctions .route(RequestPredicates.GET("/"), - (request) -> ServerResponse.status(HttpStatus.OK).body(fromObject(new Person("Jane", "Doe")))) + (request) -> ServerResponse.status(HttpStatus.OK).body(fromValue(new Person("Jane", "Doe")))) .andRoute(RequestPredicates.GET("/{foo}/{bar}"), - (request) -> ServerResponse.status(HttpStatus.OK).body(fromObject(new Person("Jane", "Doe")))) + (request) -> ServerResponse.status(HttpStatus.OK).body(fromValue(new Person("Jane", "Doe")))) .andRoute(RequestPredicates.POST("/upload"), (request) -> request.body(BodyExtractors.toMultipartData()) .map((parts) -> ServerResponse.status(HttpStatus.OK).build().block())) From be0528bdcb8cf0b0f12ab5f068928b2e6f699182 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Sep 2021 18:11:14 +0100 Subject: [PATCH 007/198] Rework the samples to avoid depending on Spring Boot Closes gh-711 --- samples/junit5/build.gradle | 27 ++---- .../junit5/SampleJUnit5Application.java | 11 +-- .../junit5/SampleJUnit5ApplicationTests.java | 8 +- samples/rest-notes-slate/build.gradle | 32 +++--- .../com/example/notes/ErrorController.java | 45 +++++++++ .../src/main/java/com/example/notes/Note.java | 12 +-- .../com/example/notes/RestNotesSlate.java | 58 +++++++++-- .../src/main/java/com/example/notes/Tag.java | 12 +-- .../src/main/resources/application.properties | 1 - .../com/example/notes/ApiDocumentation.java | 12 ++- samples/rest-notes-spring-data-rest/pom.xml | 97 +++++++++++++------ .../com/example/notes/ErrorController.java | 45 +++++++++ .../src/main/java/com/example/notes/Note.java | 12 +-- .../notes/RestNotesSpringDataRest.java | 60 ++++++++++-- .../src/main/java/com/example/notes/Tag.java | 12 +-- .../com/example/notes/ApiDocumentation.java | 8 +- .../notes/GettingStartedDocumentation.java | 10 +- .../rest-notes-spring-hateoas/build.gradle | 39 ++++---- .../com/example/notes/ErrorController.java | 45 +++++++++ .../ExceptionSupressingErrorAttributes.java | 41 -------- .../src/main/java/com/example/notes/Note.java | 10 +- .../example/notes/RestNotesSpringHateoas.java | 58 +++++++++-- .../src/main/java/com/example/notes/Tag.java | 12 +-- .../com/example/notes/ApiDocumentation.java | 10 +- .../notes/GettingStartedDocumentation.java | 12 ++- .../example/notes/NullOrNotBlankTests.java | 3 +- samples/testng/build.gradle | 22 ++--- .../testng/SampleTestNgApplication.java | 11 +-- .../testng/SampleTestNgApplicationTests.java | 8 +- samples/web-test-client/build.gradle | 17 ++-- .../SampleWebTestClientApplication.java | 15 +-- 31 files changed, 501 insertions(+), 264 deletions(-) create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/ErrorController.java delete mode 100644 samples/rest-notes-slate/src/main/resources/application.properties create mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/ErrorController.java create mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ErrorController.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index cb3c7044..284e7833 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -2,11 +2,8 @@ plugins { id "eclipse" id "java" id "org.asciidoctor.jvm.convert" version "3.3.2" - id "org.springframework.boot" version "2.4.7" } -apply plugin: 'io.spring.dependency-management' - repositories { mavenLocal() maven { url 'https://repo.spring.io/milestone' } @@ -20,26 +17,23 @@ sourceCompatibility = 17 targetCompatibility = 17 ext { + restdocsVersion = '3.0.0-SNAPSHOT' snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' - configurations { asciidoctorExtensions } dependencies { - asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' - - implementation 'org.springframework.boot:spring-boot-starter-web' + asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - testImplementation('org.springframework.boot:spring-boot-starter-test') { - exclude group: 'junit', module: 'junit;' - } - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' - testImplementation 'org.junit.jupiter:junit-jupiter-api' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + implementation 'org.springframework:spring-webmvc:6.0.0-SNAPSHOT' + + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.0' } test { @@ -53,12 +47,9 @@ asciidoctor { dependsOn test } -bootJar { +jar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { into 'static/docs' } } - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java b/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java index fec007f6..3f2d403b 100644 --- a/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java +++ b/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 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. @@ -16,18 +16,13 @@ package com.example.junit5; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@SpringBootApplication +@Configuration public class SampleJUnit5Application { - public static void main(String[] args) { - new SpringApplication(SampleJUnit5Application.class).run(args); - } - @RestController private static class SampleController { diff --git a/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java b/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java index a6e4169d..99ddd81b 100644 --- a/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java +++ b/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 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. @@ -25,15 +25,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest +@WebAppConfiguration +@ContextConfiguration(classes = SampleJUnit5Application.class) @ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) public class SampleJUnit5ApplicationTests { diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 7b7648c1..45ac6a2e 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -1,11 +1,7 @@ plugins { - id "eclipse" id "java" - id "org.springframework.boot" version "2.4.7" } -apply plugin: 'io.spring.dependency-management' - repositories { mavenLocal() maven { url 'https://repo.spring.io/milestone' } @@ -19,24 +15,31 @@ sourceCompatibility = 17 targetCompatibility = 17 ext { + restdocsVersion = '3.0.0-SNAPSHOT' snippetsDir = file('build/generated-snippets') } ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-rest' + implementation "com.fasterxml.jackson.core:jackson-databind:2.12.5" + implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" + implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" + implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" + implementation "org.springframework:spring-webmvc:6.0.0-SNAPSHOT" + implementation "org.springframework.data:spring-data-jpa:3.0.0-SNAPSHOT" + implementation "org.springframework.data:spring-data-rest-webmvc:4.0.0-SNAPSHOT" - runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'org.atteo:evo-inflector:1.2.1' - testImplementation 'com.jayway.jsonpath:json-path' - testImplementation('org.junit.vintage:junit-vintage-engine') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'com.jayway.jsonpath:json-path:2.6.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.assertj:assertj-core:3.21.0' + testImplementation 'org.hamcrest:hamcrest-library:2.2' + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" + + testRuntimeOnly 'org.glassfish:jakarta.el:4.0.2' } test { @@ -59,6 +62,3 @@ task(slate, type: Exec) { build { dependsOn 'slate' } - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/ErrorController.java b/samples/rest-notes-slate/src/main/java/com/example/notes/ErrorController.java new file mode 100644 index 00000000..557090e0 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/ErrorController.java @@ -0,0 +1,45 @@ +/* + * 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 com.example.notes; + +import java.time.Instant; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +class ErrorController { + + @RequestMapping("/error") + ResponseEntity> handleError(HttpServletRequest request) { + HttpStatus httpStatus = HttpStatus.valueOf((int)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)); + Map body = Map.of("status", httpStatus.value(), + "error", httpStatus.getReasonPhrase(), + "path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI), + "message", request.getAttribute(RequestDispatcher.ERROR_MESSAGE), + "timestamp", Instant.now().toEpochMilli()); + ResponseEntity> response = new ResponseEntity<>(body, httpStatus); + return response; + } + +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java index 53104ede..e2926477 100644 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -18,11 +18,11 @@ import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java index e20e4fec..afd1bc97 100644 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -16,14 +16,58 @@ package com.example.notes; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import javax.sql.DataSource; -@SpringBootApplication -public class RestNotesSlate { +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; - public static void main(String[] args) { - SpringApplication.run(RestNotesSlate.class, args); +import jakarta.persistence.EntityManagerFactory; + +@EnableWebMvc +@ComponentScan +@Configuration +@EnableJpaRepositories +@Import(RepositoryRestMvcConfiguration.class) +public class RestNotesSlate { + + @Bean + ObjectMapper objectMapper() { + return Jackson2ObjectMapperBuilder.json().build(); + } + + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setPackagesToScan("com.example.notes"); + HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); + jpaVendorAdapter.setShowSql(true); + jpaVendorAdapter.setGenerateDdl(true); + factory.setJpaVendorAdapter(jpaVendorAdapter); + factory.setDataSource(dataSource); + return factory; + } + + @Bean + DataSource dataSource() { + return new DriverManagerDataSource("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"); + } + + @Bean + PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); } } diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java index 8bd834f9..4ec2570f 100644 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 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. @@ -18,11 +18,11 @@ import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/samples/rest-notes-slate/src/main/resources/application.properties b/samples/rest-notes-slate/src/main/resources/application.properties deleted file mode 100644 index 8e06a828..00000000 --- a/samples/rest-notes-slate/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index 0416f6a8..c599ace8 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -38,26 +38,28 @@ import java.util.HashMap; import java.util.Map; -import jakarta.servlet.RequestDispatcher; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.RequestDispatcher; + +@WebAppConfiguration @RunWith(SpringRunner.class) -@SpringBootTest +@ContextConfiguration(classes = RestNotesSlate.class) public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 67d13cff..bde81c56 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -8,59 +8,97 @@ 0.0.1-SNAPSHOT jar - - org.springframework.boot - spring-boot-starter-parent - 2.4.7 - - - UTF-8 - 17 - 3.0.0-SNAPSHOT + 17 + 17 + 3.0.0-SNAPSHOT - org.springframework.boot - spring-boot-starter-data-rest + com.fasterxml.jackson.core + jackson-databind + 2.12.5 + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + + + org.hibernate.validator + hibernate-validator + 7.0.0.Final + + + org.hibernate + hibernate-core-jakarta + 5.5.7.Final - org.springframework.boot - spring-boot-starter-data-jpa + org.springframework + spring-webmvc + 6.0.0-SNAPSHOT + + + org.springframework.data + spring-data-jpa + 3.0.0-SNAPSHOT + + + org.springframework.data + spring-data-rest-webmvc + 4.0.0-SNAPSHOT com.h2database h2 + 1.4.200 + runtime + + + org.atteo + evo-inflector + 1.2.1 runtime - org.junit.vintage - junit-vintage-engine + com.jayway.jsonpath + json-path + 2.6.0 test - - - org.hamcrest - hamcrest-core - - - org.springframework.boot - spring-boot-starter-test + junit + junit + 4.12 test - com.jayway.jsonpath - json-path + org.assertj + assertj-core + 3.21.0 + test + + + org.glassfish + jakarta.el + 4.0.2 + test + + + org.hamcrest + hamcrest + 2.2 test org.springframework.restdocs spring-restdocs-mockmvc + ${restdocs.version} test @@ -68,12 +106,8 @@ - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins maven-surefire-plugin + 2.22.2 **/*Documentation.java @@ -104,12 +138,13 @@ org.springframework.restdocs spring-restdocs-asciidoctor - ${spring-restdocs.version} + ${restdocs.version} maven-resources-plugin + 3.2.0 copy-resources diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/ErrorController.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/ErrorController.java new file mode 100644 index 00000000..557090e0 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/ErrorController.java @@ -0,0 +1,45 @@ +/* + * 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 com.example.notes; + +import java.time.Instant; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +class ErrorController { + + @RequestMapping("/error") + ResponseEntity> handleError(HttpServletRequest request) { + HttpStatus httpStatus = HttpStatus.valueOf((int)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)); + Map body = Map.of("status", httpStatus.value(), + "error", httpStatus.getReasonPhrase(), + "path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI), + "message", request.getAttribute(RequestDispatcher.ERROR_MESSAGE), + "timestamp", Instant.now().toEpochMilli()); + ResponseEntity> response = new ResponseEntity<>(body, httpStatus); + return response; + } + +} diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java index 5160682a..e2926477 100644 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java +++ b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 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. @@ -18,11 +18,11 @@ import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java index 21195fd5..d9d380e6 100644 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java +++ b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -16,14 +16,58 @@ package com.example.notes; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import javax.sql.DataSource; -@SpringBootApplication -public class RestNotesSpringDataRest { +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; - public static void main(String[] args) { - SpringApplication.run(RestNotesSpringDataRest.class, args); - } +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManagerFactory; + +@EnableWebMvc +@ComponentScan +@Configuration +@EnableJpaRepositories +@Import(RepositoryRestMvcConfiguration.class) +public class RestNotesSpringDataRest { + + @Bean + ObjectMapper objectMapper() { + return Jackson2ObjectMapperBuilder.json().build(); + } + + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setPackagesToScan("com.example.notes"); + HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); + jpaVendorAdapter.setShowSql(true); + jpaVendorAdapter.setGenerateDdl(true); + factory.setJpaVendorAdapter(jpaVendorAdapter); + factory.setDataSource(dataSource); + return factory; + } + + @Bean + DataSource dataSource() { + return new DriverManagerDataSource("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"); + } + + @Bean + PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } + } diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java index 8bd834f9..4ec2570f 100644 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java +++ b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 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. @@ -18,11 +18,11 @@ import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index 746f3c51..3261af40 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 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. @@ -44,19 +44,21 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; +@WebAppConfiguration @RunWith(SpringRunner.class) -@SpringBootTest +@ContextConfiguration(classes = RestNotesSpringDataRest.class) public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 6dcecb1a..6cdc2f25 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -38,21 +38,25 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; +@Transactional +@WebAppConfiguration @RunWith(SpringRunner.class) -@SpringBootTest +@ContextConfiguration(classes = RestNotesSpringDataRest.class) public class GettingStartedDocumentation { @Rule diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 52fb3093..e9bc4e19 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -1,12 +1,8 @@ plugins { - id "eclipse" id "java" id "org.asciidoctor.jvm.convert" version "3.3.2" - id "org.springframework.boot" version "2.4.7" } -apply plugin: 'io.spring.dependency-management' - repositories { mavenLocal() maven { url 'https://repo.spring.io/milestone' } @@ -21,31 +17,34 @@ targetCompatibility = 17 ext { snippetsDir = file('build/generated-snippets') + restdocsVersion = '3.0.0-SNAPSHOT' } -ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' - configurations { asciidoctorExtensions } dependencies { - asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' + asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-hateoas' - implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation "com.fasterxml.jackson.core:jackson-databind:2.12.5" + implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" + implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" + implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" + implementation "org.springframework:spring-webmvc:6.0.0-SNAPSHOT" + implementation "org.springframework.data:spring-data-jpa:3.0.0-SNAPSHOT" + implementation "org.springframework.hateoas:spring-hateoas:2.0.0-SNAPSHOT" - runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'org.atteo:evo-inflector:1.2.1' - testImplementation 'com.jayway.jsonpath:json-path' - testImplementation 'org.assertj:assertj-core' - testImplementation('org.junit.vintage:junit-vintage-engine') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'com.jayway.jsonpath:json-path:2.6.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.assertj:assertj-core:3.21.0' + testImplementation 'org.hamcrest:hamcrest-library:2.2' + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" + + testRuntimeOnly 'org.glassfish:jakarta.el:4.0.2' } test { @@ -58,12 +57,10 @@ asciidoctor { dependsOn test } -bootJar { +jar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { into 'static/docs' } } -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ErrorController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ErrorController.java new file mode 100644 index 00000000..557090e0 --- /dev/null +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ErrorController.java @@ -0,0 +1,45 @@ +/* + * 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 com.example.notes; + +import java.time.Instant; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +class ErrorController { + + @RequestMapping("/error") + ResponseEntity> handleError(HttpServletRequest request) { + HttpStatus httpStatus = HttpStatus.valueOf((int)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)); + Map body = Map.of("status", httpStatus.value(), + "error", httpStatus.getReasonPhrase(), + "path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI), + "message", request.getAttribute(RequestDispatcher.ERROR_MESSAGE), + "timestamp", Instant.now().toEpochMilli()); + ResponseEntity> response = new ResponseEntity<>(body, httpStatus); + return response; + } + +} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java deleted file mode 100644 index 8da9b419..00000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2014-2017 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.notes; - -import java.util.Map; - -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.WebRequest; - -@Component -class ExceptionSupressingErrorAttributes extends DefaultErrorAttributes { - - @Override - public Map getErrorAttributes(WebRequest webRequest, - ErrorAttributeOptions options) { - Map errorAttributes = super.getErrorAttributes(webRequest, options); - errorAttributes.remove("exception"); - Object message = webRequest.getAttribute("jakarta.servlet.error.message", RequestAttributes.SCOPE_REQUEST); - if (message != null) { - errorAttributes.put("message", message); - } - return errorAttributes; - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java index 46b349da..d8ffb689 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java @@ -18,11 +18,11 @@ import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; @Entity public class Note { diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java index f2916a12..48220630 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -16,14 +16,58 @@ package com.example.notes; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import javax.sql.DataSource; -@SpringBootApplication +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.persistence.EntityManagerFactory; + +@EnableWebMvc +@ComponentScan +@Configuration +@EnableJpaRepositories +@EnableHypermediaSupport(type = HypermediaType.HAL) class RestNotesSpringHateoas { - public static void main(String[] args) { - SpringApplication.run(RestNotesSpringHateoas.class, args); + @Bean + ObjectMapper objectMapper() { + return Jackson2ObjectMapperBuilder.json().build(); } - + + @Bean + LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setPackagesToScan("com.example.notes"); + HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); + jpaVendorAdapter.setShowSql(true); + jpaVendorAdapter.setGenerateDdl(true); + factory.setJpaVendorAdapter(jpaVendorAdapter); + factory.setDataSource(dataSource); + return factory; + } + + @Bean + DataSource dataSource() { + return new DriverManagerDataSource("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"); + } + + @Bean + PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } + } diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java index b8274227..79b11611 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 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. @@ -18,11 +18,11 @@ import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; @Entity public class Tag { diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index c3e96b51..feaf512b 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 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. @@ -49,23 +49,27 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; +@Transactional +@WebAppConfiguration @RunWith(SpringRunner.class) -@SpringBootTest +@ContextConfiguration(classes = RestNotesSpringHateoas.class) public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 38ab9b60..236c0347 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -27,6 +27,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -41,21 +42,25 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; +@Transactional +@WebAppConfiguration @RunWith(SpringRunner.class) -@SpringBootTest +@ContextConfiguration(classes = RestNotesSpringHateoas.class) public class GettingStartedDocumentation { @Rule @@ -76,6 +81,7 @@ public void setUp() { .alwaysDo(document("{method-name}/{step}/", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()))) + .alwaysDo(print()) .build(); } diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java index 8b9f1c9a..60bc0e92 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 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. @@ -26,7 +26,6 @@ import org.junit.Test; - public class NullOrNotBlankTests { private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 5fd0cc4c..091abbbf 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -2,11 +2,8 @@ plugins { id "eclipse" id "java" id "org.asciidoctor.jvm.convert" version "3.3.2" - id "org.springframework.boot" version "2.4.7" } -apply plugin: 'io.spring.dependency-management' - repositories { mavenLocal() maven { url 'https://repo.spring.io/milestone' } @@ -20,21 +17,21 @@ sourceCompatibility = 17 targetCompatibility = 17 ext { + restdocsVersion = '3.0.0-SNAPSHOT' snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' - configurations { asciidoctorExtensions } dependencies { - asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' - implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' - testImplementation 'org.testng:testng:6.9.10' + asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" + + implementation "org.springframework:spring-webmvc:6.0.0-SNAPSHOT" + + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" + testImplementation "org.testng:testng:6.9.10" } test { @@ -48,12 +45,9 @@ asciidoctor { dependsOn test } -bootJar { +jar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { into 'static/docs' } } - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java index f0c6ab91..908a4126 100644 --- a/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java +++ b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -16,18 +16,13 @@ package com.example.testng; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@SpringBootApplication +@Configuration public class SampleTestNgApplication { - public static void main(String[] args) { - new SpringApplication(SampleTestNgApplication.class).run(args); - } - @RestController private static class SampleController { diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java index 1e0843b7..8606fb26 100644 --- a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 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. @@ -24,9 +24,10 @@ import java.lang.reflect.Method; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.restdocs.ManualRestDocumentation; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -34,7 +35,8 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -@SpringBootTest +@WebAppConfiguration +@ContextConfiguration(classes = SampleTestNgApplication.class) public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index a4dcddd7..2d1f4235 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -18,25 +18,23 @@ sourceCompatibility = 17 targetCompatibility = 17 ext { + restdocsVersion = '3.0.0-SNAPSHOT' snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' - configurations { asciidoctorExtensions } dependencies { - asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" + asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation 'io.projectreactor.ipc:reactor-netty:0.7.1.RELEASE' - implementation 'org.springframework:spring-context:5.0.1.RELEASE' - implementation 'org.springframework:spring-webflux:5.0.1.RELEASE' + implementation 'org.springframework:spring-context:6.0.0-SNAPSHOT' + implementation 'org.springframework:spring-webflux:6.0.0-SNAPSHOT' testImplementation 'junit:junit:4.12' - testImplementation 'org.springframework:spring-test:5.0.1.RELEASE' - testImplementation "org.springframework.restdocs:spring-restdocs-webtestclient:${project.ext['spring-restdocs.version']}" + testImplementation 'org.springframework:spring-test:6.0.0-SNAPSHOT' + testImplementation "org.springframework.restdocs:spring-restdocs-webtestclient:$restdocsVersion" } test { @@ -55,6 +53,3 @@ jar { into 'static/docs' } } - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/web-test-client/src/main/java/com/example/webtestclient/SampleWebTestClientApplication.java b/samples/web-test-client/src/main/java/com/example/webtestclient/SampleWebTestClientApplication.java index 7c4d0bd0..4ae6bc48 100644 --- a/samples/web-test-client/src/main/java/com/example/webtestclient/SampleWebTestClientApplication.java +++ b/samples/web-test-client/src/main/java/com/example/webtestclient/SampleWebTestClientApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 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. @@ -16,33 +16,22 @@ package com.example.webtestclient; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.ipc.netty.http.server.HttpServer; - @EnableWebFlux @Configuration public class SampleWebTestClientApplication { @Bean public RouterFunction routerFunction() { - return RouterFunctions.route(RequestPredicates.GET("/"), (request) -> ServerResponse.status(HttpStatus.OK).syncBody("Hello, World")); - } - - public static void main(String[] args) { - RouterFunction routerFunction = new AnnotationConfigApplicationContext(SampleWebTestClientApplication.class).getBean(RouterFunction.class); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(routerFunction)); - HttpServer httpServer = HttpServer.create(8080); - httpServer.startAndAwait(adapter); + return RouterFunctions.route(RequestPredicates.GET("/"), (request) -> ServerResponse.status(HttpStatus.OK).bodyValue("Hello, World")); } } From da98852d53827233a4cb3d0931baf498fed5b781 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Nov 2021 10:37:32 +0000 Subject: [PATCH 008/198] Upgrade to Spring Java Format 0.0.29 --- gradle.properties | 12 +----------- .../ResourceBundleConstraintDescriptionResolver.java | 1 + ...urceBundleConstraintDescriptionResolverTests.java | 1 + .../ValidatorConstraintResolverTests.java | 1 + .../mockmvc/MockMvcRequestConverterTests.java | 1 + .../mockmvc/MockMvcResponseConverterTests.java | 1 + .../MockMvcRestDocumentationIntegrationTests.java | 1 + .../RestDocumentationRequestBuildersTests.java | 1 + .../restdocs/restassured/TomcatServer.java | 5 +++-- 9 files changed, 11 insertions(+), 13 deletions(-) diff --git a/gradle.properties b/gradle.properties index 934f07db..af313fa2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,15 +3,5 @@ version=3.0.0-SNAPSHOT org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 org.gradle.parallel=true -asciidoctorj15Version=1.5.8.1 -asciidoctorj16Version=1.6.2 -asciidoctorj20Version=2.0.0 -asciidoctorj21Version=2.1.0 -asciidoctorj22Version=2.2.0 -asciidoctorj23Version=2.3.0 -asciidoctorj24Version=2.4.3 -asciidoctorj25Version=2.5.1 - -javaFormatVersion=0.0.27 - +javaFormatVersion=0.0.29 jmustacheVersion=1.12 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 e7fff063..3c9da3c9 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 @@ -42,6 +42,7 @@ import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; + import org.hibernate.validator.constraints.CodePointLength; import org.hibernate.validator.constraints.CreditCardNumber; import org.hibernate.validator.constraints.Currency; 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 e3dfd56e..d0103a72 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 @@ -49,6 +49,7 @@ import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; + import org.hibernate.validator.constraints.CodePointLength; import org.hibernate.validator.constraints.CreditCardNumber; import org.hibernate.validator.constraints.Currency; 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 3794a6c4..22b4d580 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 @@ -31,6 +31,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; import jakarta.validation.constraints.Size; + import org.assertj.core.api.Condition; import org.assertj.core.description.TextDescription; import org.hibernate.validator.constraints.CompositionType; 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 78bb536a..00372b40 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 @@ -22,6 +22,7 @@ import java.util.Iterator; import jakarta.servlet.http.Part; + import org.junit.Test; import org.springframework.http.HttpMethod; 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 4928a3fe..4b4816f8 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,6 +20,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; + import org.junit.Test; import org.springframework.http.HttpHeaders; 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 9b425dbd..8b0d91f5 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 @@ -33,6 +33,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; + import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; 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 53caf0ed..83203f65 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 @@ -19,6 +19,7 @@ import java.net.URI; import jakarta.servlet.ServletContext; + import org.junit.Test; import org.springframework.http.HttpMethod; 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 83f9b15a..9eee8b29 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 @@ -21,13 +21,14 @@ import java.util.HashMap; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; From ff411a0f603289b1d7f25b826e487ccb4f75b89b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Nov 2021 11:20:52 +0000 Subject: [PATCH 009/198] Update milestone in CI parameters --- ci/parameters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/parameters.yml b/ci/parameters.yml index 7632ead3..4b1d901a 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -3,7 +3,7 @@ github-repo-name: "spring-projects/spring-restdocs" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io" branch: "main" -milestone: "2.0.x" +milestone: "3.0.x" build-name: "spring-restdocs" concourse-url: "https://ci.spring.io" task-timeout: 1h00m From c7a47230ef90f50cc4787fd5d784587076d8bf8b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Nov 2021 11:22:04 +0000 Subject: [PATCH 010/198] Update Windows build to use JDK 17 --- ci/scripts/build-project-windows.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/scripts/build-project-windows.bat b/ci/scripts/build-project-windows.bat index 297740fa..24f0aeab 100755 --- a/ci/scripts/build-project-windows.bat +++ b/ci/scripts/build-project-windows.bat @@ -1,4 +1,4 @@ -SET "JAVA_HOME=C:\opt\jdk-8" +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 buildSamples From 615c9f3fed5f803cb63cd67f5664483aae7423f1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Nov 2021 11:42:39 +0000 Subject: [PATCH 011/198] Upgrade to latest Asciidoctor Gradle plugin --- docs/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build.gradle b/docs/build.gradle index f4367407..7cc585d9 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.asciidoctor.jvm.convert" version "3.2.0" + id "org.asciidoctor.jvm.convert" version "3.3.2" id "java-library" } From d2a5bb26eafc9e82f56a3f96d7c6b61ab1de8c48 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 18 Nov 2021 12:43:16 +0000 Subject: [PATCH 012/198] Drop support for REST Assured until it supports Jakarta EE 9 Closes gh-761 --- build.gradle | 5 - docs/build.gradle | 1 - docs/src/docs/asciidoc/configuration.adoc | 41 +- .../customizing-requests-and-responses.adoc | 30 +- .../docs/asciidoc/documenting-your-api.adoc | 216 +-------- docs/src/docs/asciidoc/getting-started.adoc | 93 +--- docs/src/docs/asciidoc/introduction.adoc | 9 +- .../CustomDefaultOperationPreprocessors.java | 49 --- .../restassured/CustomDefaultSnippets.java | 46 -- .../example/restassured/CustomEncoding.java | 45 -- .../com/example/restassured/CustomFormat.java | 45 -- .../restassured/EveryTestPreprocessing.java | 61 --- .../ExampleApplicationJUnit5Tests.java | 43 -- .../ExampleApplicationTestNgTests.java | 53 --- .../restassured/ExampleApplicationTests.java | 44 -- .../com/example/restassured/HttpHeaders.java | 48 -- .../com/example/restassured/Hypermedia.java | 51 --- .../example/restassured/InvokeService.java | 39 -- .../restassured/ParameterizedOutput.java | 45 -- .../example/restassured/PathParameters.java | 41 -- .../java/com/example/restassured/Payload.java | 115 ----- .../restassured/PerTestPreprocessing.java | 41 -- .../restassured/RequestParameters.java | 52 --- .../restassured/RequestPartPayload.java | 59 --- .../com/example/restassured/RequestParts.java | 41 -- .../restassured/RestAssuredSnippetReuse.java | 40 -- samples/rest-assured/build.gradle | 65 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/rest-assured/gradlew | 185 -------- samples/rest-assured/gradlew.bat | 89 ---- samples/rest-assured/settings.gradle | 0 .../rest-assured/src/docs/asciidoc/index.adoc | 26 -- .../SampleRestAssuredApplication.java | 41 -- .../SampleRestAssuredApplicationTests.java | 73 ---- settings.gradle | 1 - spring-restdocs-platform/build.gradle | 1 - spring-restdocs-restassured/build.gradle | 24 - ...suredOperationPreprocessorsConfigurer.java | 48 -- .../RestAssuredRequestConverter.java | 167 ------- .../RestAssuredResponseConverter.java | 53 --- .../RestAssuredRestDocumentation.java | 111 ----- ...estAssuredRestDocumentationConfigurer.java | 75 ---- .../RestAssuredSnippetConfigurer.java | 47 -- .../restassured/RestDocumentationFilter.java | 108 ----- .../preprocess/RestAssuredPreprocessors.java | 55 --- .../UriModifyingOperationPreprocessor.java | 47 -- .../operation/preprocess/package-info.java | 21 - .../restdocs/restassured/package-info.java | 20 - .../RestAssuredRequestConverterTests.java | 295 ------------- .../RestAssuredResponseConverterTests.java | 52 --- ...suredRestDocumentationConfigurerTests.java | 89 ---- ...uredRestDocumentationIntegrationTests.java | 410 ------------------ .../restdocs/restassured/TomcatServer.java | 125 ------ ...riModifyingOperationPreprocessorTests.java | 334 -------------- .../src/test/resources/body.txt | 1 - .../restdocs/templates/curl-request.snippet | 1 - 57 files changed, 23 insertions(+), 3899 deletions(-) delete mode 100644 docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java delete mode 100644 docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java delete mode 100644 docs/src/test/java/com/example/restassured/CustomEncoding.java delete mode 100644 docs/src/test/java/com/example/restassured/CustomFormat.java delete mode 100644 docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java delete mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java delete mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java delete mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTests.java delete mode 100644 docs/src/test/java/com/example/restassured/HttpHeaders.java delete mode 100644 docs/src/test/java/com/example/restassured/Hypermedia.java delete mode 100644 docs/src/test/java/com/example/restassured/InvokeService.java delete mode 100644 docs/src/test/java/com/example/restassured/ParameterizedOutput.java delete mode 100644 docs/src/test/java/com/example/restassured/PathParameters.java delete mode 100644 docs/src/test/java/com/example/restassured/Payload.java delete mode 100644 docs/src/test/java/com/example/restassured/PerTestPreprocessing.java delete mode 100644 docs/src/test/java/com/example/restassured/RequestParameters.java delete mode 100644 docs/src/test/java/com/example/restassured/RequestPartPayload.java delete mode 100644 docs/src/test/java/com/example/restassured/RequestParts.java delete mode 100644 docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java delete mode 100644 samples/rest-assured/build.gradle delete mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/rest-assured/gradlew delete mode 100644 samples/rest-assured/gradlew.bat delete mode 100644 samples/rest-assured/settings.gradle delete mode 100644 samples/rest-assured/src/docs/asciidoc/index.adoc delete mode 100644 samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java delete mode 100644 samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java delete mode 100644 spring-restdocs-restassured/build.gradle delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java delete mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java delete mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java delete mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java delete mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java delete mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java delete mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java delete mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java delete mode 100644 spring-restdocs-restassured/src/test/resources/body.txt delete mode 100644 spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet diff --git a/build.gradle b/build.gradle index 54c88ef6..dd11b52b 100644 --- a/build.gradle +++ b/build.gradle @@ -112,7 +112,6 @@ subprojects { subproject -> samples { dependOn "spring-restdocs-core:publishToMavenLocal" dependOn "spring-restdocs-mockmvc:publishToMavenLocal" - dependOn "spring-restdocs-restassured:publishToMavenLocal" dependOn "spring-restdocs-webtestclient:publishToMavenLocal" dependOn "spring-restdocs-asciidoctor:publishToMavenLocal" @@ -128,10 +127,6 @@ samples { workingDir "$projectDir/samples/testng" } - restAssured { - workingDir "$projectDir/samples/rest-assured" - } - webTestClient { workingDir "$projectDir/samples/web-test-client" } diff --git a/docs/build.gradle b/docs/build.gradle index f9ddeea1..e54dd339 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -19,7 +19,6 @@ dependencies { internal(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) testImplementation(project(":spring-restdocs-mockmvc")) - testImplementation(project(":spring-restdocs-restassured")) testImplementation(project(":spring-restdocs-webtestclient")) testImplementation("jakarta.validation:jakarta.validation-api") testImplementation("junit:junit") diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 6c6bd193..0435daf1 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -46,17 +46,6 @@ TIP: To configure a request's context path, use the `contextPath` method on -[[configuration-uris-rest-assured]] -==== REST Assured URI Customization - -REST Assured tests a service by making actual HTTP requests. As a result, URIs must be -customized once the operation on the service has been performed but before it is -documented. A -<> is provided for this purpose. - - - [[configuration-uris-webtestclient]] ==== WebTestClient URI Customization @@ -96,13 +85,6 @@ include::{examples-dir}/com/example/mockmvc/CustomEncoding.java[tags=custom-enco include::{examples-dir}/com/example/webtestclient/CustomEncoding.java[tags=custom-encoding] ---- -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom-encoding] ----- -==== - TIP: When Spring REST Docs converts the content of a request or a response to a `String`, the `charset` specified in the `Content-Type` header is used if it is available. In its absence, the JVM's default `Charset` is used. You can configure the JVM's default @@ -130,13 +112,6 @@ include::{examples-dir}/com/example/mockmvc/CustomFormat.java[tags=custom-format include::{examples-dir}/com/example/webtestclient/CustomFormat.java[tags=custom-format] ---- -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-format] ----- -==== - [[configuration-default-snippets]] @@ -168,12 +143,7 @@ include::{examples-dir}/com/example/mockmvc/CustomDefaultSnippets.java[tags=cust include::{examples-dir}/com/example/webtestclient/CustomDefaultSnippets.java[tags=custom-default-snippets] ---- -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/CustomDefaultSnippets.java[tags=custom-default-snippets] ----- -==== + [[configuration-default-preprocessors]] === Default Operation Preprocessors @@ -198,12 +168,3 @@ include::{examples-dir}/com/example/webtestclient/CustomDefaultOperationPreproce ---- <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. - -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/CustomDefaultOperationPreprocessors.java[tags=custom-default-operation-preprocessors] ----- -<1> Apply a request preprocessor that removes the header named `Foo`. -<2> Apply a response preprocessor that pretty prints its content. -==== diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 84f1268a..4f50c39e 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -27,15 +27,6 @@ include::{examples-dir}/com/example/webtestclient/PerTestPreprocessing.java[tags <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/PerTestPreprocessing.java[tags=preprocessing] ----- -<1> Apply a request preprocessor that removes the header named `Foo`. -<2> Apply a response preprocessor that pretty prints its content. -==== - Alternatively, you may want to apply the same preprocessors to every test. You can do so by using the `RestDocumentationConfigurer` API in your `@Before` method to configure the preprocessors. For example to remove the `Foo` header from all requests and pretty print @@ -58,15 +49,6 @@ include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[ta <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=setup] ----- -<1> Apply a request preprocessor that removes the header named `Foo`. -<2> Apply a response preprocessor that pretty prints its content. -==== - Then, in each test, you can perform any configuration specific to that test. The following examples show how to do so: @@ -83,13 +65,6 @@ include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[tags=use] ---- -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] ----- -==== - Various built-in preprocessors, including those illustrated above, are available through the static methods on `Preprocessors`. See <> for further details. @@ -154,9 +129,8 @@ TIP: If you use MockMvc or a WebTestClient that is not bound to a server, you should customize URIs by <>. You can use `modifyUris` on `Preprocessors` to modify any URIs in a request -or a response. When using REST Assured or WebTestClient bound to a server, this -lets you customize the URIs that appear in the documentation while testing a -local instance of the service. +or a response. When using WebTestClient bound to a server, this lets you customize the +URIs that appear in the documentation while testing a local instance of the service. diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c4a68fab..70badfa8 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -37,19 +37,6 @@ include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=links] `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <3> Expect a link whose `rel` is `bravo`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] ----- -<1> Configure Spring REST docs to produce a snippet describing the response's links. - Uses the static `links` method on - `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<2> Expect a link whose `rel` is `alpha`. Uses the static `linkWithRel` method on - `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<3> Expect a link whose `rel` is `bravo`. -==== - The result is a snippet named `links.adoc` that contains a table describing the resource's links. @@ -102,19 +89,6 @@ include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=explicit-e <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=explicit-extractor] ----- -<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on -`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -==== - -If your API represents its links in a format other than Atom or HAL, you can provide your -own implementation of the `LinkExtractor` interface to extract the links from the -response. - [[documenting-your-api-hypermedia-ignoring-common-links]] @@ -196,19 +170,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=response] on `org.springframework.restdocs.payload.PayloadDocumentation`. <3> Expect a field with the path `contact.name`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=response] ----- -<1> Configure Spring REST docs to produce a snippet describing the fields in the response - payload. To document a request, you can use `requestFields`. Both are static methods - on `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method - on `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.name`. -==== - The result is a snippet that contains a table describing the fields. For requests, this snippet is named `request-fields.adoc`. For responses, this snippet is named `response-fields.adoc`. @@ -239,16 +200,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=subsection] are now seen as having also been documented. Uses the static `subsectionWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] ----- -<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` - are now seen as having also been documented. Uses the static `subsectionWithPath` - method on `org.springframework.restdocs.payload.PayloadDocumentation`. -==== - `subsectionWithPath` can be useful for providing a high-level overview of a particular section of a payload. You can then produce separate, more detailed documentation for a subsection. See <>. @@ -434,13 +385,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=explicit-typ ---- <1> Set the field's type to `String`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] ----- -<1> Set the field's type to `String`. -==== [[documenting-your-api-request-response-payloads-fields-xml]] @@ -532,14 +476,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=single-book] ---- <1> Document `title` and `author` by using existing descriptors -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=single-book] ----- -<1> Document `title` and `author` by using existing descriptors -==== - You can also use the descriptors to document an array of books, as follows: ==== @@ -561,16 +497,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=book-array] <2> Document `[].title` and `[].author` by using the existing descriptors prefixed with `[].` -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] ----- -<1> Document the array. -<2> Document `[].title` and `[].author` by using the existing descriptors prefixed with - `[].` -==== - [[documenting-your-api-request-response-payloads-subsections]] ==== Documenting a Subsection of a Request or Response Payload @@ -624,17 +550,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=body-subsect `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet for the request body, you can use `requestBody` in place of `responseBody`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=body-subsection] ----- -<1> Produce a snippet containing a subsection of the response body. Uses the static - `responseBody` and `beneathPath` methods on - `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet for - the request body, you can use `requestBody` in place of `responseBody`. -==== - The result is a snippet with the following contents: ==== @@ -693,17 +608,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=fields-subse `org.springframework.restdocs.payload.PayloadDocumentation`. <2> Document the `high` and `low` fields. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=fields-subsection] ----- -<1> Produce a snippet describing the fields in the subsection of the response payload - beneath the path `weather.temperature`. Uses the static `beneathPath` method on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Document the `high` and `low` fields. -==== - The result is a snippet that contains a table describing the `high` and `low` fields of `weather.temperature`. To make the snippet's name distinct, an identifier for the subsection is included. By default, this identifier is `beneath-${path}`. For example, @@ -748,21 +652,6 @@ include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=re `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-query-string] ----- -<1> Configure Spring REST Docs to produce a snippet describing the request's parameters. - Uses the static `requestParameters` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document the `page` parameter. Uses the static `parameterWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document the `per_page` parameter. -<4> Perform a `GET` request with two parameters, `page` and `per_page`, in the query - string. -==== - You can also include request parameters as form data in the body of a POST request. The following examples show how to do so: @@ -781,15 +670,6 @@ include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=re ---- <1> Perform a `POST` request with a single parameter, `username`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-form-data] ----- -<1> Configure the `username` parameter. -<2> Perform the `POST` request. -==== - In all cases, the result is a snippet named `request-parameters.adoc` that contains a table describing the parameters that are supported by the resource. @@ -842,20 +722,6 @@ include::{examples-dir}/com/example/webtestclient/PathParameters.java[tags=path- `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/PathParameters.java[tags=path-parameters] ----- -<1> Configure Spring REST Docs to produce a snippet describing the request's path - parameters. Uses the static `pathParameters` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document the parameter named `latitude`. Uses the static `parameterWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document the parameter named `longitude`. -<4> Perform a `GET` request with two path parameters, `latitude` and `longitude`. -==== - The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. @@ -910,20 +776,6 @@ include::{examples-dir}/com/example/webtestclient/RequestParts.java[tags=request <3> Document the part named `file`. Uses the static `partWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/RequestParts.java[tags=request-parts] ----- -<1> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses - the static `requestParts` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document the part named `file`. Uses the static `partWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<3> Configure the request with the part named `file`. -<4> Perform the `POST` request to `/upload`. -==== - The result is a snippet named `request-parts.adoc` that contains a table describing the request parts that are supported by the resource. @@ -974,16 +826,6 @@ include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=b part named `metadata`. Uses the static `requestPartBody` method on `PayloadDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=body] ----- -<1> Configure Spring REST docs to produce a snippet containing the body of the request - part named `metadata`. Uses the static `requestPartBody` method on - `PayloadDocumentation`. -==== - The result is a snippet named `request-part-${part-name}-body.adoc` that contains the part's body. For example, documenting a part named `metadata` produces a snippet named `request-part-metadata-body.adoc`. @@ -1019,18 +861,6 @@ include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=f <2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=fields] ----- -<1> Configure Spring REST docs to produce a snippet describing the fields in the payload - of the request part named `metadata`. Uses the static `requestPartFields` method on - `PayloadDocumentation`. -<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on - `org.springframework.restdocs.payload.PayloadDocumentation`. -==== - The result is a snippet that contains a table describing the part's fields. This snippet is named `request-part-${part-name}-fields.adoc`. For example, documenting a part named `metadata` produces a snippet named `request-part-metadata-fields.adoc`. @@ -1089,21 +919,6 @@ include::{examples-dir}/com/example/webtestclient/HttpHeaders.java[tags=headers] <4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/HttpHeaders.java[tags=headers] ----- -<1> Configure Spring REST Docs to produce a snippet describing the request's headers. - Uses the static `requestHeaders` method on - `org.springframework.restdocs.headers.HeaderDocumentation`. -<2> Document the `Authorization` header. Uses the static `headerWithName` method on - `org.springframework.restdocs.headers.HeaderDocumentation. -<3> Produce a snippet describing the response's headers. Uses the static `responseHeaders` - method on `org.springframework.restdocs.headers.HeaderDocumentation`. -<4> Configure the request with an `Authorization` header that uses basic authentication. -==== - The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. Each contains a table describing the headers. @@ -1149,15 +964,6 @@ include::{examples-dir}/com/example/webtestclient/WebTestClientSnippetReuse.java <1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific to the resource that is being documented. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/RestAssuredSnippetReuse.java[tags=use] ----- -<1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific - to the resource that is being documented. -==== - The result of the example is that links with `rel` values of `first`, `last`, `next`, `previous`, `alpha`, and `bravo` are all documented. @@ -1317,10 +1123,8 @@ You can configure which snippets are produced by default. See the [[documentating-your-api-parameterized-output-directories]] === Using Parameterized Output Directories -When using MockMvc, REST Assured, or `WebTestClient` you can parameterize the output directory used by -`document`. - -The following parameters are supported: +You can parameterize the output directory used by `document`. The following +parameters are supported: [cols="1,3"] |=== @@ -1363,12 +1167,6 @@ in every test in the class. The following examples show how to do so: include::{examples-dir}/com/example/mockmvc/ParameterizedOutput.java[tags=parameterized-output] ---- -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/ParameterizedOutput.java[tags=parameterized-output] ----- - [source,java,indent=0,role="secondary"] .WebTestClient ---- @@ -1444,16 +1242,6 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=constraints] <2> Set the `constraints` attribute for the `name` field. <3> Set the `constraints` attribute for the `email` field. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/Payload.java[tags=constraints] ----- -<1> Configure the `title` attribute for the request fields snippet. -<2> Set the `constraints` attribute for the `name` field. -<3> Set the `constraints` attribute for the `email` field. -==== - The second step is to provide a custom template named `request-fields.snippet` that includes the information about the fields' constraints in the generated snippet's table and adds a title. The following example shows how to do so: diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8ae2279a..95cdee56 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -38,19 +38,6 @@ If you want to jump straight in, a number of sample applications are available: |=== - -[cols="3,2,10"] -.REST Assured -|=== -| Sample | Build system | Description - -| {samples}/rest-assured[REST Assured] -| Gradle -| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. - -|=== - - [cols="3,2,10"] .Advanced |=== @@ -79,8 +66,6 @@ Spring REST Docs has the following minimum requirements: * Java 17 * Spring Framework 6 -Additionally, the `spring-restdocs-restassured` module requires REST Assured 4 (4.4 or later). - [[getting-started-build-configuration]] === Build configuration @@ -132,8 +117,7 @@ the configuration are described in the following listings: ---- <1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. 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. + `WebTestClient` add a dependency on `spring-restdocs-webtestclient` instead. <2> Add the Asciidoctor plugin. <3> Using `prepare-package` allows the documentation to be <>. @@ -173,9 +157,7 @@ the configuration are described in the following listings: files to point to `build/generated-snippets`. It will also allow you to use the `operation` block macro. <3> 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. + you want to use `WebTestClient`, add a dependency on `spring-restdocs-webtestclient` instead. <4> Configure a property to define the output location for generated snippets. <5> Configure the `test` task to add the snippets directory as an output. <6> Configure the `asciidoctor` task @@ -257,11 +239,10 @@ from where it will be included in the jar file. [[getting-started-documentation-snippets]] === Generating Documentation Snippets Spring REST Docs uses Spring MVC's -{spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], -Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`], or -http://rest-assured.io/[REST Assured] to make requests to the service that you are -documenting. It then produces documentation snippets for the request and the resulting -response. +{spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework] or +Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] to +make requests to the service that you are documenting. It then produces documentation +snippets for the request and the resulting response. @@ -314,8 +295,8 @@ public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("cu ---- ==== -Next, you must provide an `@Before` method to configure MockMvc, WebTestClient or REST -Assured. The following examples show how to do so: +Next, you must provide an `@Before` method to configure MockMvc or WebTestClient. The +following examples show how to do so: ==== [source,java,indent=0,role="primary"] @@ -337,17 +318,6 @@ include::{examples-dir}/com/example/webtestclient/ExampleApplicationTests.java[t 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/ExampleApplicationTests.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. @@ -407,8 +377,8 @@ public class JUnit5ExampleTests { } ---- -Next, you must provide a `@BeforeEach` method to configure MockMvc, WebTestClient, or -REST Assured. The following listings show how to do so: +Next, you must provide a `@BeforeEach` method to configure MockMvc or WebTestClient. The +following listings show how to do so: ==== [source,java,indent=0,role="primary"] @@ -430,17 +400,6 @@ include::{examples-dir}/com/example/webtestclient/ExampleApplicationJUnit5Tests. 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. @@ -464,10 +423,9 @@ private ManualRestDocumentation restDocumentation = new ManualRestDocumentation( ---- ==== -Secondly, you must call `ManualRestDocumentation.beforeTest(Class, String)` -before each test. You can do so as part of the method that -configures MockMvc, WebTestClient, or REST Assured. -The following examples show how to do so: +Secondly, you must call `ManualRestDocumentation.beforeTest(Class, String)` before each +test. You can do so as part of the method that configures MockMvc or WebTestClient. The +following examples show how to do so: ==== [source,java,indent=0,role="primary"] @@ -482,20 +440,13 @@ include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[t include::{examples-dir}/com/example/webtestclient/ExampleApplicationTestNgTests.java[tags=setup] ---- -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=setup] ----- -==== - Finally, you must call `ManualRestDocumentation.afterTest` after each test. The following example shows how to do so with TestNG: ==== [source,java,indent=0] ---- -include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=teardown] +include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[tags=teardown] ---- ==== @@ -534,22 +485,6 @@ a `Consumer` of the `ExchangeResult`. You can obtain such a consumer from the st `document` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`. -[source,java,indent=0,role="secondary"] -.REST Assured ----- -include::{examples-dir}/com/example/restassured/InvokeService.java[tags=invoke-service] ----- -<1> Apply the specification that was initialized in the `@Before` method. -<2> Indicate that an `application/json` response is required. -<3> Document the call to the service, writing the snippets into a directory named `index` -(which is located beneath the configured output directory). The snippets are written by -a `RestDocumentationFilter`. You can obtain an instance of this class from the static -`document` method on `RestAssuredRestDocumentation` in the -`org.springframework.restdocs.restassured` package. -<4> Invoke the root (`/`) of the service. -<5> Assert that the service produce the expected response. -==== - By default, six snippets are written: * `/index/curl-request.adoc` diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index f20d7738..dd8ea5e1 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -11,11 +11,10 @@ produces HTML, styled and laid out to suit your needs. If you prefer, you can al configure Spring REST Docs to use Markdown. Spring REST Docs uses snippets produced by tests written with Spring MVC's -{spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring -WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or -http://rest-assured.io[REST Assured 4]. This test-driven approach helps to guarantee -the accuracy of your service's documentation. If a snippet is incorrect, the test that -produces it fails. +{spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework] or Spring +WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`]. This +test-driven approach helps to guarantee the accuracy of your service's documentation. If +a snippet is incorrect, the test that produces it fails. Documenting a RESTful service is largely about describing its resources. Two key parts of each resource's description are the details of the HTTP requests that it consumes diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java deleted file mode 100644 index cb392c91..00000000 --- a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java +++ /dev/null @@ -1,49 +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 com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; - -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -public class CustomDefaultOperationPreprocessors { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - @SuppressWarnings("unused") - private RequestSpecification spec; - - @Before - public void setup() { - // tag::custom-default-operation-preprocessors[] - this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> - .withResponseDefaults(prettyPrint())) // <2> - .build(); - // end::custom-default-operation-preprocessors[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java deleted file mode 100644 index 3eeb870d..00000000 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ /dev/null @@ -1,46 +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 com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; - -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(); - - @SuppressWarnings("unused") - private RequestSpecification spec; - - @Before - public void setUp() { - // tag::custom-default-snippets[] - this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.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 deleted file mode 100644 index bcfd0caa..00000000 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ /dev/null @@ -1,45 +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 com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; - -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -public class CustomEncoding { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - @SuppressWarnings("unused") - private RequestSpecification spec; - - @Before - public void setUp() { - // tag::custom-encoding[] - this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.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 deleted file mode 100644 index 3b6f6482..00000000 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ /dev/null @@ -1,45 +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 com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.templates.TemplateFormats; - -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -public class CustomFormat { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - @SuppressWarnings("unused") - private RequestSpecification spec; - - @Before - public void setUp() { - // tag::custom-format[] - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.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 deleted file mode 100644 index 6757da31..00000000 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ /dev/null @@ -1,61 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -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(); - - // tag::setup[] - private RequestSpecification spec; - - @Before - public void setup() { - this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> - .withResponseDefaults(prettyPrint())) // <2> - .build(); - } - // end::setup[] - - public void use() throws Exception { - // tag::use[] - RestAssured.given(this.spec) - .filter(document("index", links(linkWithRel("self").description("Canonical self link")))).when() - .get("/").then().assertThat().statusCode(is(200)); - // end::use[] - } - -} 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 7fa9bf1f..00000000 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java +++ /dev/null @@ -1,43 +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 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) -public class ExampleApplicationJUnit5Tests { - - @SuppressWarnings("unused") - // tag::setup[] - private RequestSpecification spec; - - @BeforeEach - public 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/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java deleted file mode 100644 index d1e07b21..00000000 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ /dev/null @@ -1,53 +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 com.example.restassured; - -import java.lang.reflect.Method; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; - -import org.springframework.restdocs.ManualRestDocumentation; - -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -public class ExampleApplicationTestNgTests { - - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); - - @SuppressWarnings("unused") - // tag::setup[] - private RequestSpecification spec; - - @BeforeMethod - public void setUp(Method method) { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)).build(); - this.restDocumentation.beforeTest(getClass(), method.getName()); - } - - // end::setup[] - - // tag::teardown[] - @AfterMethod - public void tearDown() { - this.restDocumentation.afterTest(); - } - // end::teardown[] - -} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java deleted file mode 100644 index 26c08c59..00000000 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ /dev/null @@ -1,44 +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 com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; - -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -public class ExampleApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - @SuppressWarnings("unused") - // tag::setup[] - private RequestSpecification spec; - - @Before - public void setUp() { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) // <1> - .build(); - } - // end::setup[] - -} diff --git a/docs/src/test/java/com/example/restassured/HttpHeaders.java b/docs/src/test/java/com/example/restassured/HttpHeaders.java deleted file mode 100644 index 697acbaf..00000000 --- a/docs/src/test/java/com/example/restassured/HttpHeaders.java +++ /dev/null @@ -1,48 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class HttpHeaders { - - private RequestSpecification spec; - - public void headers() throws Exception { - // tag::headers[] - RestAssured.given(this.spec).filter(document("headers", requestHeaders(// <1> - headerWithName("Authorization").description("Basic auth credentials")), // <2> - responseHeaders(// <3> - headerWithName("X-RateLimit-Limit") - .description("The total number of requests permitted per period"), - headerWithName("X-RateLimit-Remaining") - .description("Remaining requests permitted in current period"), - headerWithName("X-RateLimit-Reset") - .description("Time at which the rate limit period will reset")))) - .header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // <4> - .when().get("/people").then().assertThat().statusCode(is(200)); - // end::headers[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/Hypermedia.java b/docs/src/test/java/com/example/restassured/Hypermedia.java deleted file mode 100644 index 346411b3..00000000 --- a/docs/src/test/java/com/example/restassured/Hypermedia.java +++ /dev/null @@ -1,51 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class Hypermedia { - - private RequestSpecification spec; - - public void defaultExtractor() throws Exception { - // tag::links[] - RestAssured.given(this.spec).accept("application/json").filter(document("index", links(// <1> - linkWithRel("alpha").description("Link to the alpha resource"), // <2> - linkWithRel("bravo").description("Link to the bravo resource")))) // <3> - .get("/").then().assertThat().statusCode(is(200)); - // end::links[] - } - - public void explicitExtractor() throws Exception { - RestAssured.given(this.spec).accept("application/json") - // tag::explicit-extractor[] - .filter(document("index", links(halLinks(), // <1> - linkWithRel("alpha").description("Link to the alpha resource"), - linkWithRel("bravo").description("Link to the bravo resource")))) - // end::explicit-extractor[] - .get("/").then().assertThat().statusCode(is(200)); - } - -} diff --git a/docs/src/test/java/com/example/restassured/InvokeService.java b/docs/src/test/java/com/example/restassured/InvokeService.java deleted file mode 100644 index 70b884bd..00000000 --- a/docs/src/test/java/com/example/restassured/InvokeService.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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class InvokeService { - - private RequestSpecification spec; - - public void invokeService() throws Exception { - // tag::invoke-service[] - RestAssured.given(this.spec) // <1> - .accept("application/json") // <2> - .filter(document("index")) // <3> - .when().get("/") // <4> - .then().assertThat().statusCode(is(200)); // <5> - // end::invoke-service[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java deleted file mode 100644 index 6859417b..00000000 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ /dev/null @@ -1,45 +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 com.example.restassured; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; - -import org.springframework.restdocs.JUnitRestDocumentation; - -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -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)) - .addFilter(document("{method-name}/{step}")).build(); - } - // end::parameterized-output[] - -} diff --git a/docs/src/test/java/com/example/restassured/PathParameters.java b/docs/src/test/java/com/example/restassured/PathParameters.java deleted file mode 100644 index c6ca7e44..00000000 --- a/docs/src/test/java/com/example/restassured/PathParameters.java +++ /dev/null @@ -1,41 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class PathParameters { - - private RequestSpecification spec; - - public void pathParametersSnippet() throws Exception { - // tag::path-parameters[] - RestAssured.given(this.spec).filter(document("locations", pathParameters(// <1> - parameterWithName("latitude").description("The location's latitude"), // <2> - parameterWithName("longitude").description("The location's longitude")))) // <3> - .when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <4> - .then().assertThat().statusCode(is(200)); - // end::path-parameters[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java deleted file mode 100644 index 4c7b9f28..00000000 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ /dev/null @@ -1,115 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.restdocs.payload.JsonFieldType; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; - -public class Payload { - - private RequestSpecification spec; - - public void response() throws Exception { - // tag::response[] - RestAssured.given(this.spec).accept("application/json").filter(document("user", responseFields(// <1> - fieldWithPath("contact.name").description("The user's name"), // <2> - fieldWithPath("contact.email").description("The user's email address")))) // <3> - .when().get("/user/5").then().assertThat().statusCode(is(200)); - // end::response[] - } - - public void subsection() throws Exception { - // tag::subsection[] - RestAssured.given(this.spec).accept("application/json") - .filter(document("user", - responseFields(subsectionWithPath("contact").description("The user's contact details")))) // <1> - .when().get("/user/5").then().assertThat().statusCode(is(200)); - // end::subsection[] - } - - public void explicitType() throws Exception { - RestAssured.given(this.spec).accept("application/json") - // tag::explicit-type[] - .filter(document("user", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> - .description("The user's email address")))) - // end::explicit-type[] - .when().get("/user/5").then().assertThat().statusCode(is(200)); - } - - public void constraints() throws Exception { - RestAssured.given(this.spec).accept("application/json") - // tag::constraints[] - .filter(document("create-user", - requestFields(attributes(key("title").value("Fields for user creation")), // <1> - fieldWithPath("name").description("The user's name") - .attributes(key("constraints").value("Must not be null. Must not be empty")), // <2> - fieldWithPath("email").description("The user's email address") - .attributes(key("constraints").value("Must be a valid email address"))))) // <3> - // end::constraints[] - .when().post("/users").then().assertThat().statusCode(is(200)); - } - - public void descriptorReuse() throws Exception { - FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"), - fieldWithPath("author").description("Author of the book") }; - - // tag::single-book[] - RestAssured.given(this.spec).accept("application/json").filter(document("book", responseFields(book))) // <1> - .when().get("/books/1").then().assertThat().statusCode(is(200)); - // end::single-book[] - - // tag::book-array[] - RestAssured.given(this.spec).accept("application/json") - .filter(document("books", responseFields(fieldWithPath("[]").description("An array of books")) // <1> - .andWithPrefix("[].", book))) // <2> - .when().get("/books").then().assertThat().statusCode(is(200)); - // end::book-array[] - } - - public void fieldsSubsection() throws Exception { - // tag::fields-subsection[] - RestAssured.given(this.spec).accept("application/json") - .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> - fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> - fieldWithPath("low").description("The forecast low in degrees celcius")))) - .when().get("/locations/1").then().assertThat().statusCode(is(200)); - // end::fields-subsection[] - } - - public void bodySubsection() throws Exception { - // tag::body-subsection[] - RestAssured.given(this.spec).accept("application/json") - .filter(document("location", responseBody(beneathPath("weather.temperature")))) // <1> - .when().get("/locations/1").then().assertThat().statusCode(is(200)); - // end::body-subsection[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java deleted file mode 100644 index 02974491..00000000 --- a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java +++ /dev/null @@ -1,41 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class PerTestPreprocessing { - - private RequestSpecification spec; - - public void general() throws Exception { - // tag::preprocessing[] - RestAssured.given(this.spec).filter(document("index", preprocessRequest(removeHeaders("Foo")), // <1> - preprocessResponse(prettyPrint()))) // <2> - .when().get("/").then().assertThat().statusCode(is(200)); - // end::preprocessing[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/RequestParameters.java deleted file mode 100644 index 229c62be..00000000 --- a/docs/src/test/java/com/example/restassured/RequestParameters.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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class RequestParameters { - - private RequestSpecification spec; - - public void getQueryStringSnippet() throws Exception { - // tag::request-parameters-query-string[] - RestAssured.given(this.spec).filter(document("users", requestParameters(// <1> - parameterWithName("page").description("The page to retrieve"), // <2> - parameterWithName("per_page").description("Entries per page")))) // <3> - .when().get("/users?page=2&per_page=100") // <4> - .then().assertThat().statusCode(is(200)); - // end::request-parameters-query-string[] - } - - public void postFormDataSnippet() throws Exception { - // tag::request-parameters-form-data[] - RestAssured.given(this.spec) - .filter(document("create-user", - requestParameters(parameterWithName("username").description("The user's username")))) - .formParam("username", "Tester") // <1> - .when().post("/users") // <2> - .then().assertThat().statusCode(is(200)); - // end::request-parameters-form-data[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java deleted file mode 100644 index f7eb05a6..00000000 --- a/docs/src/test/java/com/example/restassured/RequestPartPayload.java +++ /dev/null @@ -1,59 +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 com.example.restassured; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class RequestPartPayload { - - private RequestSpecification spec; - - public void fields() throws Exception { - // tag::fields[] - Map metadata = new HashMap<>(); - metadata.put("version", "1.0"); - RestAssured.given(this.spec).accept("application/json") - .filter(document("image-upload", requestPartFields("metadata", // <1> - fieldWithPath("version").description("The version of the image")))) // <2> - .when().multiPart("image", new File("image.png"), "image/png").multiPart("metadata", metadata) - .post("images").then().assertThat().statusCode(is(200)); - // end::fields[] - } - - public void body() throws Exception { - // tag::body[] - Map metadata = new HashMap<>(); - metadata.put("version", "1.0"); - RestAssured.given(this.spec).accept("application/json") - .filter(document("image-upload", requestPartBody("metadata"))) // <1> - .when().multiPart("image", new File("image.png"), "image/png").multiPart("metadata", metadata) - .post("images").then().assertThat().statusCode(is(200)); - // end::body[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java deleted file mode 100644 index 32b2e0e9..00000000 --- a/docs/src/test/java/com/example/restassured/RequestParts.java +++ /dev/null @@ -1,41 +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 com.example.restassured; - -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.request.RequestDocumentation.partWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class RequestParts { - - private RequestSpecification spec; - - public void upload() throws Exception { - // tag::request-parts[] - RestAssured.given(this.spec).filter(document("users", requestParts(// <1> - partWithName("file").description("The file to upload")))) // <2> - .multiPart("file", "example") // <3> - .when().post("/upload") // <4> - .then().statusCode(is(200)); - // end::request-parts[] - } - -} diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java deleted file mode 100644 index c2267c1c..00000000 --- a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java +++ /dev/null @@ -1,40 +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 com.example.restassured; - -import com.example.SnippetReuse; -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; - -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; - -public class RestAssuredSnippetReuse extends SnippetReuse { - - private RequestSpecification spec; - - public void documentation() throws Exception { - // tag::use[] - RestAssured.given(this.spec).accept("application/json").filter(document("example", this.pagingLinks.and(// <1> - linkWithRel("alpha").description("Link to the alpha resource"), - linkWithRel("bravo").description("Link to the bravo resource")))).get("/").then().assertThat() - .statusCode(is(200)); - // end::use[] - } - -} diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle deleted file mode 100644 index f7237456..00000000 --- a/samples/rest-assured/build.gradle +++ /dev/null @@ -1,65 +0,0 @@ -plugins { - id "eclipse" - id "java" - id "org.asciidoctor.jvm.convert" version "3.3.2" - id "org.springframework.boot" version "2.4.7" -} - -apply plugin: 'io.spring.dependency-management' - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 17 -targetCompatibility = 17 - -ext { - snippetsDir = file('build/generated-snippets') -} - -ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' -ext['rest-assured.version'] = '4.4.0' -ext['groovy.version'] = '3.0.8' - -configurations { - asciidoctorExtensions -} - -dependencies { - asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' - - implementation 'org.springframework.boot:spring-boot-starter-web' - - testImplementation 'io.rest-assured:rest-assured:4.4.0' - testImplementation('org.junit.vintage:junit-vintage-engine') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' -} - -test { - outputs.dir snippetsDir -} - -asciidoctor { - configurations "asciidoctorExtensions" - inputs.dir snippetsDir - dependsOn test -} - -bootJar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

    K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ffed3a25..00000000 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew deleted file mode 100755 index 4f906e0c..00000000 --- a/samples/rest-assured/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat deleted file mode 100644 index 107acd32..00000000 --- a/samples/rest-assured/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -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. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-assured/settings.gradle b/samples/rest-assured/settings.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/samples/rest-assured/src/docs/asciidoc/index.adoc b/samples/rest-assured/src/docs/asciidoc/index.adoc deleted file mode 100644 index f89aeb6e..00000000 --- a/samples/rest-assured/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,26 +0,0 @@ -= Spring REST Docs REST Assured Sample -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs - -Sample application demonstrating how to use Spring REST Docs with REST Assured. - -`SampleRestAssuredApplicationTests` makes a call to a very simple service. The service -that is being tested is running on a random port on `localhost`. The tests make use of a -preprocessor to modify the request so that it appears to have been sent to -`https://api.example.com`. If your service includes URIs in its responses, for example -because it uses hypermedia, similar preprocessing can be applied to the response before -it is documented. - -Three snippets are produced. One showing how to make a request using cURL: - -include::{snippets}/sample/curl-request.adoc[] - -One showing the HTTP request: - -include::{snippets}/sample/http-request.adoc[] - -And one showing the HTTP response: - -include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java deleted file mode 100644 index 1c127db7..00000000 --- a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2014-2016 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 org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootApplication -public class SampleRestAssuredApplication { - - public static void main(String[] args) { - new SpringApplication(SampleRestAssuredApplication.class).run(args); - } - - @RestController - private static class SampleController { - - @RequestMapping("/") - public String index() { - return "Hello, World"; - } - - } - -} diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java deleted file mode 100644 index 8e1dc7c5..00000000 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ /dev/null @@ -1,73 +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 com.example.restassured; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; - -@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) -@RunWith(SpringJUnit4ClassRunner.class) -public class SampleRestAssuredApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - private RequestSpecification documentationSpec; - - @LocalServerPort - private int port; - - @Before - public void setUp() { - this.documentationSpec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(restDocumentation)).build(); - } - - @Test - public void sample() throws Exception { - given(this.documentationSpec) - .accept("text/plain") - .filter(document("sample", - preprocessRequest(modifyUris() - .scheme("https") - .host("api.example.com") - .removePort()))) - .when() - .port(this.port) - .get("/") - .then() - .assertThat().statusCode(is(200)); - } - -} diff --git a/settings.gradle b/settings.gradle index 2e81bf85..2b8b30b0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,5 +39,4 @@ include "spring-restdocs-asciidoctor" include "spring-restdocs-core" include "spring-restdocs-mockmvc" include "spring-restdocs-platform" -include "spring-restdocs-restassured" include "spring-restdocs-webtestclient" diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 7c411813..1be0d31e 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -14,7 +14,6 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api:5.0.0") api("jakarta.validation:jakarta.validation-api:3.0.0") api("junit:junit:4.12") - api("io.rest-assured:rest-assured:4.4.0") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.2") api("org.asciidoctor:asciidoctorj-pdf:1.6.0") diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle deleted file mode 100644 index dbcd51ba..00000000 --- a/spring-restdocs-restassured/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -plugins { - id "java-library" - id "maven-publish" -} - -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" - } - implementation("org.springframework:spring-web") - - internal(platform(project(":spring-restdocs-platform"))) - - testImplementation(testFixtures(project(":spring-restdocs-core"))) - testImplementation("com.fasterxml.jackson.core:jackson-databind") - testImplementation("junit:junit") - testImplementation("org.apache.tomcat.embed:tomcat-embed-core:10.0.11") - testImplementation("org.assertj:assertj-core") - testImplementation("org.hamcrest:hamcrest-library") - testImplementation("org.mockito:mockito-core") -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java deleted file mode 100644 index 2b738858..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java +++ /dev/null @@ -1,48 +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.restassured; - -import io.restassured.filter.Filter; -import io.restassured.filter.FilterContext; -import io.restassured.response.Response; -import io.restassured.specification.FilterableRequestSpecification; -import io.restassured.specification.FilterableResponseSpecification; - -import org.springframework.restdocs.config.OperationPreprocessorsConfigurer; - -/** - * A configurer that can be used to configure the operation preprocessors when using REST - * Assured 3. - * - * @author Filip Hrisafov - * @since 2.0.0 - */ -public final class RestAssuredOperationPreprocessorsConfigurer extends - OperationPreprocessorsConfigurer - implements Filter { - - RestAssuredOperationPreprocessorsConfigurer(RestAssuredRestDocumentationConfigurer parent) { - super(parent); - } - - @Override - public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, - FilterContext context) { - return and().filter(requestSpec, responseSpec, context); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java deleted file mode 100644 index 8da8f911..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ /dev/null @@ -1,167 +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.restassured; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map.Entry; - -import io.restassured.http.Cookie; -import io.restassured.http.Header; -import io.restassured.specification.FilterableRequestSpecification; -import io.restassured.specification.MultiPartSpecification; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.RequestConverter; -import org.springframework.restdocs.operation.RequestCookie; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StreamUtils; - -/** - * A converter for creating an {@link OperationRequest} from a REST Assured - * {@link FilterableRequestSpecification}. - * - * @author Andy Wilkinson - */ -class RestAssuredRequestConverter implements RequestConverter { - - @Override - public OperationRequest convert(FilterableRequestSpecification requestSpec) { - return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), - HttpMethod.valueOf(requestSpec.getMethod()), extractContent(requestSpec), extractHeaders(requestSpec), - extractParameters(requestSpec), extractParts(requestSpec), extractCookies(requestSpec)); - } - - private Collection extractCookies(FilterableRequestSpecification requestSpec) { - Collection cookies = new ArrayList<>(); - for (Cookie cookie : requestSpec.getCookies()) { - cookies.add(new RequestCookie(cookie.getName(), cookie.getValue())); - } - return cookies; - } - - private byte[] extractContent(FilterableRequestSpecification requestSpec) { - return convertContent(requestSpec.getBody()); - } - - private byte[] convertContent(Object content) { - if (content instanceof String) { - return ((String) content).getBytes(); - } - else if (content instanceof byte[]) { - return (byte[]) content; - } - else if (content instanceof File) { - return copyToByteArray((File) content); - } - else if (content instanceof InputStream) { - return copyToByteArray((InputStream) content); - } - else if (content == null) { - return new byte[0]; - } - else { - throw new IllegalStateException("Unsupported request content: " + content.getClass().getName()); - } - } - - private byte[] copyToByteArray(File file) { - try { - return FileCopyUtils.copyToByteArray(file); - } - catch (IOException ex) { - throw new IllegalStateException("Failed to read content from file " + file, ex); - } - } - - private byte[] copyToByteArray(InputStream inputStream) { - try { - inputStream.reset(); - } - catch (IOException ex) { - throw new IllegalStateException( - "Cannot read content from input stream " + inputStream + " due to reset() failure"); - } - try { - return StreamUtils.copyToByteArray(inputStream); - } - catch (IOException ex) { - throw new IllegalStateException("Failed to read content from input stream " + inputStream, ex); - } - } - - private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { - HttpHeaders httpHeaders = new HttpHeaders(); - for (Header header : requestSpec.getHeaders()) { - if (!isAllMediaTypesAcceptHeader(header)) { - httpHeaders.add(header.getName(), header.getValue()); - } - } - return httpHeaders; - } - - private boolean isAllMediaTypesAcceptHeader(Header header) { - return HttpHeaders.ACCEPT.equals(header.getName()) && "*/*".equals(header.getValue()); - } - - private Parameters extractParameters(FilterableRequestSpecification requestSpec) { - Parameters parameters = new Parameters(); - for (Entry entry : requestSpec.getQueryParams().entrySet()) { - if (entry.getValue() instanceof Collection) { - Collection queryParams = ((Collection) entry.getValue()); - for (Object queryParam : queryParams) { - parameters.add(entry.getKey(), queryParam.toString()); - } - } - else { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - } - for (Entry entry : requestSpec.getRequestParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - for (Entry entry : requestSpec.getFormParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - return parameters; - } - - private Collection extractParts(FilterableRequestSpecification requestSpec) { - List parts = new ArrayList<>(); - for (MultiPartSpecification multiPartSpec : requestSpec.getMultiPartParams()) { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType((multiPartSpec.getMimeType() != null) - ? MediaType.parseMediaType(multiPartSpec.getMimeType()) : MediaType.TEXT_PLAIN); - parts.add(new OperationRequestPartFactory().create(multiPartSpec.getControlName(), - multiPartSpec.getFileName(), convertContent(multiPartSpec.getContent()), headers)); - } - return parts; - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java deleted file mode 100644 index d3888e03..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java +++ /dev/null @@ -1,53 +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.restassured; - -import io.restassured.http.Header; -import io.restassured.response.Response; - -import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.ResponseConverter; - -/** - * A converter for creating an {@link OperationResponse} from a REST Assured - * {@link Response}. - * - * @author Andy Wilkinson - */ -class RestAssuredResponseConverter implements ResponseConverter { - - @Override - public OperationResponse convert(Response response) { - return new OperationResponseFactory().create(response.getStatusCode(), extractHeaders(response), - extractContent(response)); - } - - private HttpHeaders extractHeaders(Response response) { - HttpHeaders httpHeaders = new HttpHeaders(); - for (Header header : response.getHeaders()) { - httpHeaders.add(header.getName(), header.getValue()); - } - return httpHeaders; - } - - private byte[] extractContent(Response response) { - return response.getBody().asByteArray(); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java deleted file mode 100644 index fac2b825..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ /dev/null @@ -1,111 +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.restassured; - -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; -import org.springframework.restdocs.snippet.Snippet; - -/** - * Static factory methods for documenting RESTful APIs using REST Assured 3. - * - * @author Andy Wilkinson - * @since 1.2.0 - */ -public abstract class RestAssuredRestDocumentation { - - private static final RestAssuredRequestConverter REQUEST_CONVERTER = new RestAssuredRequestConverter(); - - private static final RestAssuredResponseConverter RESPONSE_CONVERTER = new RestAssuredResponseConverter(); - - private RestAssuredRestDocumentation() { - - } - - /** - * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. - * @param identifier an identifier for the API call that is being documented - * @param snippets the snippets that will document the API call - * @return a {@link RestDocumentationFilter} that will produce the documentation - */ - public static RestDocumentationFilter document(String identifier, Snippet... snippets) { - return new RestDocumentationFilter( - new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); - } - - /** - * Documents the API call with the given {@code identifier} using the given - * {@code snippets} in addition to any default snippets. The given - * {@code requestPreprocessor} is applied to the request before it is documented. - * @param identifier an identifier for the API call that is being documented - * @param requestPreprocessor the request preprocessor - * @param snippets the snippets - * @return a {@link RestDocumentationFilter} that will produce the documentation - */ - public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, - Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, - RESPONSE_CONVERTER, requestPreprocessor, snippets)); - } - - /** - * Documents the API call with the given {@code identifier} using the given - * {@code snippets} in addition to any default snippets. The given - * {@code responsePreprocessor} is applied to the request before it is documented. - * @param identifier an identifier for the API call that is being documented - * @param responsePreprocessor the response preprocessor - * @param snippets the snippets - * @return a {@link RestDocumentationFilter} that will produce the documentation - */ - public static RestDocumentationFilter document(String identifier, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, - RESPONSE_CONVERTER, responsePreprocessor, snippets)); - } - - /** - * Documents the API call with the given {@code identifier} using the given - * {@code snippets} in addition to any default snippets. The given - * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the - * request and response respectively before they are documented. - * @param identifier an identifier for the API call that is being documented - * @param requestPreprocessor the request preprocessor - * @param responsePreprocessor the response preprocessor - * @param snippets the snippets - * @return a {@link RestDocumentationFilter} that will produce the documentation - */ - public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, - RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); - } - - /** - * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be - * used to configure Spring REST Docs using the given {@code contextProvider}. - * @param contextProvider the context provider - * @return the configurer - */ - public static RestAssuredRestDocumentationConfigurer documentationConfiguration( - RestDocumentationContextProvider contextProvider) { - return new RestAssuredRestDocumentationConfigurer(contextProvider); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java deleted file mode 100644 index 3e4f4370..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ /dev/null @@ -1,75 +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.restassured; - -import java.util.HashMap; -import java.util.Map; - -import io.restassured.filter.Filter; -import io.restassured.filter.FilterContext; -import io.restassured.response.Response; -import io.restassured.specification.FilterableRequestSpecification; -import io.restassured.specification.FilterableResponseSpecification; - -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.config.RestDocumentationConfigurer; - -/** - * A REST Assured 3-specific {@link RestDocumentationConfigurer}. - * - * @author Andy Wilkinson - * @author Filip Hrisafov - * @since 1.2.0 - */ -public final class RestAssuredRestDocumentationConfigurer extends - RestDocumentationConfigurer - implements Filter { - - private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer(this); - - private final RestAssuredOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new RestAssuredOperationPreprocessorsConfigurer( - this); - - private final RestDocumentationContextProvider contextProvider; - - RestAssuredRestDocumentationConfigurer(RestDocumentationContextProvider contextProvider) { - this.contextProvider = contextProvider; - } - - @Override - public RestAssuredSnippetConfigurer snippets() { - return this.snippetConfigurer; - } - - @Override - public RestAssuredOperationPreprocessorsConfigurer operationPreprocessors() { - return this.operationPreprocessorsConfigurer; - } - - @Override - public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, - FilterContext filterContext) { - RestDocumentationContext context = this.contextProvider.beforeOperation(); - filterContext.setValue(RestDocumentationContext.class.getName(), context); - Map configuration = new HashMap<>(); - filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, configuration); - apply(configuration, context); - return filterContext.next(requestSpec, responseSpec); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java deleted file mode 100644 index 5d4bff9b..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java +++ /dev/null @@ -1,47 +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.restassured; - -import io.restassured.filter.Filter; -import io.restassured.filter.FilterContext; -import io.restassured.response.Response; -import io.restassured.specification.FilterableRequestSpecification; -import io.restassured.specification.FilterableResponseSpecification; - -import org.springframework.restdocs.config.SnippetConfigurer; - -/** - * A configurer that can be used to configure the generated documentation snippets when - * using REST Assured 3. - * - * @author Andy Wilkinson - * @since 1.2.0 - */ -public final class RestAssuredSnippetConfigurer extends - SnippetConfigurer implements Filter { - - RestAssuredSnippetConfigurer(RestAssuredRestDocumentationConfigurer parent) { - super(parent); - } - - @Override - public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, - FilterContext context) { - return and().filter(requestSpec, responseSpec, context); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java deleted file mode 100644 index 63539187..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ /dev/null @@ -1,108 +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.restassured; - -import java.util.HashMap; -import java.util.Map; - -import io.restassured.filter.Filter; -import io.restassured.filter.FilterContext; -import io.restassured.response.Response; -import io.restassured.specification.FilterableRequestSpecification; -import io.restassured.specification.FilterableResponseSpecification; - -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.snippet.Snippet; -import org.springframework.util.Assert; - -/** - * A REST Assured 3 {@link Filter} for documenting RESTful APIs. - * - * @author Andy Wilkinson - * @since 1.2.0 - */ -public class RestDocumentationFilter implements Filter { - - static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; - - private final RestDocumentationGenerator delegate; - - RestDocumentationFilter(RestDocumentationGenerator delegate) { - Assert.notNull(delegate, "delegate must be non-null"); - this.delegate = delegate; - } - - @Override - public final Response filter(FilterableRequestSpecification requestSpec, - FilterableResponseSpecification responseSpec, FilterContext context) { - Response response = context.next(requestSpec, responseSpec); - - Map configuration = getConfiguration(requestSpec, context); - - this.delegate.handle(requestSpec, response, configuration); - - return response; - } - - /** - * Returns the configuration that should be used when calling the delgate. The - * configuration is derived from the given {@code requestSpec} and {@code context}. - * @param requestSpec the request specification - * @param context the filter context - * @return the configuration - */ - protected Map getConfiguration(FilterableRequestSpecification requestSpec, FilterContext context) { - Map configuration = new HashMap<>(retrieveConfiguration(context)); - configuration.put(RestDocumentationContext.class.getName(), - context.getValue(RestDocumentationContext.class.getName())); - configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); - return configuration; - } - - /** - * Creates a new {@link RestDocumentationFilter} that will produce documentation using - * the given {@code snippets}. - * @param snippets the snippets - * @return the new result handler - */ - public final RestDocumentationFilter document(Snippet... snippets) { - return new RestDocumentationFilter(this.delegate.withSnippets(snippets)) { - - @Override - protected Map getConfiguration(FilterableRequestSpecification requestSpec, - FilterContext context) { - Map configuration = super.getConfiguration(requestSpec, context); - configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); - configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR); - configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR); - return configuration; - } - - }; - } - - private static Map retrieveConfiguration(FilterContext context) { - Map configuration = context.getValue(CONTEXT_KEY_CONFIGURATION); - Assert.state(configuration != null, - () -> "REST Docs configuration not found. Did you forget to add a " - + RestAssuredRestDocumentationConfigurer.class.getSimpleName() - + " as a filter when building the RequestSpecification?"); - return configuration; - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java deleted file mode 100644 index 86f7213d..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java +++ /dev/null @@ -1,55 +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.restassured.operation.preprocess; - -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.preprocess.Preprocessors; -import org.springframework.restdocs.operation.preprocess.UriModifyingOperationPreprocessor; - -/** - * Static factory methods for creating - * {@link org.springframework.restdocs.operation.preprocess.OperationPreprocessor - * OperationPreprocessors} for use with REST Assured 3. They can be applied to an - * {@link Operation Operation's} {@link OperationRequest request} or - * {@link OperationResponse response} before it is documented. - * - * @author Andy Wilkinson - * @since 1.1.0 - * @deprecated since 2.0.1 in favor of {@link Preprocessors} and, specifically, - * {@link Preprocessors#modifyUris()} - */ -@Deprecated -public abstract class RestAssuredPreprocessors { - - private RestAssuredPreprocessors() { - - } - - /** - * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the - * request or response by changing one or more of their host, scheme, and port. - * @return the preprocessor - * @deprecated since 2.0.1 in favor of {@link Preprocessors#modifyUris()} - */ - @Deprecated - public static UriModifyingOperationPreprocessor modifyUris() { - return new UriModifyingOperationPreprocessor(); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java deleted file mode 100644 index b7aec38d..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ /dev/null @@ -1,47 +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.restassured.operation.preprocess; - -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; - -/** - * An {@link OperationPreprocessor} that modifies URIs in the request and in the response - * by changing one or more of their host, scheme, and port. URIs in the following - * locations are modified: - *

      - *
    • {@link OperationRequest#getUri() Request URI} - *
    • {@link OperationRequest#getHeaders() Request headers} - *
    • {@link OperationRequest#getContent() Request content} - *
    • {@link OperationRequestPart#getHeaders() Request part headers} - *
    • {@link OperationRequestPart#getContent() Request part content} - *
    • {@link OperationResponse#getHeaders() Response headers} - *
    • {@link OperationResponse#getContent() Response content} - *
    - * - * @author Andy Wilkinson - * @since 1.2.0 - * @deprecated since 2.0.1 in favor of - * {@link org.springframework.restdocs.operation.preprocess.UriModifyingOperationPreprocessor} - */ -@Deprecated -public class UriModifyingOperationPreprocessor - extends org.springframework.restdocs.operation.preprocess.UriModifyingOperationPreprocessor { - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java deleted file mode 100644 index a0e80251..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java +++ /dev/null @@ -1,21 +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. - */ - -/** - * REST Assured-specific support for preprocessing an operation prior to it being - * documented. - */ -package org.springframework.restdocs.restassured.operation.preprocess; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java deleted file mode 100644 index b4c02f36..00000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java +++ /dev/null @@ -1,20 +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. - */ - -/** - * Core classes for using Spring REST Docs with REST Assured 3. - */ -package org.springframework.restdocs.restassured; 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 deleted file mode 100644 index f7590fbd..00000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ /dev/null @@ -1,295 +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.restassured; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; - -import io.restassured.RestAssured; -import io.restassured.specification.FilterableRequestSpecification; -import io.restassured.specification.RequestSpecification; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.RequestCookie; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link RestAssuredRequestConverter}. - * - * @author Andy Wilkinson - */ -public class RestAssuredRequestConverterTests { - - @ClassRule - public static TomcatServer tomcat = new TomcatServer(); - - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); - - @Test - public void requestUri() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); - requestSpec.get("/foo/bar"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:" + tomcat.getPort() + "/foo/bar")); - } - - @Test - public void requestMethod() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); - requestSpec.head("/foo/bar"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getMethod()).isEqualTo(HttpMethod.HEAD); - } - - @Test - public void queryStringParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).queryParam("foo", "bar"); - requestSpec.get("/"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); - } - - @Test - public void queryStringFromUrlParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); - requestSpec.get("/?foo=bar&foo=qix"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Arrays.asList("bar", "qix")); - } - - @Test - public void formParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).formParam("foo", "bar"); - requestSpec.get("/"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); - } - - @Test - public void requestParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).param("foo", "bar"); - requestSpec.get("/"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); - } - - @Test - 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())); - } - - @Test - public void headersWithCustomAccept() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).header("Foo", "bar") - .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())); - } - - @Test - public void cookies() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).cookie("cookie1", "cookieVal1") - .cookie("cookie2", "cookieVal2"); - requestSpec.get("/"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getCookies().size()).isEqualTo(2); - - Iterator cookieIterator = request.getCookies().iterator(); - RequestCookie cookie1 = cookieIterator.next(); - - assertThat(cookie1.getName()).isEqualTo("cookie1"); - assertThat(cookie1.getValue()).isEqualTo("cookieVal1"); - - RequestCookie cookie2 = cookieIterator.next(); - assertThat(cookie2.getName()).isEqualTo("cookie2"); - assertThat(cookie2.getValue()).isEqualTo("cookieVal2"); - } - - @Test - public void multipart() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("a", "a.txt", "alpha", null).multiPart("b", new ObjectBody("bar"), "application/json"); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - Collection parts = request.getParts(); - assertThat(parts).hasSize(2); - 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).containsExactly( - Collections.singletonList(MediaType.TEXT_PLAIN_VALUE), - Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); - } - - @Test - public void byteArrayBody() { - RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()).port(tomcat.getPort()); - requestSpec.post(); - this.factory.convert((FilterableRequestSpecification) requestSpec); - } - - @Test - public void stringBody() { - RequestSpecification requestSpec = RestAssured.given().body("body").port(tomcat.getPort()); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContentAsString()).isEqualTo("body"); - } - - @Test - public void objectBody() { - RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")).port(tomcat.getPort()); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContentAsString()).isEqualTo("{\"foo\":\"bar\"}"); - } - - @Test - public void byteArrayInputStreamBody() { - RequestSpecification requestSpec = RestAssured.given().body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) - .port(tomcat.getPort()); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); - } - - @Test - public void fileBody() { - RequestSpecification requestSpec = RestAssured.given().body(new File("src/test/resources/body.txt")) - .port(tomcat.getPort()); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContentAsString()).isEqualTo("file"); - } - - @Test - public void fileInputStreamBody() throws FileNotFoundException { - FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); - RequestSpecification requestSpec = RestAssured.given().body(inputStream).port(tomcat.getPort()); - requestSpec.post(); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Cannot read content from input stream " + inputStream + " due to reset() failure"); - this.factory.convert((FilterableRequestSpecification) requestSpec); - } - - @Test - public void multipartWithByteArrayInputStreamBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("foo", "foo.txt", - new ByteArrayInputStream("foo".getBytes())); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); - } - - @Test - public void multipartWithStringBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", "foo"); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); - } - - @Test - public void multipartWithByteArrayBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", "file", - "foo".getBytes()); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); - } - - @Test - public void multipartWithFileBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart(new File("src/test/resources/body.txt")); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("file"); - } - - @Test - public void multipartWithFileInputStreamBody() throws FileNotFoundException { - FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("foo", "foo.txt", - inputStream); - requestSpec.post(); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Cannot read content from input stream " + inputStream + " due to reset() failure"); - this.factory.convert((FilterableRequestSpecification) requestSpec); - } - - @Test - public void multipartWithObjectBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", - new ObjectBody("bar")); - requestSpec.post(); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("{\"foo\":\"bar\"}"); - } - - /** - * Sample object body to verify JSON serialization. - */ - public static class ObjectBody { - - private final String foo; - - ObjectBody(String foo) { - this.foo = foo; - } - - public String getFoo() { - return this.foo; - } - - } - -} 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 deleted file mode 100644 index f7db8684..00000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.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.restassured; - -import io.restassured.http.Headers; -import io.restassured.response.Response; -import io.restassured.response.ResponseBody; -import org.junit.Test; - -import org.springframework.restdocs.operation.OperationResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link RestAssuredResponseConverter}. - * - * @author Andy Wilkinson - */ -public class RestAssuredResponseConverterTests { - - private final RestAssuredResponseConverter converter = new RestAssuredResponseConverter(); - - @Test - public void responseWithCustomStatus() { - Response response = mock(Response.class); - given(response.getStatusCode()).willReturn(600); - given(response.getHeaders()).willReturn(new Headers()); - ResponseBody body = mock(ResponseBody.class); - given(response.getBody()).willReturn(body); - given(body.asByteArray()).willReturn(new byte[0]); - OperationResponse operationResponse = this.converter.convert(response); - assertThat(operationResponse.getStatus()).isNull(); - assertThat(operationResponse.getStatusCode()).isEqualTo(600); - } - -} 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 deleted file mode 100644 index 40d1fee5..00000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ /dev/null @@ -1,89 +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.restassured; - -import java.util.List; -import java.util.Map; - -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.mockito.ArgumentCaptor; - -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; -import org.springframework.restdocs.operation.preprocess.Preprocessors; -import org.springframework.restdocs.snippet.WriterResolver; -import org.springframework.restdocs.templates.TemplateEngine; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link RestAssuredRestDocumentationConfigurer}. - * - * @author Andy Wilkinson - * @author Filip Hrisafov - */ -public class RestAssuredRestDocumentationConfigurerTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); - - private final FilterableResponseSpecification responseSpec = mock(FilterableResponseSpecification.class); - - private final FilterContext filterContext = mock(FilterContext.class); - - private final RestAssuredRestDocumentationConfigurer configurer = new RestAssuredRestDocumentationConfigurer( - this.restDocumentation); - - @Test - public void nextFilterIsCalled() { - this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); - verify(this.filterContext).next(this.requestSpec, this.responseSpec); - } - - @Test - public void configurationIsAddedToTheContext() { - this.configurer.operationPreprocessors().withRequestDefaults(Preprocessors.prettyPrint()) - .withResponseDefaults(Preprocessors.removeHeaders("Foo")) - .filter(this.requestSpec, this.responseSpec, this.filterContext); - @SuppressWarnings("rawtypes") - ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); - verify(this.filterContext).setValue(eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), - configurationCaptor.capture()); - @SuppressWarnings("unchecked") - Map configuration = configurationCaptor.getValue(); - assertThat(configuration.get(TemplateEngine.class.getName())).isInstanceOf(TemplateEngine.class); - assertThat(configuration.get(WriterResolver.class.getName())).isInstanceOf(WriterResolver.class); - assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS)) - .isInstanceOf(List.class); - assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR)) - .isInstanceOf(OperationRequestPreprocessor.class); - assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR)) - .isInstanceOf(OperationResponsePreprocessor.class); - } - -} 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 deleted file mode 100644 index 760884c7..00000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ /dev/null @@ -1,410 +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.restassured; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.charset.StandardCharsets; -import java.util.regex.Pattern; - -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.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -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.util.FileCopyUtils; -import org.springframework.web.bind.annotation.RequestMethod; - -import static io.restassured.RestAssured.given; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.partWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -/** - * Integration tests for using Spring REST Docs with REST Assured. - * - * @author Andy Wilkinson - * @author Tomasz Kopczynski - * @author Filip Hrisafov - */ -public class RestAssuredRestDocumentationIntegrationTests { - - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - @ClassRule - public static TomcatServer tomcat = new TomcatServer(); - - @Test - public void defaultSnippetGeneration() { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("default")).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc"); - } - - @Test - public void curlSnippetWithContent() throws Exception { - String contentType = "text/plain; charset=UTF-8"; - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("curl-snippet-with-content")).accept("application/json").body("content") - .contentType(contentType).post("/").then().statusCode(200); - - assertThat(new File("build/generated-snippets/curl-snippet-with-content/curl-request.adoc")).has(content( - codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ curl 'http://localhost:" - + tomcat.getPort() + "/' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" - + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'content'")))); - } - - @Test - public void curlSnippetWithCookies() throws Exception { - String contentType = "text/plain; charset=UTF-8"; - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("curl-snippet-with-cookies")).accept("application/json").contentType(contentType) - .cookie("cookieName", "cookieVal").get("/").then().statusCode(200); - assertThat(new File("build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc")).has(content( - codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ curl 'http://localhost:" - + tomcat.getPort() + "/' -i -X GET \\%n" + " -H 'Accept: application/json' \\%n" - + " -H 'Content-Type: " + contentType + "' \\%n" + " --cookie 'cookieName=cookieVal'")))); - } - - @Test - public void curlSnippetWithEmptyParameterQueryString() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("curl-snippet-with-empty-parameter-query-string")).accept("application/json") - .param("a", "").get("/").then().statusCode(200); - assertThat( - new File("build/generated-snippets/curl-snippet-with-empty-parameter-query-string/curl-request.adoc")) - .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") - .withContent(String.format("$ curl 'http://localhost:" + tomcat.getPort() - + "/?a=' -i -X GET \\%n -H 'Accept: application/json'")))); - } - - @Test - public void curlSnippetWithQueryStringOnPost() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("curl-snippet-with-query-string")).accept("application/json").param("foo", "bar") - .param("a", "alpha").post("/?foo=bar").then().statusCode(200); - String contentType = "application/x-www-form-urlencoded; charset=ISO-8859-1"; - assertThat(new File("build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc")) - .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") - .withContent(String.format("$ curl " + "'http://localhost:" + tomcat.getPort() - + "/?foo=bar' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" - + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'a=alpha'")))); - } - - @Test - public void linksSnippet() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("links", links(linkWithRel("rel").description("The description")))) - .accept("application/json").get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "links.adoc"); - } - - @Test - public void pathParametersSnippet() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("path-parameters", - pathParameters(parameterWithName("foo").description("The description")))) - .accept("application/json").get("/{foo}", "").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/path-parameters"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "path-parameters.adoc"); - } - - @Test - public void requestParametersSnippet() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-parameters", - requestParameters(parameterWithName("foo").description("The description")))) - .accept("application/json").param("foo", "bar").get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parameters"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "request-parameters.adoc"); - } - - @Test - public void requestFieldsSnippet() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-fields", requestFields(fieldWithPath("a").description("The description")))) - .accept("application/json").body("{\"a\":\"alpha\"}").post("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-fields"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); - } - - @Test - public void requestPartsSnippet() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-parts", requestParts(partWithName("a").description("The description")))) - .multiPart("a", "foo").post("/upload").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parts"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); - } - - @Test - public void responseFieldsSnippet() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("response-fields", - responseFields(fieldWithPath("a").description("The description"), - subsectionWithPath("links").description("Links to other resources")))) - .accept("application/json").get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/response-fields"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "response-fields.adoc"); - } - - @Test - public void parameterizedOutputDirectory() throws Exception { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("{method-name}")).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/parameterized-output-directory"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - } - - @Test - public void multiStep() throws Exception { - RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) - .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(document("{method-name}-{step}")).build(); - given(spec).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc"); - given(spec).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc"); - given(spec).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc"); - } - - @Test - public void additionalSnippets() throws Exception { - RestDocumentationFilter documentation = document("{method-name}-{step}"); - RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) - .addFilter(documentationConfiguration(this.restDocumentation)).addFilter(documentation).build(); - given(spec).filter(documentation.document( - responseHeaders(headerWithName("a").description("one"), headerWithName("Foo").description("two")))) - .get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/additional-snippets-1/"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", "response-headers.adoc"); - } - - @Test - public void responseWithCookie() { - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("set-cookie", - preprocessResponse(removeHeaders(HttpHeaders.DATE, HttpHeaders.CONTENT_TYPE)))) - .get("/set-cookie").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/set-cookie"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc"); - assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc")) - .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) - .header(HttpHeaders.SET_COOKIE, "name=value; Domain=localhost; HttpOnly") - .header("Keep-Alive", "timeout=60").header("Connection", "keep-alive"))); - } - - @Test - public void preprocessedRequest() throws Exception { - Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)).header("a", "alpha") - .header("b", "bravo").contentType("application/json").accept("application/json") - .body("{\"a\":\"alpha\"}").filter(document("original-request")) - .filter(document("preprocessed-request", - preprocessRequest(prettyPrint(), replacePattern(pattern, "\"<>\""), - modifyUris().removePort(), removeHeaders("a", HttpHeaders.CONTENT_LENGTH)))) - .get("/").then().statusCode(200); - assertThat(new File("build/generated-snippets/original-request/http-request.adoc")) - .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("a", "alpha") - .header("b", "bravo").header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Type", "application/json").header("Host", "localhost:" + tomcat.getPort()) - .header("Content-Length", "13").content("{\"a\":\"alpha\"}"))); - String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); - assertThat(new File("build/generated-snippets/preprocessed-request/http-request.adoc")) - .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") - .header("Accept", MediaType.APPLICATION_JSON_VALUE).header("Content-Type", "application/json") - .header("Host", "localhost").content(prettyPrinted))); - } - - @Test - public void defaultPreprocessedRequest() throws Exception { - Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation).operationPreprocessors().withRequestDefaults( - prettyPrint(), replacePattern(pattern, "\"<>\""), modifyUris().removePort(), - removeHeaders("a", HttpHeaders.CONTENT_LENGTH))) - .header("a", "alpha").header("b", "bravo").contentType("application/json").accept("application/json") - .body("{\"a\":\"alpha\"}").filter(document("default-preprocessed-request")).get("/").then() - .statusCode(200); - String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); - assertThat(new File("build/generated-snippets/default-preprocessed-request/http-request.adoc")) - .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") - .header("Accept", MediaType.APPLICATION_JSON_VALUE).header("Content-Type", "application/json") - .header("Host", "localhost").content(prettyPrinted))); - } - - @Test - public void preprocessedResponse() throws Exception { - Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("original-response")) - .filter(document("preprocessed-response", - preprocessResponse(prettyPrint(), maskLinks(), - removeHeaders("a", "Transfer-Encoding", "Date", "Server"), - replacePattern(pattern, "\"<>\""), - modifyUris().scheme("https").host("api.example.com").removePort()))) - .get("/").then().statusCode(200); - String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " - + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); - assertThat(new File("build/generated-snippets/preprocessed-response/http-response.adoc")) - .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) - .header("Foo", "https://api.example.com/foo/bar") - .header("Content-Type", "application/json;charset=UTF-8").header("Keep-Alive", "timeout=60") - .header("Connection", "keep-alive") - .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length).content(prettyPrinted))); - } - - @Test - public void defaultPreprocessedResponse() throws Exception { - Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withResponseDefaults(prettyPrint(), maskLinks(), - removeHeaders("a", "Transfer-Encoding", "Date", "Server"), - replacePattern(pattern, "\"<>\""), - modifyUris().scheme("https").host("api.example.com").removePort())) - .filter(document("default-preprocessed-response")).get("/").then().statusCode(200); - String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " - + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); - assertThat(new File("build/generated-snippets/default-preprocessed-response/http-response.adoc")) - .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) - .header("Foo", "https://api.example.com/foo/bar") - .header("Content-Type", "application/json;charset=UTF-8").header("Keep-Alive", "timeout=60") - .header("Connection", "keep-alive") - .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length).content(prettyPrinted))); - } - - @Test - public void customSnippetTemplate() throws Exception { - ClassLoader classLoader = new URLClassLoader( - new URL[] { new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, - getClass().getClassLoader()); - ClassLoader previous = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(classLoader); - try { - given().port(tomcat.getPort()).accept("application/json") - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("custom-snippet-template")).get("/").then().statusCode(200); - } - finally { - Thread.currentThread().setContextClassLoader(previous); - } - assertThat(new File("build/generated-snippets/custom-snippet-template/curl-request.adoc")) - .hasContent("Custom curl request"); - } - - @Test - public 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 " - + "RestAssuredRestDocumentationConfigurer as a filter when building the RequestSpecification?"); - } - - @Test - public void exceptionShouldBeThrownWhenCallDocumentSnippetsRequestSpecificationNotConfigured() { - RestDocumentationFilter documentation = document("{method-name}-{step}"); - assertThatThrownBy(() -> given().port(tomcat.getPort()) - .filter(documentation.document(responseHeaders(headerWithName("a").description("one")))).get("/")) - .isInstanceOf(IllegalStateException.class) - .hasMessage("REST Docs configuration not found. Did you forget to add a " - + "RestAssuredRestDocumentationConfigurer as a filter when building the " - + "RequestSpecification?"); - } - - private void assertExpectedSnippetFilesExist(File directory, String... snippets) { - for (String snippet : snippets) { - assertThat(new File(directory, snippet)).isFile(); - } - } - - private Condition content(final Condition delegate) { - return new Condition() { - - @Override - public boolean matches(File value) { - try { - return delegate.matches(FileCopyUtils - .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8))); - } - catch (IOException ex) { - fail("Failed to read '" + value + "'", ex); - return false; - } - } - - }; - } - - private CodeBlockCondition codeBlock(TemplateFormat format, String language) { - return SnippetConditions.codeBlock(format, language); - } - - private HttpRequestCondition httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { - return SnippetConditions.httpRequest(format, requestMethod, uri); - } - - private HttpResponseCondition httpResponse(TemplateFormat format, HttpStatus status) { - return SnippetConditions.httpResponse(format, status); - } - -} 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 deleted file mode 100644 index 9eee8b29..00000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java +++ /dev/null @@ -1,125 +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.restassured; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.startup.Tomcat; -import org.junit.rules.ExternalResource; - -/** - * {@link ExternalResource} that starts and stops a Tomcat server. - * - * @author Andy Wilkinson - */ -class TomcatServer extends ExternalResource { - - private Tomcat tomcat; - - 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.start(); - this.port = this.tomcat.getConnector().getLocalPort(); - } - - @Override - protected void after() { - try { - this.tomcat.stop(); - } - catch (LifecycleException ex) { - throw new RuntimeException(ex); - } - } - - int getPort() { - return this.port; - } - - /** - * {@link HttpServlet} used to handle requests in the tests. - */ - private static final class TestServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - respondWithJson(response); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - respondWithJson(response); - } - - private void respondWithJson(HttpServletResponse response) throws IOException, JsonProcessingException { - response.setCharacterEncoding("UTF-8"); - response.setContentType("application/json"); - Map content = new HashMap<>(); - content.put("a", "alpha"); - Map link = new HashMap<>(); - link.put("rel", "rel"); - link.put("href", "href"); - content.put("links", Arrays.asList(link)); - response.getWriter().println(new ObjectMapper().writeValueAsString(content)); - response.setHeader("a", "alpha"); - response.setHeader("Foo", "http://localhost:12345/foo/bar"); - response.flushBuffer(); - } - - } - - /** - * {@link HttpServlet} used to handle cookies-related requests in the tests. - */ - private static final class CookiesServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Cookie cookie = new Cookie("name", "value"); - cookie.setDomain("localhost"); - cookie.setHttpOnly(true); - - resp.addCookie(cookie); - } - - } - -} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java deleted file mode 100644 index 96e7efa5..00000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ /dev/null @@ -1,334 +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.restassured.operation.preprocess; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.RequestCookie; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link UriModifyingOperationPreprocessor}. - * - * @author Andy Wilkinson - */ -@Deprecated -public class UriModifyingOperationPreprocessorTests { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final UriModifyingOperationPreprocessor preprocessor = new UriModifyingOperationPreprocessor(); - - @Test - public 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() { - 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")); - assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST)).isEqualTo("api.example.com:12345"); - } - - @Test - public void requestUriPortCanBeModified() { - this.preprocessor.port(23456); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345")); - assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com:23456")); - assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST)).isEqualTo("api.example.com:23456"); - } - - @Test - public void requestUriPortCanBeRemoved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345")); - assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com")); - assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST)).isEqualTo("api.example.com"); - } - - @Test - public void requestUriPathIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345/foo/bar")); - assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com/foo/bar")); - } - - @Test - public void requestUriQueryIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345?foo=bar")); - assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com?foo=bar")); - } - - @Test - public void requestUriAnchorIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345#foo")); - assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com#foo")); - } - - @Test - public void requestContentUriSchemeCanBeModified() { - this.preprocessor.scheme("https"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'https://localhost:12345' should be used"); - } - - @Test - public void requestContentUriHostCanBeModified() { - this.preprocessor.host("api.example.com"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'https://localhost:12345' should be used")); - assertThat(new String(processed.getContent())) - .isEqualTo("The uri 'https://api.example.com:12345' should be used"); - } - - @Test - public void requestContentUriPortCanBeModified() { - this.preprocessor.port(23456); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost:23456' should be used"); - } - - @Test - public void requestContentUriPortCanBeRemoved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost' should be used"); - } - - @Test - public void multipleRequestContentUrisCanBeModified() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( - "Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); - assertThat(new String(processed.getContent())) - .isEqualTo("Use 'http://localhost' or 'https://localhost' to access the service"); - } - - @Test - public void requestContentUriPathIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost/foo/bar' should be used"); - } - - @Test - public void requestContentUriQueryIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost?foo=bar' should be used"); - } - - @Test - public void requestContentUriAnchorIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent("The uri 'http://localhost:12345#foo' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost#foo' should be used"); - } - - @Test - public void responseContentUriSchemeCanBeModified() { - this.preprocessor.scheme("https"); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'https://localhost:12345' should be used"); - } - - @Test - public void responseContentUriHostCanBeModified() { - this.preprocessor.host("api.example.com"); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'https://localhost:12345' should be used")); - assertThat(new String(processed.getContent())) - .isEqualTo("The uri 'https://api.example.com:12345' should be used"); - } - - @Test - public void responseContentUriPortCanBeModified() { - this.preprocessor.port(23456); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost:23456' should be used"); - } - - @Test - public void responseContentUriPortCanBeRemoved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost' should be used"); - } - - @Test - public void multipleResponseContentUrisCanBeModified() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor.preprocess(createResponseWithContent( - "Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); - assertThat(new String(processed.getContent())) - .isEqualTo("Use 'http://localhost' or 'https://localhost' to access the service"); - } - - @Test - public void responseContentUriPathIsPreserved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost/foo/bar' should be used"); - } - - @Test - public void responseContentUriQueryIsPreserved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost?foo=bar' should be used"); - } - - @Test - public void responseContentUriAnchorIsPreserved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent("The uri 'http://localhost:12345#foo' should be used")); - assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost#foo' should be used"); - } - - @Test - public 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"); - assertThat(processed.getHeaders().getFirst("Host")).isEqualTo("api.example.com"); - } - - @Test - public 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() { - OperationRequest processed = this.preprocessor.host("api.example.com") - .preprocess(createRequestWithPartWithHeader("Foo", "https://locahost:12345")); - assertThat(processed.getParts().iterator().next().getHeaders().getFirst("Foo")) - .isEqualTo("https://api.example.com:12345"); - } - - @Test - public 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())) - .isEqualTo("The uri 'https://api.example.com:12345' should be used"); - } - - @Test - public void modifiedUriDoesNotGetDoubleEncoded() { - this.preprocessor.scheme("https"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("http://localhost:12345?foo=%7B%7D")); - assertThat(processed.getUri()).isEqualTo(URI.create("https://localhost:12345?foo=%7B%7D")); - - } - - @Test - public 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(), new Parameters(), Collections.emptyList(), - cookies); - OperationRequest processed = this.preprocessor.preprocess(request); - assertThat(processed.getCookies().size()).isEqualTo(1); - } - - private OperationRequest createRequestWithUri(String uri) { - return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], new HttpHeaders(), - new Parameters(), Collections.emptyList()); - } - - private OperationRequest createRequestWithContent(String content) { - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, content.getBytes(), - new HttpHeaders(), new Parameters(), Collections.emptyList()); - } - - private OperationRequest createRequestWithHeader(String name, String value) { - HttpHeaders headers = new HttpHeaders(); - headers.add(name, value); - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], headers, - new Parameters(), Collections.emptyList()); - } - - private OperationRequest createRequestWithPartWithHeader(String name, String value) { - HttpHeaders headers = new HttpHeaders(); - headers.add(name, value); - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], - new HttpHeaders(), new Parameters(), - Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", new byte[0], headers))); - } - - private OperationRequest createRequestWithPartWithContent(String content) { - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], - new HttpHeaders(), new Parameters(), Arrays.asList(new OperationRequestPartFactory().create("part", - "fileName", content.getBytes(), new HttpHeaders()))); - } - - private OperationResponse createResponseWithContent(String content) { - return this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), content.getBytes()); - } - - private OperationResponse createResponseWithHeader(String name, String value) { - HttpHeaders headers = new HttpHeaders(); - headers.add(name, value); - return this.responseFactory.create(HttpStatus.OK, headers, new byte[0]); - } - -} diff --git a/spring-restdocs-restassured/src/test/resources/body.txt b/spring-restdocs-restassured/src/test/resources/body.txt deleted file mode 100644 index 1a010b1c..00000000 --- a/spring-restdocs-restassured/src/test/resources/body.txt +++ /dev/null @@ -1 +0,0 @@ -file \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet deleted file mode 100644 index 07f3a48f..00000000 --- a/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet +++ /dev/null @@ -1 +0,0 @@ -Custom curl request \ No newline at end of file From a6a7a931795ba09ef3ad803cc22d58c89cdd433c Mon Sep 17 00:00:00 2001 From: izeye Date: Thu, 25 Nov 2021 23:11:25 +0900 Subject: [PATCH 013/198] Fix typo in OperationResponseFactory Javadoc See gh-765 --- .../restdocs/operation/OperationResponseFactory.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index dd658ce1..a86c6209 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -31,8 +31,8 @@ public class OperationResponseFactory { * {@code headers} will be augmented to ensure that they include a * {@code Content-Length} header. * @param status the status of the response - * @param headers the request's headers - * @param content the content of the request + * @param headers the response's headers + * @param content the content of the response * @return the {@code OperationResponse} * @deprecated since 2.0.4 in favor of {@link #create(int, HttpHeaders, byte[])} */ @@ -46,8 +46,8 @@ public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] c * {@code headers} will be augmented to ensure that they include a * {@code Content-Length} header. * @param status the status of the response - * @param headers the request's headers - * @param content the content of the request + * @param headers the response's headers + * @param content the content of the response * @return the {@code OperationResponse} */ public OperationResponse create(int status, HttpHeaders headers, byte[] content) { From 42e84d1db3bba1ef4ebb50c0e51c7f2ffb58e70a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 4 Jan 2022 11:49:26 +0000 Subject: [PATCH 014/198] Polish "Fix typo in OperationResponseFactory Javadoc" See gh-765 --- .../restdocs/operation/OperationResponseFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index a86c6209..3b9c9914 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * 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. From d31e6d4344d6ce6235badd6f013300707ae60e9e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 4 Jan 2022 13:43:09 +0000 Subject: [PATCH 015/198] Work around problems with docs.jboss.org See gh-769 --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index dd11b52b..d4e86d41 100644 --- a/build.gradle +++ b/build.gradle @@ -29,8 +29,7 @@ ext { javadocLinks = [ "https://docs.oracle.com/javase/8/docs/api/", "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", - "https://docs.jboss.org/hibernate/stable/beanvalidation/api/", - "https://docs.jboss.org/hibernate/stable/validator/api/" + "https://jakarta.ee/specifications/bean-validation/3.0/apidocs/" ] as String[] } From cc5a381ed9308c3cde7dccb02c61d976cff3c2e9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 11 Jan 2022 12:08:00 +0000 Subject: [PATCH 016/198] Use Spring backend to generate HTML reference documentation Closes gh-681 Closes gh-773 --- build.gradle | 2 +- docs/build.gradle | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d4e86d41..4e7302ad 100644 --- a/build.gradle +++ b/build.gradle @@ -176,7 +176,7 @@ task docsZip(type: Zip, dependsOn: [":docs:asciidoctor", ":api", ":restNotesSpri destinationDirectory = file("${project.buildDir}/distributions") from(project.tasks.findByPath(":docs:asciidoctor")) { - into "reference" + into "reference/htmlsingle" } from(api) { diff --git a/docs/build.gradle b/docs/build.gradle index e54dd339..9e623efb 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -13,7 +13,7 @@ configurations { } dependencies { - asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.5.0") + asciidoctorExt("io.spring.asciidoctor.backends:spring-asciidoctor-backends:0.0.2") internal(platform(project(":spring-restdocs-platform"))) internal(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) @@ -48,6 +48,9 @@ asciidoctor { attributes "revnumber": project.version, "branch-or-tag": project.version.endsWith("SNAPSHOT") ? "main": "v${project.version}" inputs.files(sourceSets.test.java) + outputOptions { + backends "spring-html" + } } task api (type: Javadoc) { From 57d2e88e7a6fea9ec1fcb6b1c9f2abc0cab56f61 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 11 Jan 2022 12:39:43 +0000 Subject: [PATCH 017/198] Remove stray reference to REST Assured See gh-761 --- docs/src/docs/asciidoc/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 16f9c9dc..153ef44c 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -18,7 +18,7 @@ Andy Wilkinson; Jay Bryant [[abstract]] -Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test, WebTestClient, or REST Assured. +Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient. include::introduction.adoc[] include::getting-started.adoc[] From b2bc4aa1ac1ef58e9381e84424a5aed722f56e37 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 12 Jan 2022 13:36:34 +0000 Subject: [PATCH 018/198] Upgrade to Jackson 2.13.1 Closes gh-774 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- spring-restdocs-platform/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 316873e2..41882377 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -22,7 +22,7 @@ ext { ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { - implementation "com.fasterxml.jackson.core:jackson-databind:2.12.5" + implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index bde81c56..a0fddd78 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -19,7 +19,7 @@ com.fasterxml.jackson.core jackson-databind - 2.12.5 + 2.13.1 jakarta.servlet diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index e9bc4e19..b69c8bc4 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -27,7 +27,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation "com.fasterxml.jackson.core:jackson-databind:2.12.5" + implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 7b2c2263..4724b818 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -8,7 +8,6 @@ javaPlatform { dependencies { constraints { - api("com.fasterxml.jackson.core:jackson-databind:2.9.5") api("com.samskivert:jmustache:$jmustacheVersion") api("jakarta.servlet:jakarta.servlet-api:5.0.0") api("jakarta.validation:jakarta.validation-api:3.0.0") @@ -26,5 +25,6 @@ dependencies { api("org.junit.jupiter:junit-jupiter-api:5.0.0") api("org.mockito:mockito-core:1.10.19") } + api(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.13.1")) api(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) } From 25f79712462c58404fd7b827c0b7227436d05274 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 13 Jan 2022 12:44:21 +0000 Subject: [PATCH 019/198] Resolve vulnerability alerts in Data REST sample --- samples/rest-notes-spring-data-rest/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index a0fddd78..8bb5ed4f 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -55,7 +55,7 @@ com.h2database h2 - 1.4.200 + 2.0.206 runtime @@ -74,7 +74,7 @@ junit junit - 4.12 + 4.13.1 test From 740feb08110019b39ba1a9fabf8448d25c6db107 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 13 Jan 2022 13:01:52 +0000 Subject: [PATCH 020/198] Upgrade to Spring Framework 6.0.0-M2 Closes gh-775 --- build.gradle | 3 ++- samples/junit5/build.gradle | 3 ++- samples/rest-notes-slate/build.gradle | 3 ++- samples/rest-notes-spring-data-rest/pom.xml | 13 ++++++++++++- samples/rest-notes-spring-hateoas/build.gradle | 3 ++- samples/testng/build.gradle | 3 ++- samples/web-test-client/build.gradle | 6 +++--- 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 4e7302ad..f5b97c91 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ allprojects { repositories { mavenLocal() mavenCentral() + maven { url "https://repo.spring.io/milestone" } maven { url "https://repo.spring.io/snapshot" } } } @@ -25,7 +26,7 @@ nohttp { } ext { - springFrameworkVersion = "6.0.0-SNAPSHOT" + springFrameworkVersion = "6.0.0-M2" javadocLinks = [ "https://docs.oracle.com/javase/8/docs/api/", "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index 78c4d4db..c9fd2aef 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -28,7 +28,8 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation 'org.springframework:spring-webmvc:6.0.0-M1' + implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation 'org.springframework:spring-webmvc' testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 41882377..84cba536 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -22,11 +22,12 @@ ext { ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { + implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" - implementation "org.springframework:spring-webmvc:6.0.0-SNAPSHOT" + implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa:3.0.0-SNAPSHOT" implementation "org.springframework.data:spring-data-rest-webmvc:4.0.0-SNAPSHOT" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 8bb5ed4f..81d487df 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -15,6 +15,18 @@ 3.0.0-SNAPSHOT + + + + org.springframework + spring-framework-bom + 6.0.0-M2 + import + pom + + + + com.fasterxml.jackson.core @@ -39,7 +51,6 @@ org.springframework spring-webmvc - 6.0.0-SNAPSHOT org.springframework.data diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index b69c8bc4..725b04ba 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -27,11 +27,12 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" + implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" - implementation "org.springframework:spring-webmvc:6.0.0-SNAPSHOT" + implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa:3.0.0-SNAPSHOT" implementation "org.springframework.hateoas:spring-hateoas:2.0.0-SNAPSHOT" diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 2b4219c5..c0509fe6 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -28,7 +28,8 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation "org.springframework:spring-webmvc:6.0.0-M1" + implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation "org.springframework:spring-webmvc" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" testImplementation "org.testng:testng:6.9.10" diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index d0025c19..94205f01 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -29,11 +29,11 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation 'org.springframework:spring-context:6.0.0-M1' - implementation 'org.springframework:spring-webflux:6.0.0-M1' + implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation 'org.springframework:spring-context' + implementation 'org.springframework:spring-webflux' testImplementation 'junit:junit:4.13.1' - testImplementation 'org.springframework:spring-test:6.0.0-M1' testImplementation "org.springframework.restdocs:spring-restdocs-webtestclient:$restdocsVersion" } From 4e2e409b800becfd3981f30238083bfa5fe9dbf8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 13 Jan 2022 14:36:34 +0000 Subject: [PATCH 021/198] Correct property name following forward merge --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 737fa0f3..0f2a7a41 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ ext { springFrameworkVersion = "6.0.0-M2" javadocLinks = [ "https://docs.oracle.com/javase/8/docs/api/", - "https://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", + "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/" ] as String[] From 62f313152656185b3a9da08bfaf38fc6789bf04f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 13 Jan 2022 14:48:24 +0000 Subject: [PATCH 022/198] Update javadoc to link to Java 17's javadoc See gh-749 --- build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 0f2a7a41..084a8c3c 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,6 @@ nohttp { ext { springFrameworkVersion = "6.0.0-M2" javadocLinks = [ - "https://docs.oracle.com/javase/8/docs/api/", "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/" @@ -94,7 +93,7 @@ subprojects { subproject -> options.links = javadocLinks options.addStringOption "-quiet" options.encoding = "UTF-8" - options.source = "1.8" + options.source = "17" } java { @@ -162,7 +161,7 @@ task api (type: Javadoc) { encoding = "UTF-8" memberLevel = "protected" outputLevel = "quiet" - source = "1.8" + source = "17" splitIndex = true use = true windowTitle = "Spring REST Docs ${project.version} API" From 6892c962d2819b43a435465c641a5b0b9de99db9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 13 Jan 2022 18:01:49 +0000 Subject: [PATCH 023/198] Use bom to manage versions of samples' Spring Data dependencies See gh-777 --- samples/rest-notes-slate/build.gradle | 5 +++-- samples/rest-notes-spring-data-rest/pom.xml | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 84cba536..51b03299 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -23,13 +23,14 @@ ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" - implementation "org.springframework.data:spring-data-jpa:3.0.0-SNAPSHOT" - implementation "org.springframework.data:spring-data-rest-webmvc:4.0.0-SNAPSHOT" + implementation "org.springframework.data:spring-data-jpa" + implementation "org.springframework.data:spring-data-rest-webmvc" runtimeOnly 'com.h2database:h2:2.0.206' runtimeOnly 'org.atteo:evo-inflector:1.2.1' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 81d487df..9d1c5b30 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -24,6 +24,13 @@ import pom + + org.springframework.data + spring-data-bom + 2022.0.0-SNAPSHOT + import + pom + @@ -55,12 +62,10 @@ org.springframework.data spring-data-jpa - 3.0.0-SNAPSHOT org.springframework.data spring-data-rest-webmvc - 4.0.0-SNAPSHOT From 4cf5892015c79f4f6d056fc0e09982898592a648 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 17 Jan 2022 13:24:58 +0000 Subject: [PATCH 024/198] Remove deprecated code Closes gh-781 --- .../operation/OperationResponseFactory.java | 16 ------------- .../restdocs/payload/FieldTypeResolver.java | 17 +------------ .../request/AbstractParametersSnippet.java | 14 +---------- .../payload/FieldTypeResolverTests.java | 23 +----------------- .../RestDocumentationRequestBuilders.java | 24 ------------------- ...RestDocumentationRequestBuildersTests.java | 12 +++++----- 6 files changed, 9 insertions(+), 97 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index 3b9c9914..17f835a6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -17,7 +17,6 @@ package org.springframework.restdocs.operation; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; /** * A factory for creating {@link OperationResponse OperationResponses}. @@ -26,21 +25,6 @@ */ public class OperationResponseFactory { - /** - * Creates a new {@link OperationResponse}. If the response has any content, the given - * {@code headers} will be augmented to ensure that they include a - * {@code Content-Length} header. - * @param status the status of the response - * @param headers the response's headers - * @param content the content of the response - * @return the {@code OperationResponse} - * @deprecated since 2.0.4 in favor of {@link #create(int, HttpHeaders, byte[])} - */ - @Deprecated - public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] content) { - return this.create(status.value(), headers, content); - } - /** * Creates a new {@link OperationResponse}. If the response has any content, the given * {@code headers} will be augmented to ensure that they include a diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java index 6c8397c3..7f574a9e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.restdocs.payload; -import java.util.Collections; import java.util.List; import org.springframework.http.MediaType; @@ -30,20 +29,6 @@ */ public interface FieldTypeResolver { - /** - * Create a {@code FieldTypeResolver} for the given {@code content} and - * {@code contentType}. - * @param content the payload that the {@code FieldTypeResolver} should handle - * @param contentType the content type of the payload - * @return the {@code FieldTypeResolver} - * @deprecated since 2.0.4 in favor of - * {@link #forContentWithDescriptors(byte[], MediaType, List)} - */ - @Deprecated - static FieldTypeResolver forContent(byte[] content, MediaType contentType) { - return forContentWithDescriptors(content, contentType, Collections.emptyList()); - } - /** * Create a {@code FieldTypeResolver} for the given {@code content} and * {@code contentType}, described by the given {@code descriptors}. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 523d65c8..cce7a7d1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * 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. @@ -126,18 +126,6 @@ private void verifyParameterDescriptors(Operation operation) { */ protected abstract void verificationFailed(Set undocumentedParameters, Set missingParameters); - /** - * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will - * be used to generate the documentation key by their - * {@link ParameterDescriptor#getName()}. - * @return the map of path descriptors - * @deprecated since 1.1.0 in favor of {@link #getParameterDescriptors()} - */ - @Deprecated - protected final Map getFieldDescriptors() { - return this.descriptorsByName; - } - /** * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will * be used to generate the documentation key by their 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 17d3edf5..8ed7fbe1 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-2019 the original author or authors. + * 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. @@ -36,27 +36,6 @@ public class FieldTypeResolverTests { @Rule public ExpectedException thrownException = ExpectedException.none(); - @Test - @Deprecated - public void whenForContentCalledWithJsonContentThenReturnsJsonFieldTypeResolver() { - assertThat(FieldTypeResolver.forContent("{\"field\": \"value\"}".getBytes(), MediaType.APPLICATION_JSON)) - .isInstanceOf(JsonContentHandler.class); - } - - @Test - @Deprecated - public void whenForContentCalledWithXmlContentThenReturnsXmlContentHandler() { - assertThat(FieldTypeResolver.forContent("
    5".getBytes(), MediaType.APPLICATION_XML)) - .isInstanceOf(XmlContentHandler.class); - } - - @Test - @Deprecated - public void whenForContentIsCalledWithInvalidContentThenExceptionIsThrown() { - this.thrownException.expect(PayloadHandlingException.class); - FieldTypeResolver.forContent("some".getBytes(), MediaType.APPLICATION_XML); - } - @Test public void whenForContentWithDescriptorsCalledWithJsonContentThenReturnsJsonFieldTypeResolver() { assertThat(FieldTypeResolver.forContentWithDescriptors("{\"field\": \"value\"}".getBytes(), diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index 9c305139..6dc03adb 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -215,30 +215,6 @@ public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI u return MockMvcRequestBuilders.request(httpMethod, uri); } - /** - * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. - * The url template will be captured and made available for documentation. - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - * @return the builder for the file upload request - * @deprecated since 2.0.6 in favor of {@link #multipart(String, Object...)} - */ - @Deprecated - public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables) { - return multipart(urlTemplate, urlVariables); - } - - /** - * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. - * @param uri the URL - * @return the builder for the file upload request - * @deprecated since 2.0.6 in favor of {@link #multipart(URI)} - */ - @Deprecated - public static MockMultipartHttpServletRequestBuilder fileUpload(URI uri) { - return multipart(uri); - } - /** * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. * The URL template will be captured and made available for documentation. 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 83203f65..d4498f3e 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-2019 the original author or authors. + * 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. @@ -30,9 +30,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.head; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.options; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -130,13 +130,13 @@ public void requestUri() { } @Test - public void fileUploadTemplate() { - assertTemplate(fileUpload("/{template}", "t"), HttpMethod.POST); + public void multipartTemplate() { + assertTemplate(multipart("/{template}", "t"), HttpMethod.POST); } @Test - public void fileUploadUri() { - assertUri(fileUpload(URI.create("/uri")), HttpMethod.POST); + public void multipartUri() { + assertUri(multipart(URI.create("/uri")), HttpMethod.POST); } private void assertTemplate(MockHttpServletRequestBuilder builder, HttpMethod httpMethod) { From e1dad3f60d1a230ac03de750217ae9338b305218 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 17 Jan 2022 15:32:25 +0000 Subject: [PATCH 025/198] Upgrade samples to use latest milestones of dependencies --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 51b03299..d1331806 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -23,7 +23,7 @@ ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M1") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 9d1c5b30..73d24fdc 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -27,7 +27,7 @@ org.springframework.data spring-data-bom - 2022.0.0-SNAPSHOT + 2022.0.0-M1 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 725b04ba..4158fee1 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,13 +28,14 @@ dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M1") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" - implementation "org.springframework.data:spring-data-jpa:3.0.0-SNAPSHOT" - implementation "org.springframework.hateoas:spring-hateoas:2.0.0-SNAPSHOT" + implementation "org.springframework.data:spring-data-jpa" + implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M1" runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'org.atteo:evo-inflector:1.2.1' From 7aae57d400e5676ed1489e3ecf57c3680eaaf6de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 17 Jan 2022 17:34:33 +0000 Subject: [PATCH 026/198] Make registry mirror vars available to milestone and RC releases --- ci/pipeline.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index df5434a3..4529a1d8 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -365,6 +365,8 @@ jobs: 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 @@ -394,6 +396,8 @@ jobs: 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 From d38d79755b62cfd9d2c4cd2c953bed43396766b2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Jan 2022 15:33:21 +0000 Subject: [PATCH 027/198] Restore support for REST Assured Closes gh-784 --- build.gradle | 5 + docs/build.gradle | 1 + docs/src/docs/asciidoc/configuration.adoc | 38 +- .../customizing-requests-and-responses.adoc | 24 +- .../docs/asciidoc/documenting-your-api.adoc | 180 +++++++- docs/src/docs/asciidoc/getting-started.adoc | 62 ++- docs/src/docs/asciidoc/introduction.adoc | 2 +- .../CustomDefaultOperationPreprocessors.java | 49 +++ .../restassured/CustomDefaultSnippets.java | 46 ++ .../example/restassured/CustomEncoding.java | 45 ++ .../com/example/restassured/CustomFormat.java | 45 ++ .../restassured/EveryTestPreprocessing.java | 61 +++ .../ExampleApplicationJUnit5Tests.java | 43 ++ .../ExampleApplicationTestNgTests.java | 53 +++ .../restassured/ExampleApplicationTests.java | 44 ++ .../com/example/restassured/HttpHeaders.java | 48 ++ .../com/example/restassured/Hypermedia.java | 51 +++ .../example/restassured/InvokeService.java | 39 ++ .../restassured/ParameterizedOutput.java | 45 ++ .../example/restassured/PathParameters.java | 41 ++ .../java/com/example/restassured/Payload.java | 115 +++++ .../restassured/PerTestPreprocessing.java | 41 ++ .../restassured/RequestParameters.java | 52 +++ .../restassured/RequestPartPayload.java | 59 +++ .../com/example/restassured/RequestParts.java | 41 ++ .../restassured/RestAssuredSnippetReuse.java | 40 ++ samples/rest-assured/build.gradle | 63 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + samples/rest-assured/gradlew | 185 ++++++++ samples/rest-assured/gradlew.bat | 89 ++++ samples/rest-assured/settings.gradle | 0 .../rest-assured/src/docs/asciidoc/index.adoc | 26 ++ .../SampleRestAssuredApplication.java | 71 +++ .../SampleRestAssuredApplicationTests.java | 77 ++++ settings.gradle | 1 + spring-restdocs-platform/build.gradle | 1 + spring-restdocs-restassured/build.gradle | 24 + ...suredOperationPreprocessorsConfigurer.java | 48 ++ .../RestAssuredRequestConverter.java | 167 +++++++ .../RestAssuredResponseConverter.java | 53 +++ .../RestAssuredRestDocumentation.java | 111 +++++ ...estAssuredRestDocumentationConfigurer.java | 75 ++++ .../RestAssuredSnippetConfigurer.java | 47 ++ .../restassured/RestDocumentationFilter.java | 108 +++++ .../restdocs/restassured/package-info.java | 20 + .../RestAssuredRequestConverterTests.java | 295 +++++++++++++ .../RestAssuredResponseConverterTests.java | 52 +++ ...suredRestDocumentationConfigurerTests.java | 89 ++++ ...uredRestDocumentationIntegrationTests.java | 410 ++++++++++++++++++ .../restdocs/restassured/TomcatServer.java | 125 ++++++ .../src/test/resources/body.txt | 1 + .../restdocs/templates/curl-request.snippet | 1 + 53 files changed, 3403 insertions(+), 11 deletions(-) create mode 100644 docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java create mode 100644 docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java create mode 100644 docs/src/test/java/com/example/restassured/CustomEncoding.java create mode 100644 docs/src/test/java/com/example/restassured/CustomFormat.java create mode 100644 docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTests.java create mode 100644 docs/src/test/java/com/example/restassured/HttpHeaders.java create mode 100644 docs/src/test/java/com/example/restassured/Hypermedia.java create mode 100644 docs/src/test/java/com/example/restassured/InvokeService.java create mode 100644 docs/src/test/java/com/example/restassured/ParameterizedOutput.java create mode 100644 docs/src/test/java/com/example/restassured/PathParameters.java create mode 100644 docs/src/test/java/com/example/restassured/Payload.java create mode 100644 docs/src/test/java/com/example/restassured/PerTestPreprocessing.java create mode 100644 docs/src/test/java/com/example/restassured/RequestParameters.java create mode 100644 docs/src/test/java/com/example/restassured/RequestPartPayload.java create mode 100644 docs/src/test/java/com/example/restassured/RequestParts.java create mode 100644 docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java create mode 100644 samples/rest-assured/build.gradle create mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/rest-assured/gradlew create mode 100644 samples/rest-assured/gradlew.bat create mode 100644 samples/rest-assured/settings.gradle create mode 100644 samples/rest-assured/src/docs/asciidoc/index.adoc create mode 100644 samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java create mode 100644 samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java create mode 100644 spring-restdocs-restassured/build.gradle create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java create mode 100644 spring-restdocs-restassured/src/test/resources/body.txt create mode 100644 spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet diff --git a/build.gradle b/build.gradle index fd0b7a58..b2b2da8d 100644 --- a/build.gradle +++ b/build.gradle @@ -117,6 +117,7 @@ subprojects { subproject -> samples { dependOn "spring-restdocs-core:publishToMavenLocal" dependOn "spring-restdocs-mockmvc:publishToMavenLocal" + dependOn "spring-restdocs-restassured:publishToMavenLocal" dependOn "spring-restdocs-webtestclient:publishToMavenLocal" dependOn "spring-restdocs-asciidoctor:publishToMavenLocal" @@ -132,6 +133,10 @@ samples { workingDir "$projectDir/samples/testng" } + restAssured { + workingDir "$projectDir/samples/rest-assured" + } + webTestClient { workingDir "$projectDir/samples/web-test-client" } diff --git a/docs/build.gradle b/docs/build.gradle index bda2710e..ce49eb9a 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -19,6 +19,7 @@ dependencies { internal(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) testImplementation(project(":spring-restdocs-mockmvc")) + testImplementation(project(":spring-restdocs-restassured")) testImplementation(project(":spring-restdocs-webtestclient")) testImplementation("jakarta.validation:jakarta.validation-api") testImplementation("junit:junit") diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index d4cebd66..ee8e4adf 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -45,6 +45,17 @@ TIP: To configure a request's context path, use the `contextPath` method on `Moc +[[configuration-uris-rest-assured]] +==== REST Assured URI Customization + +REST Assured tests a service by making actual HTTP requests. As a result, URIs must be +customized once the operation on the service has been performed but before it is +documented. A +<> is provided for this purpose. + + + [[configuration-uris-webtestclient]] ==== WebTestClient URI Customization @@ -79,6 +90,12 @@ include::{examples-dir}/com/example/mockmvc/CustomEncoding.java[tags=custom-enco include::{examples-dir}/com/example/webtestclient/CustomEncoding.java[tags=custom-encoding] ---- +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom-encoding] +---- + TIP: When Spring REST Docs converts the content of a request or a response to a `String`, the `charset` specified in the `Content-Type` header is used if it is available. In its absence, the JVM's default `Charset` is used. You can configure the JVM's default `Charset` by using the `file.encoding` system property. @@ -105,6 +122,13 @@ include::{examples-dir}/com/example/mockmvc/CustomFormat.java[tags=custom-format include::{examples-dir}/com/example/webtestclient/CustomFormat.java[tags=custom-format] ---- +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-format] +---- +==== + [[configuration-default-snippets]] @@ -134,6 +158,12 @@ include::{examples-dir}/com/example/mockmvc/CustomDefaultSnippets.java[tags=cust include::{examples-dir}/com/example/webtestclient/CustomDefaultSnippets.java[tags=custom-default-snippets] ---- +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomDefaultSnippets.java[tags=custom-default-snippets] +---- + [[configuration-default-preprocessors]] @@ -158,4 +188,10 @@ include::{examples-dir}/com/example/webtestclient/CustomDefaultOperationPreproce <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. - +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomDefaultOperationPreprocessors.java[tags=custom-default-operation-preprocessors] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 4d340e64..ec8f2932 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -24,6 +24,14 @@ include::{examples-dir}/com/example/webtestclient/PerTestPreprocessing.java[tags <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. +.REST Assured +---- +include::{examples-dir}/com/example/restassured/PerTestPreprocessing.java[tags=preprocessing] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. +==== + Alternatively, you may want to apply the same preprocessors to every test. You can do so by using the `RestDocumentationConfigurer` API in your `@Before` method to configure the preprocessors. For example, to remove the `Foo` header from all requests and pretty print all responses, you could do one of the following (depending on your testing environment): @@ -44,6 +52,14 @@ include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[ta <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=setup] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. + Then, in each test, you can perform any configuration specific to that test. The following examples show how to do so: @@ -59,6 +75,12 @@ include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[tags=use] ---- +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] +---- + Various built-in preprocessors, including those illustrated above, are available through the static methods on `Preprocessors`. See <> for further details. @@ -116,7 +138,7 @@ You can use `modifyParameters` on `Preprocessors` to add, set, and remove reques TIP: If you use MockMvc or a WebTestClient that is not bound to a server, you should customize URIs by <>. You can use `modifyUris` on `Preprocessors` to modify any URIs in a request or a response. -When using WebTestClient bound to a server, this lets you customize the URIs that appear in the documentation while testing a local instance of the service. +When using REST Assured or WebTestClient bound to a server, this lets you customize the URIs that appear in the documentation while testing a local instance of the service. diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 75b93185..5fee0d69 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -33,6 +33,18 @@ Uses the static `links` method on `org.springframework.restdocs.hypermedia.Hyper Uses the static `linkWithRel` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <3> Expect a link whose `rel` is `bravo`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] +---- +<1> Configure Spring REST docs to produce a snippet describing the response's links. + Uses the static `links` method on + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<2> Expect a link whose `rel` is `alpha`. Uses the static `linkWithRel` method on + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<3> Expect a link whose `rel` is `bravo`. + The result is a snippet named `links.adoc` that contains a table describing the resource's links. TIP: If a link in the response has a `title`, you can omit the description from its descriptor and the `title` is used. @@ -79,6 +91,14 @@ include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=explicit-e <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=explicit-extractor] +---- +<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. + If your API represents its links in a format other than Atom or HAL, you can provide your own implementation of the `LinkExtractor` interface to extract the links from the response. @@ -150,6 +170,18 @@ Both are static methods on `org.springframework.restdocs.payload.PayloadDocument Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. <3> Expect a field with the path `contact.name`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=response] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. +To document a request, you can use `requestFields`. +Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Expect a field with the path `contact.email`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.name`. + The result is a snippet that contains a table describing the fields. For requests, this snippet is named `request-fields.adoc`. For responses, this snippet is named `response-fields.adoc`. @@ -178,6 +210,14 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=subsection] `contact.email` and `contact.name` are now seen as having also been documented. Uses the static `subsectionWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` are now seen as having also been documented. +Uses the static `subsectionWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. + `subsectionWithPath` can be useful for providing a high-level overview of a particular section of a payload. You can then produce separate, more detailed documentation for a subsection. See <>. @@ -357,6 +397,13 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=explicit-typ ---- <1> Set the field's type to `String`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] +---- +<1> Set the field's type to `String`. + [[documenting-your-api-request-response-payloads-fields-xml]] @@ -440,6 +487,13 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=single-book] ---- <1> Document `title` and `author` by using existing descriptors +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` by using existing descriptors + You can also use the descriptors to document an array of books, as follows: [source,java,indent=0,role="primary"] @@ -460,6 +514,16 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=book-array] +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] +---- +<1> Document the array. +<2> Document `[].title` and `[].author` by using the existing descriptors prefixed with `[].` + + + [[documenting-your-api-request-response-payloads-subsections]] ==== Documenting a Subsection of a Request or Response Payload @@ -509,6 +573,15 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=body-subsect Uses the static `responseBody` and `beneathPath` methods on `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet for the request body, you can use `requestBody` in place of `responseBody`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. +Uses the static `responseBody` and `beneathPath` methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +To produce a snippet for the request body, you can use `requestBody` in place of `responseBody`. + The result is a snippet with the following contents: [source,json,indent=0] @@ -559,6 +632,16 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=fields-subse Uses the static `beneathPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. <2> Document the `high` and `low` fields. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=fields-subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + The result is a snippet that contains a table describing the `high` and `low` fields of `weather.temperature`. To make the snippet's name distinct, an identifier for the subsection is included. By default, this identifier is `beneath-${path}`. @@ -597,6 +680,18 @@ Uses the static `requestParameters` method on `org.springframework.restdocs.requ Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-query-string] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's parameters. +Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the `page` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `per_page` parameter. +<4> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. + You can also include request parameters as form data in the body of a POST request. The following examples show how to do so: @@ -614,6 +709,14 @@ include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=re ---- <1> Perform a `POST` request with a single parameter, `username`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-form-data] +---- +<1> Configure the `username` parameter. +<2> Perform the `POST` request. + In all cases, the result is a snippet named `request-parameters.adoc` that contains a table describing the parameters that are supported by the resource. When documenting request parameters, the test fails if an undocumented request parameter is used in the request. @@ -658,6 +761,18 @@ Uses the static `pathParameters` method on `org.springframework.restdocs.request Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/PathParameters.java[tags=path-parameters] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's path parameters. +Uses the static `pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the parameter named `latitude`. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the parameter named `longitude`. +<4> Perform a `GET` request with two path parameters, `latitude` and `longitude`. + 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`. @@ -702,6 +817,17 @@ Uses the static `requestParts` method on `org.springframework.restdocs.request.R <3> Document the part named `file`. Uses the static `partWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParts.java[tags=request-parts] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's parts. +Uses the static `requestParts` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the part named `file`. Uses the static `partWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Configure the request with the part named `file`. +<4> Perform the `POST` request to `/upload`. + The result is a snippet named `request-parts.adoc` that contains a table describing the request parts that are supported by the resource. When documenting request parts, the test fails if an undocumented part is used in the request. @@ -744,6 +870,14 @@ include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=b <1> Configure Spring REST docs to produce a snippet containing the body of the request part named `metadata`. Uses the static `requestPartBody` method on `PayloadDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request part named `metadata`. +Uses the static `requestPartBody` method on `PayloadDocumentation`. + The result is a snippet named `request-part-${part-name}-body.adoc` that contains the part's body. For example, documenting a part named `metadata` produces a snippet named `request-part-metadata-body.adoc`. @@ -774,6 +908,16 @@ Uses the static `requestPartFields` method on `PayloadDocumentation`. <2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=fields] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. +Uses the static `requestPartFields` method on `PayloadDocumentation`. +<2> Expect a field with the path `version`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. + The result is a snippet that contains a table describing the part's fields. This snippet is named `request-part-${part-name}-fields.adoc`. For example, documenting a part named `metadata` produces a snippet named `request-part-metadata-fields.adoc`. @@ -825,6 +969,19 @@ Uses the static `headerWithName` method on `org.springframework.restdocs.headers <4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/HttpHeaders.java[tags=headers] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's headers. +Uses the static `requestHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<2> Document the `Authorization` header. +Uses the static `headerWithName` method on `org.springframework.restdocs.headers.HeaderDocumentation. +<3> Produce a snippet describing the response's headers. +Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<4> Configure the request with an `Authorization` header that uses basic authentication. + The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. Each contains a table describing the headers. @@ -863,6 +1020,13 @@ include::{examples-dir}/com/example/webtestclient/WebTestClientSnippetReuse.java ---- <1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific to the resource that is being documented. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RestAssuredSnippetReuse.java[tags=use] +---- +<1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific to the resource that is being documented. + The result of the example is that links with `rel` values of `first`, `last`, `next`, `previous`, `alpha`, and `bravo` are all documented. @@ -1045,6 +1209,12 @@ The following examples show how to do so: include::{examples-dir}/com/example/mockmvc/ParameterizedOutput.java[tags=parameterized-output] ---- +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ParameterizedOutput.java[tags=parameterized-output] +---- + [source,java,indent=0,role="secondary"] .WebTestClient ---- @@ -1112,8 +1282,16 @@ include::{examples-dir}/com/example/webtestclient/Payload.java[tags=constraints] <2> Set the `constraints` attribute for the `name` field. <3> Set the `constraints` attribute for the `email` field. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=constraints] +---- +<1> Configure the `title` attribute for the request fields snippet. +<2> Set the `constraints` attribute for the `name` field. +<3> Set the `constraints` attribute for the `email` field. + The second step is to provide a custom template named `request-fields.snippet` that includes the information about the fields' constraints in the generated snippet's table and adds a title. -The following example shows how to do so: [source,indent=0] ---- diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 9ed27e94..ecd86d56 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -34,6 +34,19 @@ If you want to jump straight in, a number of sample applications are available: | Demonstrates the use of Spring REST docs with Spring WebFlux's WebTestClient. |=== + +[cols="3,2,10"] +.REST Assured +|=== +| Sample | Build system | Description + +| {samples}/rest-assured[REST Assured] +| Gradle +| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. + +|=== + + [cols="3,2,10"] .Advanced |=== @@ -63,7 +76,7 @@ Spring REST Docs has the following minimum requirements: * Java 17 * Spring Framework 6 - +Additionally, the `spring-restdocs-restassured` module requires REST Assured 4 (4.5 or later). [[getting-started-build-configuration]] === Build configuration @@ -113,7 +126,7 @@ The key parts of the configuration are described in the following listings: ---- <1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. -If you want to use `WebTestClient` add a dependency on `spring-restdocs-webtestclient` instead. +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. <2> Add the Asciidoctor plugin. <3> Using `prepare-package` allows the documentation to be <>. <4> Add `spring-restdocs-asciidoctor` as a dependency of the Asciidoctor plugin. @@ -156,7 +169,7 @@ It will also allow you to use the `operation` block macro. 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. <4> Add a dependency on `spring-restdocs-mockmvc` in the `testImplementation` configuration. -If you want to use `WebTestClient`, add a dependency on `spring-restdocs-webtestclient` instead. +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 property to define the output location for generated snippets. <6> Configure the `test` task to add the snippets directory as an output. <7> Configure the `asciidoctor` task. @@ -232,7 +245,7 @@ The following listings show how to do so in both Maven and Gradle: [[getting-started-documentation-snippets]] === Generating Documentation Snippets -Spring REST Docs uses Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework] or Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] to make requests to the service that you are documenting. +Spring REST Docs uses Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework] or Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`], or https://rest-assured.io[REST Assured] to make requests to the service that you are documenting. It then produces documentation snippets for the request and the resulting response. @@ -280,7 +293,7 @@ The following example shows how to do so: public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom"); ---- -Next, you must provide an `@Before` method to configure MockMvc or WebTestClient. +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"] @@ -299,6 +312,14 @@ include::{examples-dir}/com/example/webtestclient/ExampleApplicationTests.java[t <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/ExampleApplicationTests.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. @@ -350,7 +371,7 @@ public class JUnit5ExampleTests { } ---- -Next, you must provide a `@BeforeEach` method to configure MockMvc or WebTestClient. +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"] @@ -369,6 +390,14 @@ include::{examples-dir}/com/example/webtestclient/ExampleApplicationJUnit5Tests. <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. @@ -391,7 +420,7 @@ private ManualRestDocumentation restDocumentation = new ManualRestDocumentation( ---- Secondly, you must call `ManualRestDocumentation.beforeTest(Class, String)` before each test. -You can do so as part of the method that configures MockMvc or WebTestClient. +You can do so as part of the method that configures MockMvc, WebTestClient, or REST Assured. The following examples show how to do so: [source,java,indent=0,role="primary"] @@ -406,6 +435,12 @@ include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[t include::{examples-dir}/com/example/webtestclient/ExampleApplicationTestNgTests.java[tags=setup] ---- +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=setup] +---- + Finally, you must call `ManualRestDocumentation.afterTest` after each test. The following example shows how to do so with TestNG: @@ -444,6 +479,19 @@ include::{examples-dir}/com/example/webtestclient/InvokeService.java[tags=invoke The snippets are written by a `Consumer` of the `ExchangeResult`. You can obtain such a consumer from the static `document` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/InvokeService.java[tags=invoke-service] +---- +<1> Apply the specification that was initialized in the `@Before` method. +<2> Indicate that an `application/json` response is required. +<3> Document the call to the service, writing the snippets into a directory named `index` (which is located beneath the configured output directory). +The snippets are written by a `RestDocumentationFilter`. +You can obtain an instance of this class from the static `document` method on `RestAssuredRestDocumentation` in the `org.springframework.restdocs.restassured` package. +<4> Invoke the root (`/`) of the service. +<5> Assert that the service produce the expected response. + By default, six snippets are written: * `/index/curl-request.adoc` diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index eea2f55b..f064bb60 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -9,7 +9,7 @@ To this end, Spring REST Docs uses https://asciidoctor.org[Asciidoctor] by defau Asciidoctor processes plain text and produces HTML, styled and laid out to suit your needs. If you prefer, you can also configure Spring REST Docs to use Markdown. -Spring REST Docs uses snippets produced by tests written with Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework] or Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`]. +Spring REST Docs uses snippets produced by tests written with Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or http://rest-assured.io[REST Assured 4]. This test-driven approach helps to guarantee the accuracy of your service's documentation. If a snippet is incorrect, the test that produces it fails. diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java new file mode 100644 index 00000000..cb392c91 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java @@ -0,0 +1,49 @@ +/* + * 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 com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomDefaultOperationPreprocessors { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + @SuppressWarnings("unused") + private RequestSpecification spec; + + @Before + public void setup() { + // tag::custom-default-operation-preprocessors[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); + // end::custom-default-operation-preprocessors[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java new file mode 100644 index 00000000..3eeb870d --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -0,0 +1,46 @@ +/* + * 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 com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; + +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(); + + @SuppressWarnings("unused") + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-default-snippets[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.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 new file mode 100644 index 00000000..bcfd0caa --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -0,0 +1,45 @@ +/* + * 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 com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomEncoding { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + @SuppressWarnings("unused") + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-encoding[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.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 new file mode 100644 index 00000000..3b6f6482 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -0,0 +1,45 @@ +/* + * 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 com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.templates.TemplateFormats; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomFormat { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + @SuppressWarnings("unused") + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-format[] + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.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 new file mode 100644 index 00000000..6757da31 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -0,0 +1,61 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +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(); + + // tag::setup[] + private RequestSpecification spec; + + @Before + public void setup() { + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(removeHeaders("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); + } + // end::setup[] + + public void use() throws Exception { + // tag::use[] + RestAssured.given(this.spec) + .filter(document("index", links(linkWithRel("self").description("Canonical self link")))).when() + .get("/").then().assertThat().statusCode(is(200)); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java new file mode 100644 index 00000000..7fa9bf1f --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationJUnit5Tests.java @@ -0,0 +1,43 @@ +/* + * 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 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) +public class ExampleApplicationJUnit5Tests { + + @SuppressWarnings("unused") + // tag::setup[] + private RequestSpecification spec; + + @BeforeEach + public 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/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java new file mode 100644 index 00000000..d1e07b21 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -0,0 +1,53 @@ +/* + * 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 com.example.restassured; + +import java.lang.reflect.Method; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + +import org.springframework.restdocs.ManualRestDocumentation; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTestNgTests { + + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); + + @SuppressWarnings("unused") + // tag::setup[] + private RequestSpecification spec; + + @BeforeMethod + public void setUp(Method method) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)).build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + // end::setup[] + + // tag::teardown[] + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + // end::teardown[] + +} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java new file mode 100644 index 00000000..26c08c59 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -0,0 +1,44 @@ +/* + * 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 com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTests { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + @SuppressWarnings("unused") + // tag::setup[] + private RequestSpecification spec; + + @Before + public void setUp() { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) // <1> + .build(); + } + // end::setup[] + +} diff --git a/docs/src/test/java/com/example/restassured/HttpHeaders.java b/docs/src/test/java/com/example/restassured/HttpHeaders.java new file mode 100644 index 00000000..697acbaf --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpHeaders.java @@ -0,0 +1,48 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class HttpHeaders { + + private RequestSpecification spec; + + public void headers() throws Exception { + // tag::headers[] + RestAssured.given(this.spec).filter(document("headers", requestHeaders(// <1> + headerWithName("Authorization").description("Basic auth credentials")), // <2> + responseHeaders(// <3> + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))) + .header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // <4> + .when().get("/people").then().assertThat().statusCode(is(200)); + // end::headers[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/Hypermedia.java b/docs/src/test/java/com/example/restassured/Hypermedia.java new file mode 100644 index 00000000..346411b3 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/Hypermedia.java @@ -0,0 +1,51 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class Hypermedia { + + private RequestSpecification spec; + + public void defaultExtractor() throws Exception { + // tag::links[] + RestAssured.given(this.spec).accept("application/json").filter(document("index", links(// <1> + linkWithRel("alpha").description("Link to the alpha resource"), // <2> + linkWithRel("bravo").description("Link to the bravo resource")))) // <3> + .get("/").then().assertThat().statusCode(is(200)); + // end::links[] + } + + public void explicitExtractor() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::explicit-extractor[] + .filter(document("index", links(halLinks(), // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) + // end::explicit-extractor[] + .get("/").then().assertThat().statusCode(is(200)); + } + +} diff --git a/docs/src/test/java/com/example/restassured/InvokeService.java b/docs/src/test/java/com/example/restassured/InvokeService.java new file mode 100644 index 00000000..70b884bd --- /dev/null +++ b/docs/src/test/java/com/example/restassured/InvokeService.java @@ -0,0 +1,39 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class InvokeService { + + private RequestSpecification spec; + + public void invokeService() throws Exception { + // tag::invoke-service[] + RestAssured.given(this.spec) // <1> + .accept("application/json") // <2> + .filter(document("index")) // <3> + .when().get("/") // <4> + .then().assertThat().statusCode(is(200)); // <5> + // end::invoke-service[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java new file mode 100644 index 00000000..6859417b --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -0,0 +1,45 @@ +/* + * 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 com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.Before; +import org.junit.Rule; + +import org.springframework.restdocs.JUnitRestDocumentation; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +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)) + .addFilter(document("{method-name}/{step}")).build(); + } + // end::parameterized-output[] + +} diff --git a/docs/src/test/java/com/example/restassured/PathParameters.java b/docs/src/test/java/com/example/restassured/PathParameters.java new file mode 100644 index 00000000..c6ca7e44 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/PathParameters.java @@ -0,0 +1,41 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class PathParameters { + + private RequestSpecification spec; + + public void pathParametersSnippet() throws Exception { + // tag::path-parameters[] + RestAssured.given(this.spec).filter(document("locations", pathParameters(// <1> + parameterWithName("latitude").description("The location's latitude"), // <2> + parameterWithName("longitude").description("The location's longitude")))) // <3> + .when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <4> + .then().assertThat().statusCode(is(200)); + // end::path-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java new file mode 100644 index 00000000..4c7b9f28 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -0,0 +1,115 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +public class Payload { + + private RequestSpecification spec; + + public void response() throws Exception { + // tag::response[] + RestAssured.given(this.spec).accept("application/json").filter(document("user", responseFields(// <1> + fieldWithPath("contact.name").description("The user's name"), // <2> + fieldWithPath("contact.email").description("The user's email address")))) // <3> + .when().get("/user/5").then().assertThat().statusCode(is(200)); + // end::response[] + } + + public void subsection() throws Exception { + // tag::subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("user", + responseFields(subsectionWithPath("contact").description("The user's contact details")))) // <1> + .when().get("/user/5").then().assertThat().statusCode(is(200)); + // end::subsection[] + } + + public void explicitType() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::explicit-type[] + .filter(document("user", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> + .description("The user's email address")))) + // end::explicit-type[] + .when().get("/user/5").then().assertThat().statusCode(is(200)); + } + + public void constraints() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::constraints[] + .filter(document("create-user", + requestFields(attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints").value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints").value("Must be a valid email address"))))) // <3> + // end::constraints[] + .when().post("/users").then().assertThat().statusCode(is(200)); + } + + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + RestAssured.given(this.spec).accept("application/json").filter(document("book", responseFields(book))) // <1> + .when().get("/books/1").then().assertThat().statusCode(is(200)); + // end::single-book[] + + // tag::book-array[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("books", responseFields(fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))) // <2> + .when().get("/books").then().assertThat().statusCode(is(200)); + // end::book-array[] + } + + public void fieldsSubsection() throws Exception { + // tag::fields-subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))) + .when().get("/locations/1").then().assertThat().statusCode(is(200)); + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseBody(beneathPath("weather.temperature")))) // <1> + .when().get("/locations/1").then().assertThat().statusCode(is(200)); + // end::body-subsection[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java new file mode 100644 index 00000000..02974491 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java @@ -0,0 +1,41 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class PerTestPreprocessing { + + private RequestSpecification spec; + + public void general() throws Exception { + // tag::preprocessing[] + RestAssured.given(this.spec).filter(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))) // <2> + .when().get("/").then().assertThat().statusCode(is(200)); + // end::preprocessing[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/RequestParameters.java new file mode 100644 index 00000000..229c62be --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestParameters.java @@ -0,0 +1,52 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestParameters { + + private RequestSpecification spec; + + public void getQueryStringSnippet() throws Exception { + // tag::request-parameters-query-string[] + RestAssured.given(this.spec).filter(document("users", requestParameters(// <1> + parameterWithName("page").description("The page to retrieve"), // <2> + parameterWithName("per_page").description("Entries per page")))) // <3> + .when().get("/users?page=2&per_page=100") // <4> + .then().assertThat().statusCode(is(200)); + // end::request-parameters-query-string[] + } + + public void postFormDataSnippet() throws Exception { + // tag::request-parameters-form-data[] + RestAssured.given(this.spec) + .filter(document("create-user", + requestParameters(parameterWithName("username").description("The user's username")))) + .formParam("username", "Tester") // <1> + .when().post("/users") // <2> + .then().assertThat().statusCode(is(200)); + // end::request-parameters-form-data[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java new file mode 100644 index 00000000..f7eb05a6 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -0,0 +1,59 @@ +/* + * 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 com.example.restassured; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestPartPayload { + + private RequestSpecification spec; + + public void fields() throws Exception { + // tag::fields[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))) // <2> + .when().multiPart("image", new File("image.png"), "image/png").multiPart("metadata", metadata) + .post("images").then().assertThat().statusCode(is(200)); + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartBody("metadata"))) // <1> + .when().multiPart("image", new File("image.png"), "image/png").multiPart("metadata", metadata) + .post("images").then().assertThat().statusCode(is(200)); + // end::body[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java new file mode 100644 index 00000000..32b2e0e9 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestParts.java @@ -0,0 +1,41 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestParts { + + private RequestSpecification spec; + + public void upload() throws Exception { + // tag::request-parts[] + RestAssured.given(this.spec).filter(document("users", requestParts(// <1> + partWithName("file").description("The file to upload")))) // <2> + .multiPart("file", "example") // <3> + .when().post("/upload") // <4> + .then().statusCode(is(200)); + // end::request-parts[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java new file mode 100644 index 00000000..c2267c1c --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -0,0 +1,40 @@ +/* + * 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 com.example.restassured; + +import com.example.SnippetReuse; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RestAssuredSnippetReuse extends SnippetReuse { + + private RequestSpecification spec; + + public void documentation() throws Exception { + // tag::use[] + RestAssured.given(this.spec).accept("application/json").filter(document("example", this.pagingLinks.and(// <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))).get("/").then().assertThat() + .statusCode(is(200)); + // end::use[] + } + +} diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle new file mode 100644 index 00000000..059311b8 --- /dev/null +++ b/samples/rest-assured/build.gradle @@ -0,0 +1,63 @@ +plugins { + id "java" + id "org.asciidoctor.jvm.convert" version "3.3.2" +} + +repositories { + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } + mavenCentral() + mavenLocal() +} + +group = 'com.example' + +sourceCompatibility = 17 +targetCompatibility = 17 + +ext { + restdocsVersion = '3.0.0-SNAPSHOT' + snippetsDir = file('build/generated-snippets') +} + +configurations { + asciidoctorExtensions +} + +dependencies { + asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" + + implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation 'io.projectreactor.netty:reactor-netty-http:1.0.15' + implementation 'org.springframework:spring-context' + implementation 'org.springframework:spring-webflux' + + + testImplementation 'io.rest-assured:rest-assured:4.5.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' + testImplementation "org.springframework.restdocs:spring-restdocs-restassured:$restdocsVersion" + testImplementation 'org.springframework:spring-test' + testImplementation('org.junit.vintage:junit-vintage-engine') { + exclude group: 'org.hamcrest', module: 'hamcrest-core' + } + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.0' +} + +test { + outputs.dir snippetsDir + useJUnitPlatform() +} + +asciidoctor { + configurations "asciidoctorExtensions" + inputs.dir snippetsDir + dependsOn test +} + +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

    K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ffed3a25 --- /dev/null +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/samples/rest-assured/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/samples/rest-assured/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "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. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +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. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/rest-assured/settings.gradle b/samples/rest-assured/settings.gradle new file mode 100644 index 00000000..e69de29b diff --git a/samples/rest-assured/src/docs/asciidoc/index.adoc b/samples/rest-assured/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..f89aeb6e --- /dev/null +++ b/samples/rest-assured/src/docs/asciidoc/index.adoc @@ -0,0 +1,26 @@ += Spring REST Docs REST Assured Sample +Andy Wilkinson; +:doctype: book +:icons: font +:source-highlighter: highlightjs + +Sample application demonstrating how to use Spring REST Docs with REST Assured. + +`SampleRestAssuredApplicationTests` makes a call to a very simple service. The service +that is being tested is running on a random port on `localhost`. The tests make use of a +preprocessor to modify the request so that it appears to have been sent to +`https://api.example.com`. If your service includes URIs in its responses, for example +because it uses hypermedia, similar preprocessing can be applied to the response before +it is documented. + +Three snippets are produced. One showing how to make a request using cURL: + +include::{snippets}/sample/curl-request.adoc[] + +One showing the HTTP request: + +include::{snippets}/sample/http-request.adoc[] + +And one showing the HTTP response: + +include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java new file mode 100644 index 00000000..af82e0bb --- /dev/null +++ b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java @@ -0,0 +1,71 @@ +/* + * 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 com.example.restassured; + +import java.net.InetSocketAddress; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; + +@EnableWebFlux +@Configuration +class SampleRestAssuredApplication { + + @Bean + RouterFunction routerFunction() { + return RouterFunctions.route(RequestPredicates.GET("/"), + (request) -> ServerResponse.status(HttpStatus.OK).bodyValue("Hello, World")); + } + + @Bean + WebServer webServer(RouterFunction routerFunction) { + return new WebServer(routerFunction); + } + + static class WebServer implements DisposableBean { + + private final DisposableServer httpServer; + + WebServer(RouterFunction routerFunction) { + ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(routerFunction)); + HttpServer httpServer = HttpServer.create().handle(adapter); + this.httpServer = httpServer.bindNow(); + } + + @Override + public void destroy() throws Exception { + this.httpServer.disposeNow(); + } + + int getPort() { + return ((InetSocketAddress)this.httpServer.address()).getPort(); + } + + } + +} diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java new file mode 100644 index 00000000..d5f62a0c --- /dev/null +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -0,0 +1,77 @@ +/* + * 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 com.example.restassured; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.example.restassured.SampleRestAssuredApplication.WebServer; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; + +@ContextConfiguration(classes = SampleRestAssuredApplication.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class SampleRestAssuredApplicationTests { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + private RequestSpecification documentationSpec; + + @Autowired + private WebServer webServer; + + private int port; + + @Before + public void setUp() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)).build(); + this.port = webServer.getPort(); + } + + @Test + public void sample() throws Exception { + given(this.documentationSpec) + .accept("text/plain") + .filter(document("sample", + preprocessRequest(modifyUris() + .scheme("https") + .host("api.example.com") + .removePort()))) + .when() + .port(this.port) + .get("/") + .then() + .assertThat().statusCode(is(200)); + } + +} diff --git a/settings.gradle b/settings.gradle index d64b16d2..b6f6cf84 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,4 +39,5 @@ include "spring-restdocs-asciidoctor" include "spring-restdocs-core" include "spring-restdocs-mockmvc" include "spring-restdocs-platform" +include "spring-restdocs-restassured" include "spring-restdocs-webtestclient" diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index c97a719a..7a75ef2f 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -12,6 +12,7 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api:5.0.0") api("jakarta.validation:jakarta.validation-api:3.0.0") api("junit:junit:4.12") + api("io.rest-assured:rest-assured:4.5.0") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.2") api("org.asciidoctor:asciidoctorj-pdf:1.6.0") diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle new file mode 100644 index 00000000..dbcd51ba --- /dev/null +++ b/spring-restdocs-restassured/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "java-library" + id "maven-publish" +} + +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" + } + implementation("org.springframework:spring-web") + + internal(platform(project(":spring-restdocs-platform"))) + + testImplementation(testFixtures(project(":spring-restdocs-core"))) + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("junit:junit") + testImplementation("org.apache.tomcat.embed:tomcat-embed-core:10.0.11") + testImplementation("org.assertj:assertj-core") + testImplementation("org.hamcrest:hamcrest-library") + testImplementation("org.mockito:mockito-core") +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java new file mode 100644 index 00000000..cef1e925 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java @@ -0,0 +1,48 @@ +/* + * 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.restassured; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; + +import org.springframework.restdocs.config.OperationPreprocessorsConfigurer; + +/** + * A configurer that can be used to configure the operation preprocessors when using REST + * Assured. + * + * @author Filip Hrisafov + * @since 2.0.0 + */ +public final class RestAssuredOperationPreprocessorsConfigurer extends + OperationPreprocessorsConfigurer + implements Filter { + + RestAssuredOperationPreprocessorsConfigurer(RestAssuredRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, + FilterContext context) { + return and().filter(requestSpec, responseSpec, context); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java new file mode 100644 index 00000000..9d50090a --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -0,0 +1,167 @@ +/* + * 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.restassured; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; + +import io.restassured.http.Cookie; +import io.restassured.http.Header; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.MultiPartSpecification; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +/** + * A converter for creating an {@link OperationRequest} from a REST Assured + * {@link FilterableRequestSpecification}. + * + * @author Andy Wilkinson + */ +class RestAssuredRequestConverter implements RequestConverter { + + @Override + public OperationRequest convert(FilterableRequestSpecification requestSpec) { + return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), + HttpMethod.valueOf(requestSpec.getMethod()), extractContent(requestSpec), extractHeaders(requestSpec), + extractParameters(requestSpec), extractParts(requestSpec), extractCookies(requestSpec)); + } + + private Collection extractCookies(FilterableRequestSpecification requestSpec) { + Collection cookies = new ArrayList<>(); + for (Cookie cookie : requestSpec.getCookies()) { + cookies.add(new RequestCookie(cookie.getName(), cookie.getValue())); + } + return cookies; + } + + private byte[] extractContent(FilterableRequestSpecification requestSpec) { + return convertContent(requestSpec.getBody()); + } + + private byte[] convertContent(Object content) { + if (content instanceof String) { + return ((String) content).getBytes(); + } + else if (content instanceof byte[]) { + return (byte[]) content; + } + else if (content instanceof File) { + return copyToByteArray((File) content); + } + else if (content instanceof InputStream) { + return copyToByteArray((InputStream) content); + } + else if (content == null) { + return new byte[0]; + } + else { + throw new IllegalStateException("Unsupported request content: " + content.getClass().getName()); + } + } + + private byte[] copyToByteArray(File file) { + try { + return FileCopyUtils.copyToByteArray(file); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to read content from file " + file, ex); + } + } + + private byte[] copyToByteArray(InputStream inputStream) { + try { + inputStream.reset(); + } + catch (IOException ex) { + throw new IllegalStateException( + "Cannot read content from input stream " + inputStream + " due to reset() failure"); + } + try { + return StreamUtils.copyToByteArray(inputStream); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to read content from input stream " + inputStream, ex); + } + } + + private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Header header : requestSpec.getHeaders()) { + if (!isAllMediaTypesAcceptHeader(header)) { + httpHeaders.add(header.getName(), header.getValue()); + } + } + return httpHeaders; + } + + private boolean isAllMediaTypesAcceptHeader(Header header) { + return HttpHeaders.ACCEPT.equals(header.getName()) && "*/*".equals(header.getValue()); + } + + private Parameters extractParameters(FilterableRequestSpecification requestSpec) { + Parameters parameters = new Parameters(); + for (Entry entry : requestSpec.getQueryParams().entrySet()) { + if (entry.getValue() instanceof Collection) { + Collection queryParams = ((Collection) entry.getValue()); + for (Object queryParam : queryParams) { + parameters.add(entry.getKey(), queryParam.toString()); + } + } + else { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + } + for (Entry entry : requestSpec.getRequestParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + for (Entry entry : requestSpec.getFormParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + return parameters; + } + + private Collection extractParts(FilterableRequestSpecification requestSpec) { + List parts = new ArrayList<>(); + for (MultiPartSpecification multiPartSpec : requestSpec.getMultiPartParams()) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType((multiPartSpec.getMimeType() != null) + ? MediaType.parseMediaType(multiPartSpec.getMimeType()) : MediaType.TEXT_PLAIN); + parts.add(new OperationRequestPartFactory().create(multiPartSpec.getControlName(), + multiPartSpec.getFileName(), convertContent(multiPartSpec.getContent()), headers)); + } + return parts; + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java new file mode 100644 index 00000000..a8e44f99 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -0,0 +1,53 @@ +/* + * 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.restassured; + +import io.restassured.http.Header; +import io.restassured.response.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.ResponseConverter; + +/** + * A converter for creating an {@link OperationResponse} from a REST Assured + * {@link Response}. + * + * @author Andy Wilkinson + */ +class RestAssuredResponseConverter implements ResponseConverter { + + @Override + public OperationResponse convert(Response response) { + return new OperationResponseFactory().create(response.getStatusCode(), extractHeaders(response), + extractContent(response)); + } + + private HttpHeaders extractHeaders(Response response) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Header header : response.getHeaders()) { + httpHeaders.add(header.getName(), header.getValue()); + } + return httpHeaders; + } + + private byte[] extractContent(Response response) { + return response.getBody().asByteArray(); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java new file mode 100644 index 00000000..a5309d49 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -0,0 +1,111 @@ +/* + * 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.restassured; + +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.generate.RestDocumentationGenerator; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting RESTful APIs using REST Assured. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public abstract class RestAssuredRestDocumentation { + + private static final RestAssuredRequestConverter REQUEST_CONVERTER = new RestAssuredRequestConverter(); + + private static final RestAssuredResponseConverter RESPONSE_CONVERTER = new RestAssuredResponseConverter(); + + private RestAssuredRestDocumentation() { + + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets}. + * @param identifier an identifier for the API call that is being documented + * @param snippets the snippets that will document the API call + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, Snippet... snippets) { + return new RestDocumentationFilter( + new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} is applied to the request before it is documented. + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, + Snippet... snippets) { + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, requestPreprocessor, snippets)); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code responsePreprocessor} is applied to the request before it is documented. + * @param identifier an identifier for the API call that is being documented + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, responsePreprocessor, snippets)); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the + * request and response respectively before they are documented. + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); + } + + /** + * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be + * used to configure Spring REST Docs using the given {@code contextProvider}. + * @param contextProvider the context provider + * @return the configurer + */ + public static RestAssuredRestDocumentationConfigurer documentationConfiguration( + RestDocumentationContextProvider contextProvider) { + return new RestAssuredRestDocumentationConfigurer(contextProvider); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java new file mode 100644 index 00000000..401d02b8 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -0,0 +1,75 @@ +/* + * 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.restassured; + +import java.util.HashMap; +import java.util.Map; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.config.RestDocumentationConfigurer; + +/** + * A REST Assured-specific {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + * @author Filip Hrisafov + * @since 1.2.0 + */ +public final class RestAssuredRestDocumentationConfigurer extends + RestDocumentationConfigurer + implements Filter { + + private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer(this); + + private final RestAssuredOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new RestAssuredOperationPreprocessorsConfigurer( + this); + + private final RestDocumentationContextProvider contextProvider; + + RestAssuredRestDocumentationConfigurer(RestDocumentationContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public RestAssuredSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + @Override + public RestAssuredOperationPreprocessorsConfigurer operationPreprocessors() { + return this.operationPreprocessorsConfigurer; + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, + FilterContext filterContext) { + RestDocumentationContext context = this.contextProvider.beforeOperation(); + filterContext.setValue(RestDocumentationContext.class.getName(), context); + Map configuration = new HashMap<>(); + filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, configuration); + apply(configuration, context); + return filterContext.next(requestSpec, responseSpec); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java new file mode 100644 index 00000000..250f61fb --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -0,0 +1,47 @@ +/* + * 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.restassured; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; + +import org.springframework.restdocs.config.SnippetConfigurer; + +/** + * A configurer that can be used to configure the generated documentation snippets when + * using REST Assured. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public final class RestAssuredSnippetConfigurer extends + SnippetConfigurer implements Filter { + + RestAssuredSnippetConfigurer(RestAssuredRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, + FilterContext context) { + return and().filter(requestSpec, responseSpec, context); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java new file mode 100644 index 00000000..e52b9c31 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -0,0 +1,108 @@ +/* + * 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.restassured; + +import java.util.HashMap; +import java.util.Map; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.generate.RestDocumentationGenerator; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.Assert; + +/** + * A REST Assured {@link Filter} for documenting RESTful APIs. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class RestDocumentationFilter implements Filter { + + static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; + + private final RestDocumentationGenerator delegate; + + RestDocumentationFilter(RestDocumentationGenerator delegate) { + Assert.notNull(delegate, "delegate must be non-null"); + this.delegate = delegate; + } + + @Override + public final Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext context) { + Response response = context.next(requestSpec, responseSpec); + + Map configuration = getConfiguration(requestSpec, context); + + this.delegate.handle(requestSpec, response, configuration); + + return response; + } + + /** + * Returns the configuration that should be used when calling the delgate. The + * configuration is derived from the given {@code requestSpec} and {@code context}. + * @param requestSpec the request specification + * @param context the filter context + * @return the configuration + */ + protected Map getConfiguration(FilterableRequestSpecification requestSpec, FilterContext context) { + Map configuration = new HashMap<>(retrieveConfiguration(context)); + configuration.put(RestDocumentationContext.class.getName(), + context.getValue(RestDocumentationContext.class.getName())); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); + return configuration; + } + + /** + * Creates a new {@link RestDocumentationFilter} that will produce documentation using + * the given {@code snippets}. + * @param snippets the snippets + * @return the new result handler + */ + public final RestDocumentationFilter document(Snippet... snippets) { + return new RestDocumentationFilter(this.delegate.withSnippets(snippets)) { + + @Override + protected Map getConfiguration(FilterableRequestSpecification requestSpec, + FilterContext context) { + Map configuration = super.getConfiguration(requestSpec, context); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR); + return configuration; + } + + }; + } + + private static Map retrieveConfiguration(FilterContext context) { + Map configuration = context.getValue(CONTEXT_KEY_CONFIGURATION); + Assert.state(configuration != null, + () -> "REST Docs configuration not found. Did you forget to add a " + + RestAssuredRestDocumentationConfigurer.class.getSimpleName() + + " as a filter when building the RequestSpecification?"); + return configuration; + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java new file mode 100644 index 00000000..502b9a02 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Core classes for using Spring REST Docs with REST Assured. + */ +package org.springframework.restdocs.restassured; 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 new file mode 100644 index 00000000..ba17a0b5 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -0,0 +1,295 @@ +/* + * 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.restassured; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; + +import io.restassured.RestAssured; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.RequestSpecification; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RestAssuredRequestConverter}. + * + * @author Andy Wilkinson + */ +public class RestAssuredRequestConverterTests { + + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); + + @Test + public void requestUri() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); + requestSpec.get("/foo/bar"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:" + tomcat.getPort() + "/foo/bar")); + } + + @Test + public void requestMethod() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); + requestSpec.head("/foo/bar"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getMethod()).isEqualTo(HttpMethod.HEAD); + } + + @Test + public void queryStringParameters() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).queryParam("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters()).hasSize(1); + assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); + } + + @Test + public void queryStringFromUrlParameters() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); + requestSpec.get("/?foo=bar&foo=qix"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters()).hasSize(1); + assertThat(request.getParameters()).containsEntry("foo", Arrays.asList("bar", "qix")); + } + + @Test + public void formParameters() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).formParam("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters()).hasSize(1); + assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); + } + + @Test + public void requestParameters() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).param("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters()).hasSize(1); + assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); + } + + @Test + 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())); + } + + @Test + public void headersWithCustomAccept() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).header("Foo", "bar") + .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())); + } + + @Test + public void cookies() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).cookie("cookie1", "cookieVal1") + .cookie("cookie2", "cookieVal2"); + requestSpec.get("/"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getCookies().size()).isEqualTo(2); + + Iterator cookieIterator = request.getCookies().iterator(); + RequestCookie cookie1 = cookieIterator.next(); + + assertThat(cookie1.getName()).isEqualTo("cookie1"); + assertThat(cookie1.getValue()).isEqualTo("cookieVal1"); + + RequestCookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName()).isEqualTo("cookie2"); + assertThat(cookie2.getValue()).isEqualTo("cookieVal2"); + } + + @Test + public void multipart() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) + .multiPart("a", "a.txt", "alpha", null).multiPart("b", new ObjectBody("bar"), "application/json"); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + Collection parts = request.getParts(); + assertThat(parts).hasSize(2); + 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).containsExactly( + Collections.singletonList(MediaType.TEXT_PLAIN_VALUE), + Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); + } + + @Test + public void byteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()).port(tomcat.getPort()); + requestSpec.post(); + this.factory.convert((FilterableRequestSpecification) requestSpec); + } + + @Test + public void stringBody() { + RequestSpecification requestSpec = RestAssured.given().body("body").port(tomcat.getPort()); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString()).isEqualTo("body"); + } + + @Test + public void objectBody() { + RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")).port(tomcat.getPort()); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString()).isEqualTo("{\"foo\":\"bar\"}"); + } + + @Test + public void byteArrayInputStreamBody() { + RequestSpecification requestSpec = RestAssured.given().body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) + .port(tomcat.getPort()); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); + } + + @Test + public void fileBody() { + RequestSpecification requestSpec = RestAssured.given().body(new File("src/test/resources/body.txt")) + .port(tomcat.getPort()); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString()).isEqualTo("file"); + } + + @Test + public void fileInputStreamBody() throws FileNotFoundException { + FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); + RequestSpecification requestSpec = RestAssured.given().body(inputStream).port(tomcat.getPort()); + requestSpec.post(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Cannot read content from input stream " + inputStream + " due to reset() failure"); + this.factory.convert((FilterableRequestSpecification) requestSpec); + } + + @Test + public void multipartWithByteArrayInputStreamBody() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("foo", "foo.txt", + new ByteArrayInputStream("foo".getBytes())); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); + } + + @Test + public void multipartWithStringBody() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", "foo"); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); + } + + @Test + public void multipartWithByteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", "file", + "foo".getBytes()); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); + } + + @Test + public void multipartWithFileBody() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) + .multiPart(new File("src/test/resources/body.txt")); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("file"); + } + + @Test + public void multipartWithFileInputStreamBody() throws FileNotFoundException { + FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("foo", "foo.txt", + inputStream); + requestSpec.post(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Cannot read content from input stream " + inputStream + " due to reset() failure"); + this.factory.convert((FilterableRequestSpecification) requestSpec); + } + + @Test + public void multipartWithObjectBody() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", + new ObjectBody("bar")); + requestSpec.post(); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("{\"foo\":\"bar\"}"); + } + + /** + * Sample object body to verify JSON serialization. + */ + public static class ObjectBody { + + private final String foo; + + ObjectBody(String foo) { + this.foo = foo; + } + + public String getFoo() { + return this.foo; + } + + } + +} 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 new file mode 100644 index 00000000..c3f79015 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java @@ -0,0 +1,52 @@ +/* + * 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.restassured; + +import io.restassured.http.Headers; +import io.restassured.response.Response; +import io.restassured.response.ResponseBody; +import org.junit.Test; + +import org.springframework.restdocs.operation.OperationResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestAssuredResponseConverter}. + * + * @author Andy Wilkinson + */ +public class RestAssuredResponseConverterTests { + + private final RestAssuredResponseConverter converter = new RestAssuredResponseConverter(); + + @Test + public void responseWithCustomStatus() { + Response response = mock(Response.class); + given(response.getStatusCode()).willReturn(600); + given(response.getHeaders()).willReturn(new Headers()); + ResponseBody body = mock(ResponseBody.class); + given(response.getBody()).willReturn(body); + given(body.asByteArray()).willReturn(new byte[0]); + OperationResponse operationResponse = this.converter.convert(response); + assertThat(operationResponse.getStatus()).isNull(); + assertThat(operationResponse.getStatusCode()).isEqualTo(600); + } + +} 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 new file mode 100644 index 00000000..0cebef3b --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -0,0 +1,89 @@ +/* + * 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.restassured; + +import java.util.List; +import java.util.Map; + +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.mockito.ArgumentCaptor; + +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.generate.RestDocumentationGenerator; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.operation.preprocess.Preprocessors; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.TemplateEngine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestAssuredRestDocumentationConfigurer}. + * + * @author Andy Wilkinson + * @author Filip Hrisafov + */ +public class RestAssuredRestDocumentationConfigurerTests { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); + + private final FilterableResponseSpecification responseSpec = mock(FilterableResponseSpecification.class); + + private final FilterContext filterContext = mock(FilterContext.class); + + private final RestAssuredRestDocumentationConfigurer configurer = new RestAssuredRestDocumentationConfigurer( + this.restDocumentation); + + @Test + public void nextFilterIsCalled() { + this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); + verify(this.filterContext).next(this.requestSpec, this.responseSpec); + } + + @Test + public void configurationIsAddedToTheContext() { + this.configurer.operationPreprocessors().withRequestDefaults(Preprocessors.prettyPrint()) + .withResponseDefaults(Preprocessors.removeHeaders("Foo")) + .filter(this.requestSpec, this.responseSpec, this.filterContext); + @SuppressWarnings("rawtypes") + ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); + verify(this.filterContext).setValue(eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), + configurationCaptor.capture()); + @SuppressWarnings("unchecked") + Map configuration = configurationCaptor.getValue(); + assertThat(configuration.get(TemplateEngine.class.getName())).isInstanceOf(TemplateEngine.class); + assertThat(configuration.get(WriterResolver.class.getName())).isInstanceOf(WriterResolver.class); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS)) + .isInstanceOf(List.class); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR)) + .isInstanceOf(OperationRequestPreprocessor.class); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR)) + .isInstanceOf(OperationResponsePreprocessor.class); + } + +} 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 new file mode 100644 index 00000000..a83a378d --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -0,0 +1,410 @@ +/* + * 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.restassured; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +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.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; +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.util.FileCopyUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +/** + * Integration tests for using Spring REST Docs with REST Assured. + * + * @author Andy Wilkinson + * @author Tomasz Kopczynski + * @author Filip Hrisafov + */ +public class RestAssuredRestDocumentationIntegrationTests { + + @Rule + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); + + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); + + @Test + public void defaultSnippetGeneration() { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("default")).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void curlSnippetWithContent() throws Exception { + String contentType = "text/plain; charset=UTF-8"; + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-content")).accept("application/json").body("content") + .contentType(contentType).post("/").then().statusCode(200); + + assertThat(new File("build/generated-snippets/curl-snippet-with-content/curl-request.adoc")).has(content( + codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ curl 'http://localhost:" + + tomcat.getPort() + "/' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" + + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'content'")))); + } + + @Test + public void curlSnippetWithCookies() throws Exception { + String contentType = "text/plain; charset=UTF-8"; + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-cookies")).accept("application/json").contentType(contentType) + .cookie("cookieName", "cookieVal").get("/").then().statusCode(200); + assertThat(new File("build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc")).has(content( + codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ curl 'http://localhost:" + + tomcat.getPort() + "/' -i -X GET \\%n" + " -H 'Accept: application/json' \\%n" + + " -H 'Content-Type: " + contentType + "' \\%n" + " --cookie 'cookieName=cookieVal'")))); + } + + @Test + public void curlSnippetWithEmptyParameterQueryString() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-empty-parameter-query-string")).accept("application/json") + .param("a", "").get("/").then().statusCode(200); + assertThat( + new File("build/generated-snippets/curl-snippet-with-empty-parameter-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl 'http://localhost:" + tomcat.getPort() + + "/?a=' -i -X GET \\%n -H 'Accept: application/json'")))); + } + + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-query-string")).accept("application/json").param("foo", "bar") + .param("a", "alpha").post("/?foo=bar").then().statusCode(200); + String contentType = "application/x-www-form-urlencoded; charset=ISO-8859-1"; + assertThat(new File("build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl " + "'http://localhost:" + tomcat.getPort() + + "/?foo=bar' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" + + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'a=alpha'")))); + } + + @Test + public void linksSnippet() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("links", links(linkWithRel("rel").description("The description")))) + .accept("application/json").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "links.adoc"); + } + + @Test + public void pathParametersSnippet() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("path-parameters", + pathParameters(parameterWithName("foo").description("The description")))) + .accept("application/json").get("/{foo}", "").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/path-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "path-parameters.adoc"); + } + + @Test + public void requestParametersSnippet() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-parameters", + requestParameters(parameterWithName("foo").description("The description")))) + .accept("application/json").param("foo", "bar").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parameters.adoc"); + } + + @Test + public void requestFieldsSnippet() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-fields", requestFields(fieldWithPath("a").description("The description")))) + .accept("application/json").body("{\"a\":\"alpha\"}").post("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-fields"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); + } + + @Test + public void requestPartsSnippet() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-parts", requestParts(partWithName("a").description("The description")))) + .multiPart("a", "foo").post("/upload").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parts"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); + } + + @Test + public void responseFieldsSnippet() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("response-fields", + responseFields(fieldWithPath("a").description("The description"), + subsectionWithPath("links").description("Links to other resources")))) + .accept("application/json").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/response-fields"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "response-fields.adoc"); + } + + @Test + public void parameterizedOutputDirectory() throws Exception { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("{method-name}")).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/parameterized-output-directory"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void multiStep() throws Exception { + RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(document("{method-name}-{step}")).build(); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void additionalSnippets() throws Exception { + RestDocumentationFilter documentation = document("{method-name}-{step}"); + RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) + .addFilter(documentationConfiguration(this.restDocumentation)).addFilter(documentation).build(); + given(spec).filter(documentation.document( + responseHeaders(headerWithName("a").description("one"), headerWithName("Foo").description("two")))) + .get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", "response-headers.adoc"); + } + + @Test + public void responseWithCookie() { + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("set-cookie", + preprocessResponse(removeHeaders(HttpHeaders.DATE, HttpHeaders.CONTENT_TYPE)))) + .get("/set-cookie").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/set-cookie"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header(HttpHeaders.SET_COOKIE, "name=value; Domain=localhost; HttpOnly") + .header("Keep-Alive", "timeout=60").header("Connection", "keep-alive"))); + } + + @Test + public void preprocessedRequest() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)).header("a", "alpha") + .header("b", "bravo").contentType("application/json").accept("application/json") + .body("{\"a\":\"alpha\"}").filter(document("original-request")) + .filter(document("preprocessed-request", + preprocessRequest(prettyPrint(), replacePattern(pattern, "\"<>\""), + modifyUris().removePort(), removeHeaders("a", HttpHeaders.CONTENT_LENGTH)))) + .get("/").then().statusCode(200); + assertThat(new File("build/generated-snippets/original-request/http-request.adoc")) + .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("a", "alpha") + .header("b", "bravo").header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", "application/json").header("Host", "localhost:" + tomcat.getPort()) + .header("Content-Length", "13").content("{\"a\":\"alpha\"}"))); + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); + assertThat(new File("build/generated-snippets/preprocessed-request/http-request.adoc")) + .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") + .header("Accept", MediaType.APPLICATION_JSON_VALUE).header("Content-Type", "application/json") + .header("Host", "localhost").content(prettyPrinted))); + } + + @Test + public void defaultPreprocessedRequest() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation).operationPreprocessors().withRequestDefaults( + prettyPrint(), replacePattern(pattern, "\"<>\""), modifyUris().removePort(), + removeHeaders("a", HttpHeaders.CONTENT_LENGTH))) + .header("a", "alpha").header("b", "bravo").contentType("application/json").accept("application/json") + .body("{\"a\":\"alpha\"}").filter(document("default-preprocessed-request")).get("/").then() + .statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); + assertThat(new File("build/generated-snippets/default-preprocessed-request/http-request.adoc")) + .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") + .header("Accept", MediaType.APPLICATION_JSON_VALUE).header("Content-Type", "application/json") + .header("Host", "localhost").content(prettyPrinted))); + } + + @Test + public void preprocessedResponse() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("original-response")) + .filter(document("preprocessed-response", + preprocessResponse(prettyPrint(), maskLinks(), + removeHeaders("a", "Transfer-Encoding", "Date", "Server"), + replacePattern(pattern, "\"<>\""), + modifyUris().scheme("https").host("api.example.com").removePort()))) + .get("/").then().statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); + assertThat(new File("build/generated-snippets/preprocessed-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header("Foo", "https://api.example.com/foo/bar") + .header("Content-Type", "application/json;charset=UTF-8").header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive") + .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length).content(prettyPrinted))); + } + + @Test + public void defaultPreprocessedResponse() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withResponseDefaults(prettyPrint(), maskLinks(), + removeHeaders("a", "Transfer-Encoding", "Date", "Server"), + replacePattern(pattern, "\"<>\""), + modifyUris().scheme("https").host("api.example.com").removePort())) + .filter(document("default-preprocessed-response")).get("/").then().statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); + assertThat(new File("build/generated-snippets/default-preprocessed-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header("Foo", "https://api.example.com/foo/bar") + .header("Content-Type", "application/json;charset=UTF-8").header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive") + .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length).content(prettyPrinted))); + } + + @Test + public void customSnippetTemplate() throws Exception { + ClassLoader classLoader = new URLClassLoader( + new URL[] { new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, + getClass().getClassLoader()); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + try { + given().port(tomcat.getPort()).accept("application/json") + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("custom-snippet-template")).get("/").then().statusCode(200); + } + finally { + Thread.currentThread().setContextClassLoader(previous); + } + assertThat(new File("build/generated-snippets/custom-snippet-template/curl-request.adoc")) + .hasContent("Custom curl request"); + } + + @Test + public 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 " + + "RestAssuredRestDocumentationConfigurer as a filter when building the RequestSpecification?"); + } + + @Test + public void exceptionShouldBeThrownWhenCallDocumentSnippetsRequestSpecificationNotConfigured() { + RestDocumentationFilter documentation = document("{method-name}-{step}"); + assertThatThrownBy(() -> given().port(tomcat.getPort()) + .filter(documentation.document(responseHeaders(headerWithName("a").description("one")))).get("/")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("REST Docs configuration not found. Did you forget to add a " + + "RestAssuredRestDocumentationConfigurer as a filter when building the " + + "RequestSpecification?"); + } + + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { + for (String snippet : snippets) { + assertThat(new File(directory, snippet)).isFile(); + } + } + + private Condition content(final Condition delegate) { + return new Condition() { + + @Override + public boolean matches(File value) { + try { + return delegate.matches(FileCopyUtils + .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8))); + } + catch (IOException ex) { + fail("Failed to read '" + value + "'", ex); + return false; + } + } + + }; + } + + private CodeBlockCondition codeBlock(TemplateFormat format, String language) { + return SnippetConditions.codeBlock(format, language); + } + + private HttpRequestCondition httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { + return SnippetConditions.httpRequest(format, requestMethod, uri); + } + + private HttpResponseCondition httpResponse(TemplateFormat format, HttpStatus status) { + return SnippetConditions.httpResponse(format, status); + } + +} 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 new file mode 100644 index 00000000..f09029a9 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014-20212the 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.restassured; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.junit.rules.ExternalResource; + +/** + * {@link ExternalResource} that starts and stops a Tomcat server. + * + * @author Andy Wilkinson + */ +class TomcatServer extends ExternalResource { + + private Tomcat tomcat; + + 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.start(); + this.port = this.tomcat.getConnector().getLocalPort(); + } + + @Override + protected void after() { + try { + this.tomcat.stop(); + } + catch (LifecycleException ex) { + throw new RuntimeException(ex); + } + } + + int getPort() { + return this.port; + } + + /** + * {@link HttpServlet} used to handle requests in the tests. + */ + private static final class TestServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + respondWithJson(response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + respondWithJson(response); + } + + private void respondWithJson(HttpServletResponse response) throws IOException, JsonProcessingException { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + Map content = new HashMap<>(); + content.put("a", "alpha"); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + content.put("links", Arrays.asList(link)); + response.getWriter().println(new ObjectMapper().writeValueAsString(content)); + response.setHeader("a", "alpha"); + response.setHeader("Foo", "http://localhost:12345/foo/bar"); + response.flushBuffer(); + } + + } + + /** + * {@link HttpServlet} used to handle cookies-related requests in the tests. + */ + private static final class CookiesServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + Cookie cookie = new Cookie("name", "value"); + cookie.setDomain("localhost"); + cookie.setHttpOnly(true); + + resp.addCookie(cookie); + } + + } + +} diff --git a/spring-restdocs-restassured/src/test/resources/body.txt b/spring-restdocs-restassured/src/test/resources/body.txt new file mode 100644 index 00000000..1a010b1c --- /dev/null +++ b/spring-restdocs-restassured/src/test/resources/body.txt @@ -0,0 +1 @@ +file \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet new file mode 100644 index 00000000..07f3a48f --- /dev/null +++ b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet @@ -0,0 +1 @@ +Custom curl request \ No newline at end of file From 894dac4b90bb8b24d0ab4ea635953bff8e6a2db9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 24 Jan 2022 12:11:57 +0000 Subject: [PATCH 028/198] Polish --- .../org/springframework/restdocs/restassured/TomcatServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f09029a9..2c1502ae 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-20212the original author or authors. + * 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. From dae2f692a4d89b3bd19461b10e68b827e56bd798 Mon Sep 17 00:00:00 2001 From: Jan Nielsen Date: Fri, 4 Mar 2022 15:07:43 +0100 Subject: [PATCH 029/198] Resolve merge conflict in comment See gh-787 --- .../src/test/java/com/example/notes/ApiDocumentation.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index 29addc86..64c880f2 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,9 +1,5 @@ /* -<<<<<<< HEAD - * Copyright 2014-2021 the original author or authors. -======= * Copyright 2014-2022 the original author or authors. ->>>>>>> 2.0.x * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From f47663360d5b381a69f95554aac99622aa3de2f5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 18 Mar 2022 13:51:48 +0000 Subject: [PATCH 030/198] Upgrade to Spring Framework 6.0.0-M3 Closes gh-788 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b2b2da8d..c9c9fdd3 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ nohttp { } ext { - springFrameworkVersion = "6.0.0-M2" + springFrameworkVersion = "6.0.0-M3" javadocLinks = [ "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", "https://docs.jboss.org/hibernate/validator/7.0/api/", From 6c341cc63c6361b6cd18fc77bdaad8ce2fbdd8c1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Mar 2022 15:57:31 +0000 Subject: [PATCH 031/198] Upgrade samples to Spring Framework 6.0.0-M3 See gh-788 --- samples/junit5/build.gradle | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- samples/web-test-client/build.gradle | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index c9fd2aef..9439b896 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -28,7 +28,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") implementation 'org.springframework:spring-webmvc' testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 059311b8..b2374812 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -27,7 +27,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") implementation 'io.projectreactor.netty:reactor-netty-http:1.0.15' implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d1331806..19bc31a0 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -22,7 +22,7 @@ ext { ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { - implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M1") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 73d24fdc..e4eaff3a 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -20,7 +20,7 @@ org.springframework spring-framework-bom - 6.0.0-M2 + 6.0.0-M3 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 4158fee1..c1accf21 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -27,7 +27,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M1") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index c0509fe6..60f78f1e 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -28,7 +28,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") implementation "org.springframework:spring-webmvc" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index 94205f01..d83cfa20 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -29,7 +29,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M2") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' From f27a0041960f43fa986e450ccc5ef7bcebd3dd0c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 21 Mar 2022 15:59:41 +0000 Subject: [PATCH 032/198] Upgrade samples to Spring Data 2022.0.0-M2 --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index e4eaff3a..abf65d7b 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -27,7 +27,7 @@ org.springframework.data spring-data-bom - 2022.0.0-M1 + 2022.0.0-M2 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index c1accf21..43795d05 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,14 +28,14 @@ dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M1") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M2") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" - implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M1" + implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M2" runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'org.atteo:evo-inflector:1.2.1' From ba88d6eaf82125efb3c72da972f8091c31fc403d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 22 Mar 2022 14:24:36 +0000 Subject: [PATCH 033/198] Upgrade samples to Spring Data 2022.0.0-M3 Closes gh-789 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 19bc31a0..5952349d 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -23,7 +23,7 @@ ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M1") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M3") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index abf65d7b..47ba689e 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -27,7 +27,7 @@ org.springframework.data spring-data-bom - 2022.0.0-M2 + 2022.0.0-M3 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 43795d05..5a34e0f6 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M2") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M3") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" From f01d18b049ee66b1d374ddbaef2f129b50331629 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 22 Mar 2022 14:30:20 +0000 Subject: [PATCH 034/198] Upgrade Spring HATEOAS sample to H2 2.0.206 --- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 5a34e0f6..58c8e74d 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation "org.springframework.data:spring-data-jpa" implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M2" - runtimeOnly 'com.h2database:h2:1.4.200' + runtimeOnly 'com.h2database:h2:2.0.206' runtimeOnly 'org.atteo:evo-inflector:1.2.1' testImplementation 'com.jayway.jsonpath:json-path:2.6.0' From df6b902461fd6410e85bc008e9981da589243f05 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 25 Mar 2022 09:41:19 +0000 Subject: [PATCH 035/198] Upgrade to REST Assured 5.0.0 Closes gh-791 --- docs/src/docs/asciidoc/getting-started.adoc | 2 +- samples/rest-assured/build.gradle | 2 +- spring-restdocs-platform/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index ecd86d56..bd60c9a4 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -76,7 +76,7 @@ Spring REST Docs has the following minimum requirements: * Java 17 * Spring Framework 6 -Additionally, the `spring-restdocs-restassured` module requires REST Assured 4 (4.5 or later). +Additionally, the `spring-restdocs-restassured` module requires REST Assured 5. [[getting-started-build-configuration]] === Build configuration diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index b2374812..40f5ca60 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -33,7 +33,7 @@ dependencies { implementation 'org.springframework:spring-webflux' - testImplementation 'io.rest-assured:rest-assured:4.5.0' + testImplementation 'io.rest-assured:rest-assured:5.0.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' testImplementation "org.springframework.restdocs:spring-restdocs-restassured:$restdocsVersion" testImplementation 'org.springframework:spring-test' diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 7a75ef2f..f99a1681 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -12,7 +12,7 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api:5.0.0") api("jakarta.validation:jakarta.validation-api:3.0.0") api("junit:junit:4.12") - api("io.rest-assured:rest-assured:4.5.0") + api("io.rest-assured:rest-assured:5.0.0") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.2") api("org.asciidoctor:asciidoctorj-pdf:1.6.0") From e8d5e8ae7a3cfcc9cec76475cfa0bfdf6c28e523 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 11 Apr 2022 15:37:28 +0100 Subject: [PATCH 036/198] Start building against Spring Framwork 6.0 M4 snapshots See gh-794 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c9c9fdd3..a204edc6 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ nohttp { } ext { - springFrameworkVersion = "6.0.0-M3" + springFrameworkVersion = "6.0.0-SNAPSHOT" javadocLinks = [ "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", "https://docs.jboss.org/hibernate/validator/7.0/api/", From 170e9c5b5ce795f929b66e3c0a7ce3c2a0f5f2cd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2022 10:47:36 +0100 Subject: [PATCH 037/198] Polish repository configuration --- build.gradle | 9 ++++++--- settings.gradle | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index a204edc6..fb2bc10d 100644 --- a/build.gradle +++ b/build.gradle @@ -8,10 +8,13 @@ plugins { allprojects { group = "org.springframework.restdocs" repositories { - mavenLocal() mavenCentral() - maven { url "https://repo.spring.io/milestone" } - maven { url "https://repo.spring.io/snapshot" } + if (version.contains('-')) { + maven { url "https://repo.spring.io/milestone" } + } + if (version.endsWith('-SNAPSHOT')) { + maven { url "https://repo.spring.io/snapshot" } + } } } diff --git a/settings.gradle b/settings.gradle index b6f6cf84..9566d005 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,5 @@ pluginManagement { repositories { - mavenLocal() mavenCentral() maven { url "https://repo.spring.io/plugins-release-local" } maven { url "https://repo.spring.io/release" } From 169c29b9a49fce151de3f2c0ee5cc6a3a2002975 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2022 10:57:09 +0100 Subject: [PATCH 038/198] Start building samples against Framework, Data, and HATEOAS snapshots See gh-794 See gh-795 --- samples/junit5/build.gradle | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 4 ++-- samples/rest-notes-spring-data-rest/pom.xml | 4 ++-- samples/rest-notes-spring-hateoas/build.gradle | 6 +++--- samples/testng/build.gradle | 2 +- samples/web-test-client/build.gradle | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index 9439b896..9f0db453 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -28,7 +28,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") + implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") implementation 'org.springframework:spring-webmvc' testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 40f5ca60..c2563d5b 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -27,7 +27,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") + implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") implementation 'io.projectreactor.netty:reactor-netty-http:1.0.15' implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 5952349d..91877add 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -22,8 +22,8 @@ ext { ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { - implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M3") + implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 47ba689e..680df8fa 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -20,14 +20,14 @@ org.springframework spring-framework-bom - 6.0.0-M3 + 6.0.0-SNAPSHOT import pom org.springframework.data spring-data-bom - 2022.0.0-M3 + 2022.0.0-SNAPSHOT import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 58c8e74d..480cef63 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -27,15 +27,15 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M3") + implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" - implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M2" + implementation "org.springframework.hateoas:spring-hateoas:2.0.0-SNAPSHOT" runtimeOnly 'com.h2database:h2:2.0.206' runtimeOnly 'org.atteo:evo-inflector:1.2.1' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 60f78f1e..bbcddbed 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -28,7 +28,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") + implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") implementation "org.springframework:spring-webmvc" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index d83cfa20..95ee6c6c 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -29,7 +29,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M3") + implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' From 683686ea467c2022c4c8a02766520b6f9b298e65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2022 12:10:59 +0100 Subject: [PATCH 039/198] Restrict samples' usage of Maven local --- samples/junit5/build.gradle | 6 +++++- samples/rest-assured/build.gradle | 6 +++++- samples/rest-notes-slate/build.gradle | 6 +++++- samples/rest-notes-spring-hateoas/build.gradle | 6 +++++- samples/testng/build.gradle | 6 +++++- samples/web-test-client/build.gradle | 6 +++++- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index 9f0db453..996f8639 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -5,7 +5,11 @@ plugins { } repositories { - mavenLocal() + mavenLocal { + content { + includeGroup("org.springframework.restdocs") + } + } maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } mavenCentral() diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index c2563d5b..5f9dce85 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -4,10 +4,14 @@ plugins { } repositories { + mavenLocal { + content { + includeGroup("org.springframework.restdocs") + } + } maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } mavenCentral() - mavenLocal() } group = 'com.example' diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 91877add..ca92d07e 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,11 @@ plugins { } repositories { - mavenLocal() + mavenLocal { + content { + includeGroup("org.springframework.restdocs") + } + } maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } mavenCentral() diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 480cef63..7029d45e 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -4,7 +4,11 @@ plugins { } repositories { - mavenLocal() + mavenLocal { + content { + includeGroup("org.springframework.restdocs") + } + } maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } mavenCentral() diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index bbcddbed..c347521a 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -5,7 +5,11 @@ plugins { } repositories { - mavenLocal() + mavenLocal { + content { + includeGroup("org.springframework.restdocs") + } + } maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } mavenCentral() diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index 95ee6c6c..65c99a5c 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -6,10 +6,14 @@ apply plugin: 'java' apply plugin: 'eclipse' repositories { + mavenLocal { + content { + includeGroup("org.springframework.restdocs") + } + } maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } mavenCentral() - mavenLocal() } group = 'com.example' From 768a43a61ea326afa7249ff58f9c7e01d7fc5fbc Mon Sep 17 00:00:00 2001 From: Jihoon Cha Date: Mon, 7 Mar 2022 18:54:34 +0900 Subject: [PATCH 040/198] Provide a preprocessor for modifying request and response headers See gh-584 --- .../customizing-requests-and-responses.adoc | 7 + .../HeaderRemovingOperationPreprocessor.java | 2 + ...HeadersModifyingOperationPreprocessor.java | 227 ++++++++++++++++++ .../operation/preprocess/Preprocessors.java | 14 ++ ...rsModifyingOperationPreprocessorTests.java | 155 ++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index ec8f2932..06776da4 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -125,6 +125,13 @@ Any occurrences that match a regular expression are replaced. +[[customizing-requests-and-responses-preprocessors-modify-headers]] +==== Modifying Headers + +You can use `modifyHeaders` on `Preprocessors` to add, set, and remove request or response headers. + + + [[customizing-requests-and-responses-preprocessors-modify-request-parameters]] ==== Modifying Request Parameters diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 01710e99..f8bf8cc9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -31,7 +31,9 @@ * against the headers found * * @author Andy Wilkinson + * @deprecated Use {@link HeadersModifyingOperationPreprocessor} instead */ +@Deprecated class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); 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 new file mode 100644 index 00000000..fb002dac --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java @@ -0,0 +1,227 @@ +/* + * 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.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.util.Assert; + +/** + * An {@link OperationPreprocessor} that can be used to modify a request's + * {@link OperationRequest#getHeaders()} by adding, setting, and removing headers. + * + * @author Jihoon Cha + */ +public class HeadersModifyingOperationPreprocessor implements OperationPreprocessor { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + + private final List modifications = new ArrayList<>(); + + @Override + public OperationRequest preprocess(OperationRequest request) { + HttpHeaders headers = copyHttpHeaders(request.getHeaders()); + for (Modification modification : this.modifications) { + modification.applyTo(headers); + } + return this.requestFactory.createFrom(request, headers); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + HttpHeaders headers = copyHttpHeaders(response.getHeaders()); + for (Modification modification : this.modifications) { + modification.applyTo(headers); + } + return this.responseFactory.createFrom(response, headers); + } + + private HttpHeaders copyHttpHeaders(HttpHeaders headers) { + HttpHeaders copy = new HttpHeaders(); + for (String name : headers.keySet()) { + List values = headers.get(name); + if (values == null) { + continue; + } + copy.put(name, new ArrayList<>(values)); + } + return copy; + } + + /** + * Adds a header with the given {@code name} and {@code value}. + * @param name the name + * @param value the value + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor add(String name, String value) { + this.modifications.add(new AddHeaderModification(name, value)); + return this; + } + + /** + * Sets the header with the given {@code name} to have the given {@code values}. + * @param name the name + * @param values the values + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor set(String name, String... values) { + Assert.notEmpty(values, "At least one value must be provided"); + this.modifications.add(new SetHeaderModification(name, Arrays.asList(values))); + return this; + } + + /** + * Removes the header with the given {@code name}. + * @param name the name of the parameter + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor remove(String name) { + this.modifications.add(new RemoveHeaderModification(name)); + return this; + } + + /** + * Removes the given {@code value} from the header with the given {@code name}. + * @param name the name + * @param value the value + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor remove(String name, String value) { + this.modifications.add(new RemoveValueHeaderModification(name, value)); + return this; + } + + /** + * Remove headers that match the given {@code namePattern} regular expression. + * @param namePattern the name pattern + * @return {@code this} + * @see Matcher#matches() + */ + public HeadersModifyingOperationPreprocessor remove(Pattern namePattern) { + this.modifications.add(new RemoveHeadersByNamePatternModification(namePattern)); + return this; + } + + private interface Modification { + + void applyTo(HttpHeaders headers); + + } + + private static final class AddHeaderModification implements Modification { + + private final String name; + + private final String value; + + private AddHeaderModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.add(this.name, this.value); + } + + } + + private static final class SetHeaderModification implements Modification { + + private final String name; + + private final List values; + + private SetHeaderModification(String name, List values) { + this.name = name; + this.values = values; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.put(this.name, this.values); + } + + } + + private static final class RemoveHeaderModification implements Modification { + + private final String name; + + private RemoveHeaderModification(String name) { + this.name = name; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.remove(this.name); + } + + } + + private static final class RemoveValueHeaderModification implements Modification { + + private final String name; + + private final String value; + + private RemoveValueHeaderModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void applyTo(HttpHeaders headers) { + List values = headers.get(this.name); + if (values != null) { + values.remove(this.value); + if (values.isEmpty()) { + headers.remove(this.name); + } + } + } + + } + + private static final class RemoveHeadersByNamePatternModification implements Modification { + + private final Pattern namePattern; + + private RemoveHeadersByNamePatternModification(Pattern namePattern) { + this.namePattern = namePattern; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.keySet().removeIf((name) -> this.namePattern.matcher(name).matches()); + } + + } + +} 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 6be9107c..4fb1647e 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 @@ -31,6 +31,7 @@ * * @author Andy Wilkinson * @author Roland Huss + * @author Jihoon Cha */ public final class Preprocessors { @@ -73,8 +74,10 @@ public static OperationPreprocessor prettyPrint() { * {@code headersToRemove}. * @param headerNames the header names * @return the preprocessor + * @deprecated Use {@link #modifyHeaders()} instead * @see String#equals(Object) */ + @Deprecated public static OperationPreprocessor removeHeaders(String... headerNames) { return new HeaderRemovingOperationPreprocessor(new ExactMatchHeaderFilter(headerNames)); } @@ -85,8 +88,10 @@ public static OperationPreprocessor removeHeaders(String... headerNames) { * {@code headerNamePatterns} regular expressions. * @param headerNamePatterns the header name patterns * @return the preprocessor + * @deprecated Use {@link #modifyHeaders()} instead * @see java.util.regex.Matcher#matches() */ + @Deprecated public static OperationPreprocessor removeMatchingHeaders(String... headerNamePatterns) { return new HeaderRemovingOperationPreprocessor(new PatternMatchHeaderFilter(headerNamePatterns)); } @@ -132,6 +137,15 @@ public static ParametersModifyingOperationPreprocessor modifyParameters() { return new ParametersModifyingOperationPreprocessor(); } + /** + * Returns a {@code HeadersModifyingOperationPreprocessor} that can then be configured + * to modify the headers of the request. + * @return the preprocessor + */ + public static HeadersModifyingOperationPreprocessor modifyHeaders() { + return new HeadersModifyingOperationPreprocessor(); + } + /** * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the * request or response by changing one or more of their host, scheme, and port. 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 new file mode 100644 index 00000000..78129895 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java @@ -0,0 +1,155 @@ +/* + * 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.operation.preprocess; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.regex.Pattern; + +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.Parameters; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HeadersModifyingOperationPreprocessor}. + * + * @author Jihoon Cha + */ +public class HeadersModifyingOperationPreprocessorTests { + + private final HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + + @Test + public void addNewHeader() { + HttpHeaders headers = new HttpHeaders(); + OperationPreprocessor preprocessor = this.preprocessor.add("a", "alpha"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha")); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha")); + } + + @Test + public void addValueToExistingHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "apple"); + OperationPreprocessor preprocessor = this.preprocessor.add("a", "alpha"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + Arrays.asList("apple", "alpha")); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + Arrays.asList("apple", "alpha")); + } + + @Test + public void setNewHeader() { + HttpHeaders headers = new HttpHeaders(); + OperationPreprocessor preprocessor = this.preprocessor.set("a", "alpha", "avocado"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha", "avocado")); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha", "avocado")); + } + + @Test + public void setExistingHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "apple"); + OperationPreprocessor preprocessor = this.preprocessor.set("a", "alpha", "avocado"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha", "avocado")); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha", "avocado")); + } + + @Test + public void removeNonExistentHeader() { + HttpHeaders headers = new HttpHeaders(); + OperationPreprocessor preprocessor = this.preprocessor.remove("a"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + } + + @Test + public void removeHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "apple"); + OperationPreprocessor preprocessor = this.preprocessor.remove("a"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + } + + @Test + public void removeHeaderValueForNonExistentHeader() { + HttpHeaders headers = new HttpHeaders(); + OperationPreprocessor preprocessor = this.preprocessor.remove("a", "apple"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + } + + @Test + public void removeHeaderValueWithMultipleValues() { + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "apple"); + headers.add("a", "alpha"); + OperationPreprocessor preprocessor = this.preprocessor.remove("a", "apple"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha")); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + Arrays.asList("alpha")); + } + + @Test + public void removeHeaderValueWithSingleValueRemovesEntryEntirely() { + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "apple"); + OperationPreprocessor preprocessor = this.preprocessor.remove("a", "apple"); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + } + + @Test + public void removeHeadersByNamePattern() { + HttpHeaders headers = new HttpHeaders(); + headers.add("apple", "apple"); + headers.add("alpha", "alpha"); + headers.add("avocado", "avocado"); + headers.add("bravo", "bravo"); + OperationPreprocessor preprocessor = this.preprocessor.remove(Pattern.compile("^a.*")); + assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders().size()).isEqualTo(2); + assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders().size()).isEqualTo(1); + } + + private OperationRequest createRequest(HttpHeaders headers) { + return new OperationRequestFactory().create(URI.create("http://localhost:8080"), HttpMethod.GET, new byte[0], + headers, new Parameters(), Collections.emptyList()); + } + + private OperationResponse createResponse(HttpHeaders headers) { + return new OperationResponseFactory().create(HttpStatus.OK.value(), headers, new byte[0]); + } + +} From 36418f47d7a06664496edbc3ff5aa41c86f4e5e2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 May 2022 14:27:49 +0100 Subject: [PATCH 041/198] Polish "Provide a preprocessor for modifying request and response headers" See gh-584 --- .../customizing-requests-and-responses.adoc | 15 +- .../HeaderRemovingOperationPreprocessor.java | 69 ---------- ...HeadersModifyingOperationPreprocessor.java | 37 ++--- .../operation/preprocess/Preprocessors.java | 19 ++- ...derRemovingOperationPreprocessorTests.java | 104 -------------- ...rsModifyingOperationPreprocessorTests.java | 128 ++++++++++-------- 6 files changed, 102 insertions(+), 270 deletions(-) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 06776da4..f267612f 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -108,12 +108,10 @@ You can also specify a different replacement if you wish. -[[customizing-requests-and-responses-preprocessors-remove-headers]] -==== Removing Headers - -`removeHeaders` on `Preprocessors` removes any headers from the request or response where the name is equal to any of the given header names. +[[customizing-requests-and-responses-preprocessors-modify-headers]] +==== Modifying Headers -`removeMatchingHeaders` on `Preprocessors` removes any headers from the request or response where the name matches any of the given regular expression patterns. +You can use `modifyHeaders` on `Preprocessors` to add, set, and remove request or response headers. @@ -125,13 +123,6 @@ Any occurrences that match a regular expression are replaced. -[[customizing-requests-and-responses-preprocessors-modify-headers]] -==== Modifying Headers - -You can use `modifyHeaders` on `Preprocessors` to add, set, and remove request or response headers. - - - [[customizing-requests-and-responses-preprocessors-modify-request-parameters]] ==== Modifying Request Parameters diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java deleted file mode 100644 index f8bf8cc9..00000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ /dev/null @@ -1,69 +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.operation.preprocess; - -import java.util.List; -import java.util.Map.Entry; - -import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; - -/** - * An {@link OperationPreprocessor} that removes headers. The headers to remove are - * provided as constructor arguments and can be either plain string or patterns to match - * against the headers found - * - * @author Andy Wilkinson - * @deprecated Use {@link HeadersModifyingOperationPreprocessor} instead - */ -@Deprecated -class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final HeaderFilter headerFilter; - - HeaderRemovingOperationPreprocessor(HeaderFilter headerFilter) { - this.headerFilter = headerFilter; - } - - @Override - public OperationResponse preprocess(OperationResponse response) { - return this.responseFactory.createFrom(response, removeHeaders(response.getHeaders())); - } - - @Override - public OperationRequest preprocess(OperationRequest request) { - return this.requestFactory.createFrom(request, removeHeaders(request.getHeaders())); - } - - private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { - HttpHeaders processedHeaders = new HttpHeaders(); - for (Entry> header : originalHeaders.entrySet()) { - if (!this.headerFilter.excludeHeader(header.getKey())) { - processedHeaders.put(header.getKey(), header.getValue()); - } - } - return processedHeaders; - } - -} 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 fb002dac..72078847 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 @@ -30,10 +30,12 @@ import org.springframework.util.Assert; /** - * An {@link OperationPreprocessor} that can be used to modify a request's - * {@link OperationRequest#getHeaders()} by adding, setting, and removing headers. + * An {@link OperationPreprocessor} that modifies a request or response by adding, + * setting, or removing headers. * * @author Jihoon Cha + * @author Andy Wilkinson + * @since 3.0.0 */ public class HeadersModifyingOperationPreprocessor implements OperationPreprocessor { @@ -45,32 +47,21 @@ public class HeadersModifyingOperationPreprocessor implements OperationPreproces @Override public OperationRequest preprocess(OperationRequest request) { - HttpHeaders headers = copyHttpHeaders(request.getHeaders()); - for (Modification modification : this.modifications) { - modification.applyTo(headers); - } - return this.requestFactory.createFrom(request, headers); + return this.requestFactory.createFrom(request, preprocess(request.getHeaders())); } @Override public OperationResponse preprocess(OperationResponse response) { - HttpHeaders headers = copyHttpHeaders(response.getHeaders()); - for (Modification modification : this.modifications) { - modification.applyTo(headers); - } - return this.responseFactory.createFrom(response, headers); + return this.responseFactory.createFrom(response, preprocess(response.getHeaders())); } - private HttpHeaders copyHttpHeaders(HttpHeaders headers) { - HttpHeaders copy = new HttpHeaders(); - for (String name : headers.keySet()) { - List values = headers.get(name); - if (values == null) { - continue; - } - copy.put(name, new ArrayList<>(values)); + private HttpHeaders preprocess(HttpHeaders headers) { + HttpHeaders modifiedHeaders = new HttpHeaders(); + modifiedHeaders.putAll(headers); + for (Modification modification : this.modifications) { + modification.applyTo(modifiedHeaders); } - return copy; + return modifiedHeaders; } /** @@ -123,8 +114,8 @@ public HeadersModifyingOperationPreprocessor remove(String name, String value) { * @return {@code this} * @see Matcher#matches() */ - public HeadersModifyingOperationPreprocessor remove(Pattern namePattern) { - this.modifications.add(new RemoveHeadersByNamePatternModification(namePattern)); + public HeadersModifyingOperationPreprocessor removeMatching(String namePattern) { + this.modifications.add(new RemoveHeadersByNamePatternModification(Pattern.compile(namePattern))); return this; } 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 4fb1647e..a70b2169 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 @@ -74,12 +74,17 @@ public static OperationPreprocessor prettyPrint() { * {@code headersToRemove}. * @param headerNames the header names * @return the preprocessor - * @deprecated Use {@link #modifyHeaders()} instead + * @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) { - return new HeaderRemovingOperationPreprocessor(new ExactMatchHeaderFilter(headerNames)); + HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + for (String headerName : headerNames) { + preprocessor.remove(headerName); + } + return preprocessor; } /** @@ -88,12 +93,17 @@ public static OperationPreprocessor removeHeaders(String... headerNames) { * {@code headerNamePatterns} regular expressions. * @param headerNamePatterns the header name patterns * @return the preprocessor - * @deprecated Use {@link #modifyHeaders()} instead + * @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) { - return new HeaderRemovingOperationPreprocessor(new PatternMatchHeaderFilter(headerNamePatterns)); + HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + for (String headerNamePattern : headerNamePatterns) { + preprocessor.removeMatching(headerNamePattern); + } + return preprocessor; } /** @@ -141,6 +151,7 @@ public static ParametersModifyingOperationPreprocessor modifyParameters() { * Returns a {@code HeadersModifyingOperationPreprocessor} that can then be configured * to modify the headers of the request. * @return the preprocessor + * @since 3.0.0 */ public static HeadersModifyingOperationPreprocessor modifyHeaders() { return new HeadersModifyingOperationPreprocessor(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java deleted file mode 100644 index 87f5e8c0..00000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ /dev/null @@ -1,104 +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.operation.preprocess; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link HeaderRemovingOperationPreprocessorTests}. - * - * @author Andy Wilkinson - * @author Roland Huss - */ -public class HeaderRemovingOperationPreprocessorTests { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( - new ExactMatchHeaderFilter("b")); - - @Test - public void modifyRequestHeaders() { - OperationRequest request = this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - new byte[0], getHttpHeaders(), new Parameters(), Collections.emptyList()); - OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().size()).isEqualTo(2); - assertThat(preprocessed.getHeaders()).containsEntry("a", Arrays.asList("alpha")); - assertThat(preprocessed.getHeaders()).containsEntry("Host", Arrays.asList("localhost")); - } - - @Test - public void modifyResponseHeaders() { - OperationResponse response = createResponse(); - OperationResponse preprocessed = this.preprocessor.preprocess(response); - assertThat(preprocessed.getHeaders().size()).isEqualTo(1); - assertThat(preprocessed.getHeaders()).containsEntry("a", Arrays.asList("alpha")); - } - - @Test - public void modifyWithPattern() { - OperationResponse response = createResponse("content-length", "1234"); - HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter("co.*le(.)gth]")); - OperationResponse preprocessed = processor.preprocess(response); - assertThat(preprocessed.getHeaders().size()).isEqualTo(2); - assertThat(preprocessed.getHeaders()).containsEntry("a", Arrays.asList("alpha")); - assertThat(preprocessed.getHeaders()).containsEntry("b", Arrays.asList("bravo", "banana")); - } - - @Test - public void removeAllHeaders() { - HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter(".*")); - OperationResponse preprocessed = processor.preprocess(createResponse()); - assertThat(preprocessed.getHeaders().size()).isEqualTo(0); - } - - private OperationResponse createResponse(String... extraHeaders) { - return this.responseFactory.create(HttpStatus.OK.value(), getHttpHeaders(extraHeaders), new byte[0]); - } - - private HttpHeaders getHttpHeaders(String... extraHeaders) { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("a", "alpha"); - httpHeaders.add("b", "bravo"); - httpHeaders.add("b", "banana"); - for (int i = 0; i < extraHeaders.length; i += 2) { - httpHeaders.add(extraHeaders[i], extraHeaders[i + 1]); - } - return httpHeaders; - } - -} 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 78129895..ad55d141 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 @@ -19,7 +19,8 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; -import java.util.regex.Pattern; +import java.util.List; +import java.util.function.Consumer; import org.junit.Test; @@ -38,6 +39,7 @@ * Tests for {@link HeadersModifyingOperationPreprocessor}. * * @author Jihoon Cha + * @author Andy Wilkinson */ public class HeadersModifyingOperationPreprocessorTests { @@ -45,110 +47,120 @@ public class HeadersModifyingOperationPreprocessorTests { @Test public void addNewHeader() { - HttpHeaders headers = new HttpHeaders(); - OperationPreprocessor preprocessor = this.preprocessor.add("a", "alpha"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + this.preprocessor.add("a", "alpha"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).containsEntry("a", Arrays.asList("alpha")); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).containsEntry("a", Arrays.asList("alpha")); } @Test public void addValueToExistingHeader() { - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "apple"); - OperationPreprocessor preprocessor = this.preprocessor.add("a", "alpha"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", - Arrays.asList("apple", "alpha")); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", - Arrays.asList("apple", "alpha")); + 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")); } @Test public void setNewHeader() { - HttpHeaders headers = new HttpHeaders(); - OperationPreprocessor preprocessor = this.preprocessor.set("a", "alpha", "avocado"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", + this.preprocessor.set("a", "alpha", "avocado"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).containsEntry("a", Arrays.asList("alpha", "avocado")); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).containsEntry("a", Arrays.asList("alpha", "avocado")); } @Test public void setExistingHeader() { - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "apple"); - OperationPreprocessor preprocessor = this.preprocessor.set("a", "alpha", "avocado"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", - Arrays.asList("alpha", "avocado")); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", - Arrays.asList("alpha", "avocado")); + 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")); } @Test public void removeNonExistentHeader() { - HttpHeaders headers = new HttpHeaders(); - OperationPreprocessor preprocessor = this.preprocessor.remove("a"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + this.preprocessor.remove("a"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).doesNotContainKey("a"); } @Test public void removeHeader() { - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "apple"); OperationPreprocessor preprocessor = this.preprocessor.remove("a"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) + .doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) + .doesNotContainKey("a"); } @Test public void removeHeaderValueForNonExistentHeader() { - HttpHeaders headers = new HttpHeaders(); - OperationPreprocessor preprocessor = this.preprocessor.remove("a", "apple"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).doesNotContainKey("a"); } @Test public void removeHeaderValueWithMultipleValues() { - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "apple"); - headers.add("a", "alpha"); - OperationPreprocessor preprocessor = this.preprocessor.remove("a", "apple"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).containsEntry("a", - Arrays.asList("alpha")); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).containsEntry("a", - Arrays.asList("alpha")); + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor + .preprocess(createRequest((headers) -> headers.addAll("a", List.of("apple", "alpha")))).getHeaders()) + .containsEntry("a", Arrays.asList("alpha")); + assertThat(this.preprocessor + .preprocess(createResponse((headers) -> headers.addAll("a", List.of("apple", "alpha")))).getHeaders()) + .containsEntry("a", Arrays.asList("alpha")); } @Test public void removeHeaderValueWithSingleValueRemovesEntryEntirely() { - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "apple"); - OperationPreprocessor preprocessor = this.preprocessor.remove("a", "apple"); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders()).doesNotContainKey("a"); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders()).doesNotContainKey("a"); + 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"); } @Test public void removeHeadersByNamePattern() { - HttpHeaders headers = new HttpHeaders(); - headers.add("apple", "apple"); - headers.add("alpha", "alpha"); - headers.add("avocado", "avocado"); - headers.add("bravo", "bravo"); - OperationPreprocessor preprocessor = this.preprocessor.remove(Pattern.compile("^a.*")); - assertThat(preprocessor.preprocess(createRequest(headers)).getHeaders().size()).isEqualTo(2); - assertThat(preprocessor.preprocess(createResponse(headers)).getHeaders().size()).isEqualTo(1); + Consumer headersCustomizer = (headers) -> { + headers.add("apple", "apple"); + headers.add("alpha", "alpha"); + headers.add("avocado", "avocado"); + 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"); + } + + private OperationRequest createRequest() { + return createRequest(null); } - private OperationRequest createRequest(HttpHeaders headers) { + private OperationRequest createRequest(Consumer headersCustomizer) { + HttpHeaders headers = new HttpHeaders(); + if (headersCustomizer != null) { + headersCustomizer.accept(headers); + } return new OperationRequestFactory().create(URI.create("http://localhost:8080"), HttpMethod.GET, new byte[0], headers, new Parameters(), Collections.emptyList()); } - private OperationResponse createResponse(HttpHeaders headers) { + private OperationResponse createResponse() { + return createResponse(null); + } + + private OperationResponse createResponse(Consumer headersCustomizer) { + HttpHeaders headers = new HttpHeaders(); + if (headersCustomizer != null) { + headersCustomizer.accept(headers); + } return new OperationResponseFactory().create(HttpStatus.OK.value(), headers, new byte[0]); } From c549837a38b171c78e4127a0c9662bfa8672d3c5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 14:09:34 +0100 Subject: [PATCH 042/198] Upgrade to Spring Framework 6.0.0-M4 Closes gh-794 --- build.gradle | 2 +- samples/junit5/build.gradle | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- samples/web-test-client/build.gradle | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index fb2bc10d..b57f6f19 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ nohttp { } ext { - springFrameworkVersion = "6.0.0-SNAPSHOT" + springFrameworkVersion = "6.0.0-M4" javadocLinks = [ "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", "https://docs.jboss.org/hibernate/validator/7.0/api/", diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index 996f8639..97505238 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -32,7 +32,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation 'org.springframework:spring-webmvc' testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 5f9dce85..57911822 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -31,7 +31,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation 'io.projectreactor.netty:reactor-netty-http:1.0.15' implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index ca92d07e..a5078913 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { - implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 680df8fa..1a272b40 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -20,7 +20,7 @@ org.springframework spring-framework-bom - 6.0.0-SNAPSHOT + 6.0.0-M4 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 7029d45e..d19bfaf6 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -31,7 +31,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index c347521a..ef4f6153 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -32,7 +32,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation "org.springframework:spring-webmvc" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index 65c99a5c..bd96e7f0 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-SNAPSHOT") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' From 156f6d21bb25f2489384816aad763f680ecf7882 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 14:20:36 +0100 Subject: [PATCH 043/198] Upgrade samples to Spring Data 2022.0.0-M4 Closes gh-795 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index a5078913..85c02359 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -27,7 +27,7 @@ ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 1a272b40..d4134466 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -27,7 +27,7 @@ org.springframework.data spring-data-bom - 2022.0.0-SNAPSHOT + 2022.0.0-M4 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index d19bfaf6..a7672e02 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -32,7 +32,7 @@ dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-SNAPSHOT") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" From 0c0bca709deac2b755b105950911eb568ea3a4b6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 14:21:06 +0100 Subject: [PATCH 044/198] Upgrade samples to Spring HATEOAS 2.0.0-M3 Closes gh-798 --- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index a7672e02..21c9facc 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -39,7 +39,7 @@ dependencies { implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" - implementation "org.springframework.hateoas:spring-hateoas:2.0.0-SNAPSHOT" + implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M3" runtimeOnly 'com.h2database:h2:2.0.206' runtimeOnly 'org.atteo:evo-inflector:1.2.1' From ff9f00d313176fe8600f314ad4448abe89cb8e14 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 15:02:02 +0100 Subject: [PATCH 045/198] Upgrade to Jackson 2.13.3 Closes gh-802 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- spring-restdocs-platform/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 85c02359..28ece4af 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -28,7 +28,7 @@ ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") - implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" + implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index d4134466..bf89f9c0 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -38,7 +38,7 @@ com.fasterxml.jackson.core jackson-databind - 2.13.1 + 2.13.3 jakarta.servlet diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 21c9facc..8691b745 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -33,7 +33,7 @@ dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") - implementation "com.fasterxml.jackson.core:jackson-databind:2.13.1" + implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index f99a1681..4f3d9707 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -25,6 +25,6 @@ dependencies { api("org.junit.jupiter:junit-jupiter-api:5.0.0") api("org.mockito:mockito-core:4.2.0") } - api(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.13.1")) + api(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.13.3")) api(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) } From 03e7004a71e1594dbd594d51f83feb7b2980e403 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 15:06:50 +0100 Subject: [PATCH 046/198] Upgrade samples to H2 2.1.210 Closes gh-803 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 28ece4af..65f012bc 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -36,7 +36,7 @@ dependencies { implementation "org.springframework.data:spring-data-jpa" implementation "org.springframework.data:spring-data-rest-webmvc" - runtimeOnly 'com.h2database:h2:2.0.206' + runtimeOnly 'com.h2database:h2:2.1.210' runtimeOnly 'org.atteo:evo-inflector:1.2.1' testImplementation 'com.jayway.jsonpath:json-path:2.6.0' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index bf89f9c0..fd5f20f3 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -71,7 +71,7 @@ com.h2database h2 - 2.0.206 + 2.1.210 runtime diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 8691b745..6a87350f 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -41,7 +41,7 @@ dependencies { implementation "org.springframework.data:spring-data-jpa" implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M3" - runtimeOnly 'com.h2database:h2:2.0.206' + runtimeOnly 'com.h2database:h2:2.1.210' runtimeOnly 'org.atteo:evo-inflector:1.2.1' testImplementation 'com.jayway.jsonpath:json-path:2.6.0' From 23537a21715bc714260cc380d50ea32d5f17b9ab Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 15:09:11 +0100 Subject: [PATCH 047/198] Upgrade to Hibernate Validator 7.0.4.Final Closes gh-804 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- spring-restdocs-platform/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 65f012bc..94513efe 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" - implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" + implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index fd5f20f3..43a6c0cb 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -48,7 +48,7 @@ org.hibernate.validator hibernate-validator - 7.0.0.Final + 7.0.4.Final org.hibernate diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 6a87350f..52d0206e 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -35,7 +35,7 @@ dependencies { implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" - implementation "org.hibernate.validator:hibernate-validator:7.0.0.Final" + implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 4f3d9707..47fd83e6 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -19,7 +19,7 @@ dependencies { api("org.assertj:assertj-core:3.11.1") api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") - api("org.hibernate.validator:hibernate-validator:7.0.0.Final") + api("org.hibernate.validator:hibernate-validator:7.0.4.Final") api("org.javamoney:moneta:1.1") api("org.jruby:jruby-complete:9.1.13.0") api("org.junit.jupiter:junit-jupiter-api:5.0.0") From 24378bbf8e2a82fb22d0550c9d70bf1281fcc4a8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 15:10:51 +0100 Subject: [PATCH 048/198] Upgrade to REST Assured 5.0.1 Closes gh-805 --- samples/rest-assured/build.gradle | 2 +- spring-restdocs-platform/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 57911822..f01476be 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'org.springframework:spring-webflux' - testImplementation 'io.rest-assured:rest-assured:5.0.0' + testImplementation 'io.rest-assured:rest-assured:5.0.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' testImplementation "org.springframework.restdocs:spring-restdocs-restassured:$restdocsVersion" testImplementation 'org.springframework:spring-test' diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 47fd83e6..4f3bb67e 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -12,7 +12,7 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api:5.0.0") api("jakarta.validation:jakarta.validation-api:3.0.0") api("junit:junit:4.12") - api("io.rest-assured:rest-assured:5.0.0") + api("io.rest-assured:rest-assured:5.0.1") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.2") api("org.asciidoctor:asciidoctorj-pdf:1.6.0") From ee4616c522945faef074f3f46879131af1772923 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 15:12:31 +0100 Subject: [PATCH 049/198] Upgrade samples to Hibernate 5.6.9.Final Closes gh-806 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 94513efe..1d98a1e9 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -31,7 +31,7 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" - implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" + implementation "org.hibernate:hibernate-core-jakarta:5.6.9.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" implementation "org.springframework.data:spring-data-rest-webmvc" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 43a6c0cb..7e012c9f 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -53,7 +53,7 @@ org.hibernate hibernate-core-jakarta - 5.5.7.Final + 5.6.9.Final org.springframework diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 52d0206e..9ae2cd05 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -36,7 +36,7 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" - implementation "org.hibernate:hibernate-core-jakarta:5.5.7.Final" + implementation "org.hibernate:hibernate-core-jakarta:5.6.9.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M3" From 2a5ff22494c72e7f240a676540ae1b98a6a213df Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 May 2022 17:23:44 +0100 Subject: [PATCH 050/198] Upgrade Slate sample to Slate 2.13.0 Closes gh-807 --- samples/rest-notes-slate/slate/CHANGELOG.md | 13 +++++++++++++ samples/rest-notes-slate/slate/Dockerfile | 3 --- samples/rest-notes-slate/slate/Gemfile | 4 ++-- samples/rest-notes-slate/slate/Gemfile.lock | 12 ++++++------ samples/rest-notes-slate/slate/Vagrantfile | 4 ++-- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md index 568509d6..6e7e39df 100644 --- a/samples/rest-notes-slate/slate/CHANGELOG.md +++ b/samples/rest-notes-slate/slate/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Version 2.13.0 + +*April 22, 2022* + +* __Drop support for ruby 2.5__ +* Bump rouge from 3.26.1 to 3.28.0 +* Formally support ruby 3.1 +* Bump nokogiri from 1.12.5 to 1.13.4 +* Build docker images for multiple architectures (e.g. `aarch64`) +* Remove `VOLUME` declaration from Dockerfile (thanks @aemengo) + +The security vulnerabilities reported against recent versions of nokogiri should not affect slate users with a regular setup. + ## Version 2.12.0 *November 04, 2021* diff --git a/samples/rest-notes-slate/slate/Dockerfile b/samples/rest-notes-slate/slate/Dockerfile index 504da654..077ef3b1 100644 --- a/samples/rest-notes-slate/slate/Dockerfile +++ b/samples/rest-notes-slate/slate/Dockerfile @@ -2,9 +2,6 @@ FROM ruby:2.6-slim WORKDIR /srv/slate -VOLUME /srv/slate/build -VOLUME /srv/slate/source - EXPOSE 4567 COPY Gemfile . diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile index 3b8c130d..7604fd42 100644 --- a/samples/rest-notes-slate/slate/Gemfile +++ b/samples/rest-notes-slate/slate/Gemfile @@ -1,4 +1,4 @@ -ruby '>= 2.5' +ruby '>= 2.6' source 'https://rubygems.org' # Middleman @@ -8,6 +8,6 @@ gem 'middleman-autoprefixer', '~> 3.0' gem 'middleman-sprockets', '~> 4.1' gem 'rouge', '~> 3.21' gem 'redcarpet', '~> 3.5.0' -gem 'nokogiri', '~> 1.12.1' +gem 'nokogiri', '~> 1.13.3' gem 'sass' gem 'webrick' diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock index 18702217..bd60f7f6 100644 --- a/samples/rest-notes-slate/slate/Gemfile.lock +++ b/samples/rest-notes-slate/slate/Gemfile.lock @@ -80,10 +80,10 @@ GEM middleman-syntax (3.2.0) middleman-core (>= 3.2) rouge (~> 3.2) - mini_portile2 (2.6.1) + mini_portile2 (2.8.0) minitest (5.14.4) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + nokogiri (1.13.4) + mini_portile2 (~> 2.8.0) racc (~> 1.4) padrino-helpers (0.15.1) i18n (>= 0.6.7, < 2) @@ -93,14 +93,14 @@ GEM parallel (1.21.0) parslet (2.0.0) public_suffix (4.0.6) - racc (1.5.2) + racc (1.6.0) rack (2.2.3) rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) redcarpet (3.5.1) rexml (3.2.5) - rouge (3.26.1) + rouge (3.28.0) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -132,7 +132,7 @@ DEPENDENCIES middleman-autoprefixer (~> 3.0) middleman-sprockets (~> 4.1) middleman-syntax (~> 3.2) - nokogiri (~> 1.12.1) + nokogiri (~> 1.13.3) redcarpet (~> 3.5.0) rouge (~> 3.21) sass diff --git a/samples/rest-notes-slate/slate/Vagrantfile b/samples/rest-notes-slate/slate/Vagrantfile index 200839d0..769b8a5b 100644 --- a/samples/rest-notes-slate/slate/Vagrantfile +++ b/samples/rest-notes-slate/slate/Vagrantfile @@ -1,5 +1,5 @@ Vagrant.configure(2) do |config| - config.vm.box = "ubuntu/bionic64" + config.vm.box = "ubuntu/focal64" config.vm.network :forwarded_port, guest: 4567, host: 4567 config.vm.provider "virtualbox" do |vb| vb.memory = "2048" @@ -28,7 +28,7 @@ Vagrant.configure(2) do |config| echo "==============================================" echo "Installing app dependencies" cd /vagrant - sudo gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)" + sudo gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)" bundle config build.nokogiri --use-system-libraries bundle install SHELL From b88ee737c662896c4114ee464f66063ee9a82b82 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Jun 2022 16:43:29 +0100 Subject: [PATCH 051/198] Upgrade CI image to Java 17.0.3.1+2 Closes gh-817 --- ci/images/get-jdk-url.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index a3babd3a..90744442 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,7 +3,7 @@ set -e case "$1" in java17) - echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17%2B35/OpenJDK17-jdk_x64_linux_hotspot_17_35.tar.gz" + 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" From cee26fdb475d9897254b35a6bdc0b71279bd7cd1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 Jul 2022 08:21:47 +0100 Subject: [PATCH 052/198] Upgrade to Spring Framework 6.0.0.M5 Closes gh-822 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b57f6f19..ed97a8eb 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ nohttp { } ext { - springFrameworkVersion = "6.0.0-M4" + springFrameworkVersion = "6.0.0-M5" javadocLinks = [ "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", "https://docs.jboss.org/hibernate/validator/7.0/api/", From 5a7dde16ad1977ba0d2989ded857c3c0ae009396 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 Jul 2022 10:02:16 +0100 Subject: [PATCH 053/198] Add forward merge infrastructure Closes gh-823 --- git/hooks/forward-merge | 135 ++++++++++++++++++++++++++++++++ git/hooks/prepare-forward-merge | 71 +++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100755 git/hooks/forward-merge create mode 100755 git/hooks/prepare-forward-merge diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge new file mode 100755 index 00000000..acbb6008 --- /dev/null +++ b/git/hooks/forward-merge @@ -0,0 +1,135 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +class ForwardMerge + attr_reader :issue, :milestone, :message, :line + def initialize(issue, milestone, message, line) + @issue = issue + @milestone = milestone + @message = message + @line = line + end +end + +def find_forward_merges(message_file) + $log.debug "Searching for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + message = File.read(message_file) + forward_merges = [] + message.each_line do |line| + $log.debug "Checking #{line} for message" + match = /^(?:Fixes|Closes) gh-(\d+) in (\d\.\d\.[\dx](?:[\.\-](?:M|RC)\d)?)$/.match(line) + if match then + issue = match[1] + milestone = match[2] + $log.debug "Matched reference to issue #{issue} in milestone #{milestone}" + forward_merges << ForwardMerge.new(issue, milestone, message, line) + end + end + $log.debug "No match in merge message" unless forward_merges + return forward_merges +end + +def get_issue(username, password, repository, number) + $log.debug "Getting issue #{number} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Get.new(uri.path) + request.basic_auth(username, password) + response = http.request(request) + $log.debug "Get HTTP response #{response.code}" + return JSON.parse(response.body) unless response.code != '200' + puts "Failed to retrieve issue #{number}: #{response.message}" + exit 1 +end + +def find_milestone(username, password, repository, title) + $log.debug "Finding milestone #{title} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/milestones") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Get.new(uri.path) + request.basic_auth(username, password) + response = http.request(request) + milestones = JSON.parse(response.body) + if title.end_with?(".x") + 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.sort_by { |v| Gem::Version.new(v) } + $log.debug "Considering candidates #{titles}" + if(titles.empty?) + puts "Cannot find nearest milestone for prefix #{title}" + exit 1 + end + title = titles.first + $log.debug "Found nearest milestone #{title}" + end + milestones.each do |milestone| + $log.debug "Considering #{milestone['title']}" + return milestone['number'] if milestone['title'] == title + end + puts "Milestone #{title} not found" + exit 1 +end + +def create_issue(username, password, repository, original, title, labels, milestone, milestone_name, dry_run) + $log.debug "Finding forward-merge issue in GitHub repository #{repository} for '#{title}'" + uri = URI("https://api.github.com/repos/#{repository}/issues") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') + request.basic_auth(username, password) + request.body = { + title: title, + labels: labels, + milestone: milestone.to_i, + body: "Forward port of issue ##{original} to #{milestone_name}." + }.to_json + if dry_run then + puts "Dry run" + puts "POSTing to #{uri} with body #{request.body}" + return "dry-run" + end + response = JSON.parse(http.request(request).body) + $log.debug "Created new issue #{response['number']}" + return response['number'] +end + +$log.debug "Running forward-merge hook script" +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" +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' + +forward_merges.each do |forward_merge| + existing_issue = get_issue(username, password, repository, forward_merge.issue) + title = existing_issue['title'] + labels = existing_issue['labels'].map { |label| label['name'] } + labels << "status: forward-port" + $log.debug "Processing issue '#{title}'" + + milestone = find_milestone(username, password, repository, forward_merge.milestone) + new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run) + + puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}" + rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n") + File.write(message_file, rewritten_message) +end diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge new file mode 100755 index 00000000..2b049790 --- /dev/null +++ b/git/hooks/prepare-forward-merge @@ -0,0 +1,71 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$main_branch = "3.0.x" + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +def get_fixed_issues() + $log.debug "Searching for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + fixed = [] + message = `git log -1 --pretty=%B #{rev}` + message.each_line do |line| + $log.debug "Checking #{line} for message" + fixed << line.strip if /^(?:Fixes|Closes) gh-(\d+)/.match(line) + end + $log.debug "Found fixed issues #{fixed}" + return fixed; +end + +def rewrite_message(message_file, fixed) + current_branch = `git rev-parse --abbrev-ref HEAD`.strip + if current_branch == "main" + current_branch = $main_branch + end + rewritten_message = "" + message = File.read(message_file) + message.each_line do |line| + match = /^Merge.*branch\ '(.*)'(?:\ into\ (.*))?$/.match(line) + if match + from_branch = match[1] + if from_branch.include? "/" + from_branch = from_branch.partition("/").last + end + to_branch = match[2] + $log.debug "Rewriting merge message" + line = "Merge branch '#{from_branch}'" + (to_branch ? " into #{to_branch}\n" : "\n") + end + if fixed and line.start_with?("#") + $log.debug "Adding fixed" + rewritten_message << "\n" + fixed.each do |fixes| + rewritten_message << "#{fixes} in #{current_branch}\n" + end + fixed = nil + end + rewritten_message << line + end + return rewritten_message +end + +$log.debug "Running prepare-forward-merge hook script" + +message_file=ARGV[0] +message_type=ARGV[1] + +if message_type != "merge" + $log.debug "Not a merge commit" + exit 0; +end + +$log.debug "Searching for forward merge" +fixed = get_fixed_issues() +rewritten_message = rewrite_message(message_file, fixed) +File.write(message_file, rewritten_message) From fbad11e0a11d3d733e491a5cabfd6be0377b6def Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 Jul 2022 11:05:58 +0100 Subject: [PATCH 054/198] Update documentation examples to use new modifyHeaders preprocessor Closes gh-827 --- .../mockmvc/CustomDefaultOperationPreprocessors.java | 6 +++--- .../java/com/example/mockmvc/EveryTestPreprocessing.java | 6 +++--- .../test/java/com/example/mockmvc/PerTestPreprocessing.java | 6 +++--- .../restassured/CustomDefaultOperationPreprocessors.java | 6 +++--- .../com/example/restassured/EveryTestPreprocessing.java | 6 +++--- .../java/com/example/restassured/PerTestPreprocessing.java | 6 +++--- .../webtestclient/CustomDefaultOperationPreprocessors.java | 6 +++--- .../com/example/webtestclient/EveryTestPreprocessing.java | 6 +++--- .../com/example/webtestclient/PerTestPreprocessing.java | 6 +++--- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java index af890124..cb1871ed 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-2021 the original author or authors. + * 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. @@ -25,8 +25,8 @@ import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; public class CustomDefaultOperationPreprocessors { @@ -43,7 +43,7 @@ public void setup() { // tag::custom-default-operation-preprocessors[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); // end::custom-default-operation-preprocessors[] diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 09bd27bb..d29e4cbe 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-2021 the original author or authors. + * 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. @@ -29,8 +29,8 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class EveryTestPreprocessing { @@ -47,7 +47,7 @@ public class EveryTestPreprocessing { public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); } diff --git a/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java index c39db4db..3db99fcb 100644 --- a/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * 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. @@ -20,10 +20,10 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class PerTestPreprocessing { @@ -33,7 +33,7 @@ public class PerTestPreprocessing { public void general() throws Exception { // tag::preprocessing[] this.mockMvc.perform(get("/")).andExpect(status().isOk()) - .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + .andDo(document("index", preprocessRequest(modifyHeaders().remove("Foo")), // <1> preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java index cb392c91..5776929f 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-2021 the original author or authors. + * 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. @@ -23,8 +23,8 @@ import org.springframework.restdocs.JUnitRestDocumentation; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultOperationPreprocessors { @@ -40,7 +40,7 @@ public void setup() { // tag::custom-default-operation-preprocessors[] this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); // end::custom-default-operation-preprocessors[] diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index 6757da31..e34ba255 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-2021 the original author or authors. + * 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. @@ -27,8 +27,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; @@ -44,7 +44,7 @@ public class EveryTestPreprocessing { public void setup() { this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); } diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java index 02974491..7e19d8be 100644 --- a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * 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. @@ -20,10 +20,10 @@ import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class PerTestPreprocessing { @@ -32,7 +32,7 @@ public class PerTestPreprocessing { public void general() throws Exception { // tag::preprocessing[] - RestAssured.given(this.spec).filter(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + RestAssured.given(this.spec).filter(document("index", preprocessRequest(modifyHeaders().remove("Foo")), // <1> preprocessResponse(prettyPrint()))) // <2> .when().get("/").then().assertThat().statusCode(is(200)); // end::preprocessing[] diff --git a/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java index d1688d53..cb6d6974 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-2019 the original author or authors. + * 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. @@ -23,8 +23,8 @@ import org.springframework.restdocs.JUnitRestDocumentation; 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.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; public class CustomDefaultOperationPreprocessors { @@ -46,7 +46,7 @@ public void setup() { .configureClient() .filter(documentationConfiguration(this.restDocumentation) .operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); // end::custom-default-operation-preprocessors[] diff --git a/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java b/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java index 4b08af95..9b6cc294 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-2019 the original author or authors. + * 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. @@ -25,8 +25,8 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; @@ -48,7 +48,7 @@ public void setup() { .configureClient() .filter(documentationConfiguration(this.restDocumentation) .operationPreprocessors() - .withRequestDefaults(removeHeaders("Foo")) // <1> + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> .withResponseDefaults(prettyPrint())) // <2> .build(); } diff --git a/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java b/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java index f6d45f18..cd1254ef 100644 --- a/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * 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. @@ -18,10 +18,10 @@ 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.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; public class PerTestPreprocessing { @@ -34,7 +34,7 @@ public void general() { // tag::preprocessing[] this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody() .consumeWith(document("index", - preprocessRequest(removeHeaders("Foo")), // <1> + preprocessRequest(modifyHeaders().remove("Foo")), // <1> preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } From bdba92efe7a5ff902b67dbed7c897ac4a20fb76b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 Jul 2022 15:22:07 +0100 Subject: [PATCH 055/198] Provide a bom that manages all modules Closes gh-831 --- build.gradle | 6 +++--- gradle/publish-maven.gradle | 3 +++ settings.gradle | 1 + spring-restdocs-bom/build.gradle | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 spring-restdocs-bom/build.gradle diff --git a/build.gradle b/build.gradle index a44720bb..0076f29d 100644 --- a/build.gradle +++ b/build.gradle @@ -87,8 +87,6 @@ subprojects { subproject -> } plugins.withType(MavenPublishPlugin) { - subproject.apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" - javadoc { description = "Generates project-level javadoc for use in -javadoc jar" options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED @@ -107,7 +105,9 @@ subprojects { subproject -> } } } - + plugins.withType(MavenPublishPlugin) { + subproject.apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + } tasks.withType(GenerateModuleMetadata) { enabled = false } diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle index c04bca25..3c50ccb7 100644 --- a/gradle/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -21,6 +21,9 @@ plugins.withType(MavenPublishPlugin) { } } } + project.plugins.withType(JavaPlatformPlugin) { + from components.javaPlatform + } pom { name = project.provider { project.description } description = project.provider { project.description } diff --git a/settings.gradle b/settings.gradle index d680481f..071d415d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -35,6 +35,7 @@ settings.gradle.projectsLoaded { include "docs" include "spring-restdocs-asciidoctor" +include "spring-restdocs-bom" include "spring-restdocs-core" include "spring-restdocs-mockmvc" include "spring-restdocs-platform" diff --git a/spring-restdocs-bom/build.gradle b/spring-restdocs-bom/build.gradle new file mode 100644 index 00000000..bd0b4ce5 --- /dev/null +++ b/spring-restdocs-bom/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "java-platform" + id "maven-publish" +} + +description = "Spring REST Docs Bill of Materials" + +dependencies { + constraints { + api(project(":spring-restdocs-asciidoctor")) + api(project(":spring-restdocs-core")) + api(project(":spring-restdocs-mockmvc")) + api(project(":spring-restdocs-restassured")) + api(project(":spring-restdocs-webtestclient")) + } +} \ No newline at end of file From 9a44205253d1cc87a217858f2008e8ee7ba88568 Mon Sep 17 00:00:00 2001 From: Achim Grimm Date: Sun, 24 Apr 2022 21:33:00 +0200 Subject: [PATCH 056/198] Specify language derived from Content-Type in body snippets See gh-797 --- .../restdocs/payload/AbstractBodySnippet.java | 6 ++++++ .../asciidoctor/default-request-body.snippet | 2 +- .../asciidoctor/default-response-body.snippet | 2 +- .../markdown/default-request-body.snippet | 2 +- .../markdown/default-response-body.snippet | 2 +- .../payload/RequestBodySnippetTests.java | 16 ++++++++++++++++ .../payload/ResponseBodySnippetTests.java | 16 ++++++++++++++++ 7 files changed, 42 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java index f9895e89..dfb1d963 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java @@ -73,6 +73,11 @@ protected AbstractBodySnippet(String name, String type, PayloadSubsectionExtract protected Map createModel(Operation operation) { try { MediaType contentType = getContentType(operation); + String language = null; + if (contentType != null) { + language = (contentType.getSubtypeSuffix() != null) ? contentType.getSubtypeSuffix() + : contentType.getSubtype(); + } byte[] content = getContent(operation); if (this.subsectionExtractor != null) { content = this.subsectionExtractor.extractSubsection(content, contentType); @@ -80,6 +85,7 @@ protected Map createModel(Operation operation) { Charset charset = extractCharset(contentType); String body = (charset != null) ? new String(content, charset) : new String(content); Map model = new HashMap<>(); + model.put("language", language); model.put("body", body); return model; } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet index e6cb8e9b..00da2d08 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet @@ -1,4 +1,4 @@ -[source,options="nowrap"] +[source{{#language}},{{language}}{{/language}},options="nowrap"] ---- {{body}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet index e6cb8e9b..00da2d08 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet @@ -1,4 +1,4 @@ -[source,options="nowrap"] +[source{{#language}},{{language}}{{/language}},options="nowrap"] ---- {{body}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet index b897d409..6abf3f7e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet @@ -1,3 +1,3 @@ -``` +```{{#language}}{{language}}{{/language}} {{body}} ``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet index b897d409..6abf3f7e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet @@ -1,3 +1,3 @@ -``` +```{{#language}}{{language}}{{/language}} {{body}} ``` \ No newline at end of file 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 87e35e55..07bc6242 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 @@ -20,6 +20,8 @@ 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; @@ -58,6 +60,20 @@ public void requestWithNoBody() throws IOException { assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock(null, "nowrap").withContent("")); } + @Test + public void requestWithMediaTypeJson() throws IOException { + requestBody().document(this.operationBuilder.request("http://localhost") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build()); + assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("json", "nowrap").withContent("")); + } + + @Test + public void requestWithMediaTypeXml() throws IOException { + requestBody().document(this.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("")); + } + @Test public void subsectionOfRequestBody() throws IOException { requestBody(beneathPath("a.b")).document( 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 078fe097..8955a004 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 @@ -20,6 +20,8 @@ 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; @@ -58,6 +60,20 @@ public void responseWithNoBody() throws IOException { assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock(null, "nowrap").withContent("")); } + @Test + public void responseWithMediaTypeJson() 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 responseWithMediaTypeXml() throws IOException { + new ResponseBodySnippet().document(this.operationBuilder.response() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE).build()); + assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("xml", "nowrap").withContent("")); + } + @Test public void subsectionOfResponseBody() throws IOException { responseBody(beneathPath("a.b")) From aa9cef9f02889443fdd91689c0bb808c1f06c56a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 Jul 2022 15:47:31 +0100 Subject: [PATCH 057/198] Polish "Specify language derived from Content-Type in body snippets" See gh-797 --- .../restdocs/payload/AbstractBodySnippet.java | 16 +++++---- .../default-request-part-body.snippet | 2 +- .../default-request-part-body.snippet | 2 +- .../payload/RequestBodyPartSnippetTests.java | 36 ++++++++++++++++++- .../payload/RequestBodySnippetTests.java | 20 +++++++++-- .../payload/ResponseBodySnippetTests.java | 20 +++++++++-- 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java index dfb1d963..3ccd5688 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -31,6 +31,7 @@ * document a RESTful resource's request or response body. * * @author Andy Wilkinson + * @author Achim Grimm */ public abstract class AbstractBodySnippet extends TemplatedSnippet { @@ -73,11 +74,7 @@ protected AbstractBodySnippet(String name, String type, PayloadSubsectionExtract protected Map createModel(Operation operation) { try { MediaType contentType = getContentType(operation); - String language = null; - if (contentType != null) { - language = (contentType.getSubtypeSuffix() != null) ? contentType.getSubtypeSuffix() - : contentType.getSubtype(); - } + String language = determineLanguage(contentType); byte[] content = getContent(operation); if (this.subsectionExtractor != null) { content = this.subsectionExtractor.extractSubsection(content, contentType); @@ -94,6 +91,13 @@ protected Map createModel(Operation operation) { } } + private String determineLanguage(MediaType contentType) { + if (contentType == null) { + return null; + } + return (contentType.getSubtypeSuffix() != null) ? contentType.getSubtypeSuffix() : contentType.getSubtype(); + } + private Charset extractCharset(MediaType contentType) { if (contentType == null) { return null; diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet index e6cb8e9b..00da2d08 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet @@ -1,4 +1,4 @@ -[source,options="nowrap"] +[source{{#language}},{{language}}{{/language}},options="nowrap"] ---- {{body}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet index b897d409..6abf3f7e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet @@ -1,3 +1,3 @@ -``` +```{{#language}}{{language}}{{/language}} {{body}} ``` \ No newline at end of file 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 85a686d9..b88288ee 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-2019 the original author or authors. + * 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. @@ -20,6 +20,8 @@ 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; @@ -61,6 +63,38 @@ public void requestPartWithNoBody() throws IOException { .is(codeBlock(null, "nowrap").withContent("")); } + @Test + public void requestPartWithJsonMediaType() throws IOException { + requestPartBody("one").document(this.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("")); + } + + @Test + public void requestPartWithJsonSubtypeMediaType() throws IOException { + requestPartBody("one").document(this.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("")); + } + + @Test + public void requestPartWithXmlMediaType() throws IOException { + requestPartBody("one").document(this.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("")); + } + + @Test + public void requestPartWithXmlSubtypeMediaType() throws IOException { + requestPartBody("one").document(this.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("")); + } + @Test public void subsectionOfRequestPartBody() throws IOException { requestPartBody("one", beneathPath("a.b")).document(this.operationBuilder.request("http://localhost") 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 07bc6242..622bef5e 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-2019 the original author or authors. + * 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. @@ -61,14 +61,28 @@ public void requestWithNoBody() throws IOException { } @Test - public void requestWithMediaTypeJson() throws IOException { + public void requestWithJsonMediaType() throws IOException { requestBody().document(this.operationBuilder.request("http://localhost") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build()); assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("json", "nowrap").withContent("")); } @Test - public void requestWithMediaTypeXml() throws IOException { + public void requestWithJsonSubtypeMediaType() throws IOException { + requestBody().document(this.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("")); + } + + @Test + public void requestWithXmlMediaType() throws IOException { + requestBody().document(this.operationBuilder.request("http://localhost") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).build()); + assertThat(this.generatedSnippets.snippet("request-body")).is(codeBlock("xml", "nowrap").withContent("")); + } + + @Test + public void requestWithXmlSubtypeMediaType() throws IOException { requestBody().document(this.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("")); 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 8955a004..1b5cd46f 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-2019 the original author or authors. + * 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. @@ -61,14 +61,28 @@ public void responseWithNoBody() throws IOException { } @Test - public void responseWithMediaTypeJson() throws IOException { + 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 responseWithMediaTypeXml() throws IOException { + public void responseWithJsonSubtypeMediaType() throws IOException { + new ResponseBodySnippet().document(this.operationBuilder.response() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE).build()); + assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("json", "nowrap").withContent("")); + } + + @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("")); + } + + @Test + public void responseWithXmlSubtypeMediaType() throws IOException { new ResponseBodySnippet().document(this.operationBuilder.response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE).build()); assertThat(this.generatedSnippets.snippet("response-body")).is(codeBlock("xml", "nowrap").withContent("")); From f72a9f1067eabc7004cfc26b8f25d91a805de0b1 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 7 Mar 2019 14:26:31 +1100 Subject: [PATCH 058/198] Add support for documenting request and response cookies See gh-592 --- config/checkstyle/checkstyle.xml | 3 +- docs/build.gradle | 1 + .../docs/asciidoc/documenting-your-api.adoc | 55 +++++ .../java/com/example/mockmvc/HttpCookies.java | 46 ++++ .../com/example/restassured/HttpCookies.java | 44 ++++ .../example/webtestclient/HttpCookies.java | 47 ++++ .../cookies/AbstractCookiesSnippet.java | 145 +++++++++++ .../restdocs/cookies/CookieDescriptor.java | 69 ++++++ .../restdocs/cookies/CookieDocumentation.java | 232 ++++++++++++++++++ .../cookies/RequestCookiesSnippet.java | 109 ++++++++ .../cookies/ResponseCookiesSnippet.java | 105 ++++++++ .../restdocs/cookies/package-info.java | 20 ++ .../restdocs/operation/OperationResponse.java | 11 + .../operation/OperationResponseFactory.java | 34 ++- .../restdocs/operation/ResponseCookie.java | 57 +++++ .../operation/StandardOperationResponse.java | 14 +- .../UriModifyingOperationPreprocessor.java | 2 +- .../default-request-cookies.snippet | 9 + .../default-response-cookies.snippet | 9 + .../markdown/default-request-cookies.snippet | 5 + .../markdown/default-response-cookies.snippet | 5 + .../RequestCookiesSnippetFailureTests.java | 60 +++++ .../cookies/RequestCookiesSnippetTests.java | 144 +++++++++++ .../ResponseCookiesSnippetFailureTests.java | 62 +++++ .../cookies/ResponseCookiesSnippetTests.java | 141 +++++++++++ .../request-cookies-with-extra-column.snippet | 10 + .../request-cookies-with-title.snippet | 10 + ...response-cookies-with-extra-column.snippet | 10 + .../response-cookies-with-title.snippet | 10 + .../request-cookies-with-extra-column.snippet | 5 + .../request-cookies-with-title.snippet | 6 + ...response-cookies-with-extra-column.snippet | 5 + .../response-cookies-with-title.snippet | 6 + .../testfixtures/GeneratedSnippets.java | 8 + .../testfixtures/OperationBuilder.java | 13 +- .../mockmvc/MockMvcResponseConverter.java | 25 +- .../RestAssuredResponseConverter.java | 24 +- .../WebTestClientResponseConverter.java | 28 ++- 38 files changed, 1572 insertions(+), 17 deletions(-) create mode 100644 docs/src/test/java/com/example/mockmvc/HttpCookies.java create mode 100644 docs/src/test/java/com/example/restassured/HttpCookies.java create mode 100644 docs/src/test/java/com/example/webtestclient/HttpCookies.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 3de6d5ac..612ebd17 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -5,7 +5,8 @@ - + diff --git a/docs/build.gradle b/docs/build.gradle index ce49eb9a..c24ded86 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -21,6 +21,7 @@ dependencies { testImplementation(project(":spring-restdocs-mockmvc")) testImplementation(project(":spring-restdocs-restassured")) 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") diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 5fee0d69..0f267476 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -988,6 +988,61 @@ Each contains a table describing the headers. When documenting HTTP Headers, the test fails if a documented header is not found in the request or response. +[[documenting-your-api-http-cookies]] +=== HTTP Cookies + +You can document the cookies in a request or response by using `requestCookies` and +`responseCookies`, respectively. The following examples show how to do so: + +==== +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/HttpCookies.java[tags=cookies] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on + `org.springframework.restdocs.cookies.CookieDocumentation`. +<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on + `org.springframework.restdocs.cookies.CookieDocumentation. +<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies` + method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on + `org.springframework.restdocs.cookies.CookieDocumentation`. +<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on + `org.springframework.restdocs.cookies.CookieDocumentation. +<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies` + method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on + `org.springframework.restdocs.cookies.CookieDocumentation`. +<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on + `org.springframework.restdocs.cookies.CookieDocumentation. +<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies` + method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`. +==== + +The result is a snippet named `request-cookies.adoc` and a snippet named +`response-cookies.adoc`. Each contains a table describing the cookies. + +When documenting HTTP Cookies, the test fails if a documented cookie is not found in +the request or response. [[documenting-your-api-reusing-snippets]] === Reusing Snippets diff --git a/docs/src/test/java/com/example/mockmvc/HttpCookies.java b/docs/src/test/java/com/example/mockmvc/HttpCookies.java new file mode 100644 index 00000000..aab39db7 --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/HttpCookies.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 jakarta.servlet.http.Cookie; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HttpCookies { + + private MockMvc mockMvc; + + public void cookies() throws Exception { + // tag::cookies[] + this.mockMvc.perform(get("/").cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))) // <1> + .andExpect(status().isOk()).andDo(document("cookies", requestCookies(// <2> + cookieWithName("JSESSIONID").description("Session token")), // <3> + responseCookies(// <4> + cookieWithName("JSESSIONID").description("Updated session token"), + cookieWithName("logged_in") + .description("Set to true if the user is currently logged in")))); + // end::cookies[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/HttpCookies.java b/docs/src/test/java/com/example/restassured/HttpCookies.java new file mode 100644 index 00000000..f6b76293 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpCookies.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2017 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.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class HttpCookies { + + private RequestSpecification spec; + + public void cookies() throws Exception { + // tag::cookies[] + RestAssured.given(this.spec).filter(document("cookies", requestCookies(// <1> + cookieWithName("JSESSIONID").description("Saved session token")), // <2> + responseCookies(// <3> + cookieWithName("logged_in").description("If user is logged in"), + cookieWithName("JSESSIONID").description("Updated session token")))) + .cookie("JSESSIONID", "ACBCDFD0FF93D5BB") // <4> + .when().get("/people").then().assertThat().statusCode(is(200)); + // end::cookies[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/HttpCookies.java b/docs/src/test/java/com/example/webtestclient/HttpCookies.java new file mode 100644 index 00000000..8979520d --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/HttpCookies.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2017 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.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class HttpCookies { + + // @formatter:off + + private WebTestClient webTestClient; + + public void cookies() throws Exception { + // tag::cookies[] + this.webTestClient + .get().uri("/people").cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1> + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("cookies", + requestCookies(// <2> + cookieWithName("JSESSIONID").description("Session token")), // <3> + responseCookies(// <4> + cookieWithName("JSESSIONID") + .description("Updated session token"), + cookieWithName("logged_in") + .description("User is logged in")))); + // end::cookies[] + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java new file mode 100644 index 00000000..865ea397 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java @@ -0,0 +1,145 @@ +/* + * Copyright 2014-2017 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response cookies. + * + * @author Andreas Evers + * @author Clyde Stubbs + * @since 2.1 + */ +public abstract class AbstractCookiesSnippet extends TemplatedSnippet { + + private List cookieDescriptors; + + protected final boolean ignoreUndocumentedCookies; + + private String type; + + /** + * Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named + * {@code -cookies}. The cookies will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. + * @param type the type of the cookies + * @param descriptors the cookie descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected AbstractCookiesSnippet(String type, List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super(type + "-cookies", attributes); + for (CookieDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName(), "The name of the cookie must not be null"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), "The description of the cookie must not be null"); + } + } + this.cookieDescriptors = descriptors; + this.type = type; + this.ignoreUndocumentedCookies = ignoreUndocumentedCookies; + } + + @Override + protected Map createModel(Operation operation) { + validateCookieDocumentation(operation); + + Map model = new HashMap<>(); + List> cookies = new ArrayList<>(); + model.put("cookies", cookies); + for (CookieDescriptor descriptor : this.cookieDescriptors) { + cookies.add(createModelForDescriptor(descriptor)); + } + return model; + } + + private void validateCookieDocumentation(Operation operation) { + List missingCookies = findMissingCookies(operation); + if (!missingCookies.isEmpty()) { + List names = new ArrayList<>(); + for (CookieDescriptor cookieDescriptor : missingCookies) { + names.add(cookieDescriptor.getName()); + } + throw new SnippetException( + "Cookies with the following names were not found" + " in the " + this.type + ": " + names); + } + } + + /** + * Finds the cookies that are missing from the operation. A cookie is missing if it is + * described by one of the {@code cookieDescriptors} but is not present in the + * operation. + * @param operation the operation + * @return descriptors for the cookies that are missing from the operation + */ + protected List findMissingCookies(Operation operation) { + List missingCookies = new ArrayList<>(); + Set actualCookies = extractActualCookies(operation); + for (CookieDescriptor cookieDescriptor : this.cookieDescriptors) { + if (!cookieDescriptor.isOptional() && !actualCookies.contains(cookieDescriptor.getName())) { + missingCookies.add(cookieDescriptor); + } + } + + return missingCookies; + } + + /** + * Extracts the names of the cookies from the request or response of the given + * {@code operation}. + * @param operation the operation + * @return the cookie names + */ + protected abstract Set extractActualCookies(Operation operation); + + /** + * Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to + * generate the documentation. + * @return the cookie descriptors + */ + protected final List getCookieDescriptors() { + return this.cookieDescriptors; + } + + /** + * Returns a model for the given {@code descriptor}. + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(CookieDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java new file mode 100644 index 00000000..51a4787a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014-2015 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 org.springframework.restdocs.snippet.IgnorableDescriptor; + +/** + * A description of a cookie found in a request or response. + * + * @author Andreas Evers + * @author Clyde Stubbs + * @since 2.1 + * @see CookieDocumentation#cookieWithName(String) + */ +public class CookieDescriptor extends IgnorableDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code CookieDescriptor} describing the cookie with the given + * {@code name}. + * @param name the name + */ + protected CookieDescriptor(String name) { + this.name = name; + } + + /** + * Marks the cookie as optional. + * @return {@code this} + */ + public final CookieDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name for the cookie. + * @return the cookie name + */ + public final String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described cookie is optional, otherwise {@code false}. + * @return {@code true} if the described cookie is optional, otherwise {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java new file mode 100644 index 00000000..0a88d13a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java @@ -0,0 +1,232 @@ +/* + * Copyright 2014-2016 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.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API's request and response cookies. + * + * @author Andreas Evers + * @author Andy Wilkinson + * @author Marcel Overdijk + * @author Clyde Stubbs + * @since 2.1 + */ +public abstract class CookieDocumentation { + + private CookieDocumentation() { + + } + + /** + * Creates a {@code CookieDescriptor} that describes a cookie with the given + * {@code name}. + * @param name the name of the cookie + * @return a {@code CookieDescriptor} ready for further configuration + */ + public static CookieDescriptor cookieWithName(String name) { + return new CookieDescriptor(name); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(CookieDescriptor... descriptors) { + return requestCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(List descriptors) { + return new RequestCookiesSnippet(descriptors); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(Map attributes, + CookieDescriptor... descriptors) { + return requestCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any cookies present in the request that are not + * documented will result in an error. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(Map attributes, + List descriptors) { + return new RequestCookiesSnippet(descriptors, attributes, false); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. An undocumented cookie in the request will not + * generate an error. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(Map attributes, + List descriptors) { + return new RequestCookiesSnippet(descriptors, attributes, true); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional or ignored, and is not present + * in the request, a failure will occur. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(CookieDescriptor... descriptors) { + return responseCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional or ignored, and is not present + * in the request, a failure will occur. If a cookie is present in the response but is + * undocumented a failure will occur. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(List descriptors) { + return new ResponseCookiesSnippet(descriptors); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional or ignored, and is not present + * in the request, a failure will occur. No failure will occur if a cookie is present + * but undocumented. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(List descriptors) { + return new ResponseCookiesSnippet(descriptors, null, true); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a cookie is present in the response but is + * undocumented a failure will occur. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(Map attributes, + CookieDescriptor... descriptors) { + return responseCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a cookie is present in the response but is + * undocumented a failure will occur. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(Map attributes, + List descriptors) { + return new ResponseCookiesSnippet(descriptors, attributes, false); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. No failure will occur if a cookie is present but + * undocumented. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(Map attributes, + List descriptors) { + return new ResponseCookiesSnippet(descriptors, attributes, true); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java new file mode 100644 index 00000000..1a8d7ccc --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2016 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.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.RequestCookie; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the cookies in a request. + * + * @author Andreas Evers + * @author Andy Wilkinson + * @author Clyde Stubbs + * @since 2.1 + * @see CookieDocumentation#requestCookies(CookieDescriptor...) + * @see CookieDocumentation#requestCookies(Map, CookieDescriptor...) + */ +public class RequestCookiesSnippet extends AbstractCookiesSnippet { + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. + * @param descriptors the descriptors + */ + protected RequestCookiesSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies if set undocumented cookies will be ignored + */ + protected RequestCookiesSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super("request", descriptors, attributes, ignoreUndocumentedCookies); + } + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented cookies will not be + * ignored. + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestCookiesSnippet(List descriptors, Map attributes) { + super("request", descriptors, attributes, false); + } + + @Override + protected Set extractActualCookies(Operation operation) { + HashSet actualCookies = new HashSet<>(); + for (RequestCookie cookie : operation.getRequest().getCookies()) { + actualCookies.add(cookie.getName()); + } + return actualCookies; + } + + /** + * Returns a new {@code RequestCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestCookiesSnippet and(CookieDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestCookiesSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); + return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), false); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java new file mode 100644 index 00000000..99076115 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014-2016 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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.ResponseCookie; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the cookies in a response. + * + * @author Andreas Evers + * @author Andy Wilkinson + * @author Clyde Stubbs + * @since 2.1 + * @see CookieDocumentation#responseCookies(CookieDescriptor...) + * @see CookieDocumentation#responseCookies(Map, CookieDescriptor...) + */ +public class ResponseCookiesSnippet extends AbstractCookiesSnippet { + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. + * @param descriptors the descriptors + */ + protected ResponseCookiesSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented cookies will cause a + * failure. + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected ResponseCookiesSnippet(List descriptors, Map attributes) { + super("response", descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies ignore any cookies that are undocumented + */ + protected ResponseCookiesSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super("response", descriptors, attributes, ignoreUndocumentedCookies); + } + + @Override + protected Set extractActualCookies(Operation operation) { + return operation.getResponse().getCookies().stream().map(ResponseCookie::getName).collect(Collectors.toSet()); + } + + /** + * Returns a new {@code ResponseCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseCookiesSnippet and(CookieDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code ResponseCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseCookiesSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); + return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), this.ignoreUndocumentedCookies); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java new file mode 100644 index 00000000..d59b9452 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2015 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. + */ + +/** + * Documenting the cookies of a RESTful API's requests and responses. + */ +package org.springframework.restdocs.cookies; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index 378160d4..4778f84f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.operation; +import java.util.Collection; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -23,6 +25,7 @@ * The response that was received as part of performing an operation on a RESTful service. * * @author Andy Wilkinson + * @author Clyde Stubbs * @see Operation * @see Operation#getRequest() */ @@ -64,4 +67,12 @@ default int getStatusCode() { */ String getContentAsString(); + /** + * Returns the {@link ResponseCookie cookies} returned with the response. If no + * cookies were returned an empty collection is returned. + * @return the cookies, never {@code null} + * @since 2.1 + */ + Collection getCookies(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index 17f835a6..125f744b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -16,26 +16,47 @@ package org.springframework.restdocs.operation; +import java.util.Collection; +import java.util.Collections; + import org.springframework.http.HttpHeaders; /** * A factory for creating {@link OperationResponse OperationResponses}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ public class OperationResponseFactory { + /** + * Creates a new {@link OperationResponse} without cookies. If the response has any + * content, the given {@code headers} will be augmented to ensure that they include a + * {@code Content-Length} header. + * @param status the status of the response + * @param headers the request's headers + * @param content the content of the request + * @return the {@code OperationResponse} + */ + public OperationResponse create(int status, HttpHeaders headers, byte[] content) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), content, + Collections.emptyList()); + } + /** * Creates a new {@link OperationResponse}. If the response has any content, the given * {@code headers} will be augmented to ensure that they include a * {@code Content-Length} header. * @param status the status of the response - * @param headers the response's headers - * @param content the content of the response + * @param headers the request's headers + * @param content the content of the request + * @param cookies the cookies * @return the {@code OperationResponse} + * @since 3.0 */ - public OperationResponse create(int status, HttpHeaders headers, byte[] content) { - return new StandardOperationResponse(status, augmentHeaders(headers, content), content); + public OperationResponse create(int status, HttpHeaders headers, byte[] content, + Collection cookies) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), content, cookies); } /** @@ -49,7 +70,7 @@ public OperationResponse create(int status, HttpHeaders headers, byte[] content) */ public OperationResponse createFrom(OperationResponse original, byte[] newContent) { return new StandardOperationResponse(original.getStatusCode(), - getUpdatedHeaders(original.getHeaders(), newContent), newContent); + getUpdatedHeaders(original.getHeaders(), newContent), newContent, original.getCookies()); } /** @@ -60,7 +81,8 @@ public OperationResponse createFrom(OperationResponse original, byte[] newConten * @return the new response with the new headers */ public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { - return new StandardOperationResponse(original.getStatusCode(), newHeaders, original.getContent()); + return new StandardOperationResponse(original.getStatusCode(), newHeaders, original.getContent(), + original.getCookies()); } private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java new file mode 100644 index 00000000..69ed9271 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2018 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.operation; + +/** + * A representation of a Cookie returned in a response. + * + * @author Clyde Stubbs + * @since 2.1 + */ +public final class ResponseCookie { + + private final String name; + + private final String value; + + /** + * Creates a new {@code ResponseCookie} with the given {@code name} and {@code value}. + * @param name the name of the cookie + * @param value the value of the cookie + */ + public ResponseCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of the cookie. + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the cookie. + * @return the value + */ + public String getValue() { + return this.value; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 3a169365..73676e6a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.operation; +import java.util.Collection; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -23,21 +25,26 @@ * Standard implementation of {@link OperationResponse}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse { private final int status; + private Collection cookies; + /** * Creates a new response with the given {@code status}, {@code headers}, and * {@code content}. * @param status the status of the response * @param headers the headers of the response * @param content the content of the response + * @param cookies any cookies included in the response */ - StandardOperationResponse(int status, HttpHeaders headers, byte[] content) { + StandardOperationResponse(int status, HttpHeaders headers, byte[] content, Collection cookies) { super(content, headers); this.status = status; + this.cookies = cookies; } @Override @@ -50,4 +57,9 @@ public int getStatusCode() { return this.status; } + @Override + public Collection getCookies() { + return this.cookies; + } + } 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 e16ee4e5..231f4e2c 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 @@ -142,7 +142,7 @@ public OperationRequest preprocess(OperationRequest request) { @Override public OperationResponse preprocess(OperationResponse response) { return this.contentModifyingDelegate.preprocess(new OperationResponseFactory().create(response.getStatusCode(), - modify(response.getHeaders()), response.getContent())); + modify(response.getHeaders()), response.getContent(), response.getCookies())); } private HttpHeaders modify(HttpHeaders headers) { diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet new file mode 100644 index 00000000..0c531505 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#cookies}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet new file mode 100644 index 00000000..0c531505 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#cookies}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet new file mode 100644 index 00000000..dbc046b8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#cookies}} +`{{name}}` | {{description}} +{{/cookies}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet new file mode 100644 index 00000000..dbc046b8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#cookies}} +`{{name}}` | {{description}} +{{/cookies}} 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 new file mode 100644 index 00000000..c10ab68d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2018 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.io.IOException; +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 Andy Wilkinson + * @author Clyde Stubbs + */ +public class RequestCookiesSnippetFailureTests { + + @Rule + public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); + + @Test + public void missingRequestCookie() throws IOException { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("Accept").description("one"))) + .document(this.operationBuilder.request("http://localhost").build())) + .withMessage("Cookies with the following names were not found" + " in the request: [Accept]"); + } + + @Test + public void undocumentedRequestCookieAndMissingRequestHeader() throws IOException { + assertThatExceptionOfType(SnippetException.class).isThrownBy(() -> new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("Accept").description("one"))) + .document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test").build())) + .withMessageEndingWith("Cookies with the following names were not found" + " in the request: [Accept]"); + } + +} 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 new file mode 100644 index 00000000..bb2685f3 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2014-2018 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.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 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; + +/** + * Tests for {@link RequestCookiesSnippet}. + * + * @author Andreas Evers + * @author Andy Wilkinson + * @author Clyde Stubbs + */ +public class RequestCookiesSnippetTests extends AbstractSnippetTests { + + public RequestCookiesSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestWithCookies() throws IOException { + new RequestCookiesSnippet(Arrays.asList(CookieDocumentation.cookieWithName("Session").description("one"), + CookieDocumentation.cookieWithName("User").description("two"), + CookieDocumentation.cookieWithName("Timeout").description("three"), + CookieDocumentation.cookieWithName("Preference").description("four"), + CookieDocumentation.cookieWithName("Connection").description("five"))) + .document(this.operationBuilder.request("http://localhost").cookie("Session", "test") + .cookie("User", "nobody").cookie("Timeout", "3600").cookie("Preference", "inverse") + .cookie("Connection", "secure").build()); + assertThat(this.generatedSnippets.requestCookies()) + .is(tableWithHeader("Name", "Description").row("`Session`", "one").row("`User`", "two") + .row("`Timeout`", "three").row("`Preference`", "four").row("`Connection`", "five")); + } + + @Test + public void undocumentedRequestCookie() throws IOException { + new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one"))) + .document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test") + .cookie("Second", "*/*").build()); + assertThat(this.generatedSnippets.requestCookies()) + .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + } + + @Test + public void requestCookiesWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-cookies")) + .willReturn(snippetResource("request-cookies-with-title")); + new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))) + .document(this.operationBuilder + .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .request("http://localhost").cookie("X-Test", "test").build()); + assertThat(this.generatedSnippets.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")); + new RequestCookiesSnippet(Arrays.asList( + CookieDocumentation.cookieWithName("X-Test").description("one").attributes(key("foo").value("alpha")), + CookieDocumentation.cookieWithName("Accept-Encoding").description("two") + .attributes(key("foo").value("bravo")), + CookieDocumentation.cookieWithName("Accept").description("three") + .attributes(key("foo").value("charlie")))) + .document(this.operationBuilder + .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .request("http://localhost").cookie("X-Test", "test") + .cookie("Accept-Encoding", "gzip, deflate").cookie("Accept", "*/*").build()); + assertThat(this.generatedSnippets.requestCookies()).is(// + tableWithHeader("Name", "Description", "Foo").row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo").row("Accept", "three", "charlie")); + } + + @Test + public void additionalDescriptors() throws IOException { + CookieDocumentation + .requestCookies(CookieDocumentation.cookieWithName("X-Test").description("one"), + CookieDocumentation.cookieWithName("Accept").description("two"), + CookieDocumentation.cookieWithName("Accept-Encoding").description("three"), + CookieDocumentation.cookieWithName("Accept-Language").description("four")) + .and(CookieDocumentation.cookieWithName("Cache-Control").description("five"), + CookieDocumentation.cookieWithName("Connection").description("six")) + .document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test") + .cookie("Accept", "*/*").cookie("Accept-Encoding", "gzip, deflate") + .cookie("Accept-Language", "en-US,en;q=0.5").cookie("Cache-Control", "max-age=0") + .cookie("Connection", "keep-alive").build()); + assertThat(this.generatedSnippets.requestCookies()).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")); + } + + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.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"))); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { + return input; + } + return input.replace("|", "\\|"); + } + +} 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 new file mode 100644 index 00000000..100edc90 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2018 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.io.IOException; +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 ResponseCookiesSnippet} due to missing or + * undocumented cookies. + * + * @author Andy Wilkinson + * @author Clyde Stubbs + */ +public class ResponseCookiesSnippetFailureTests { + + @Rule + public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); + + @Test + public void missingResponseCookie() throws IOException { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseCookiesSnippet(Collections + .singletonList(CookieDocumentation.cookieWithName("Content-Type").description("one"))) + .document(this.operationBuilder.response().build())) + .withMessage("Cookies with the following names were not found" + " in the response: [Content-Type]"); + } + + @Test + public void undocumentedResponseCookieAndMissingResponseHeader() throws IOException { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseCookiesSnippet(Collections + .singletonList(CookieDocumentation.cookieWithName("Content-Type").description("one"))) + .document(this.operationBuilder.response().cookie("X-Test", "test").build())) + .withMessageEndingWith( + "Cookies with the following names were not found" + " in the response: [Content-Type]"); + } + +} 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 new file mode 100644 index 00000000..ec05d3d9 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2014-2018 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.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 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; + +/** + * Tests for {@link ResponseCookiesSnippet}. + * + * @author Andreas Evers + * @author Andy Wilkinson + * @author Clyde Stubbs + */ +public class ResponseCookiesSnippetTests extends AbstractSnippetTests { + + public ResponseCookiesSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void responseWithCookies() throws IOException { + new ResponseCookiesSnippet(Arrays.asList(CookieDocumentation.cookieWithName("X-Test").description("one"), + CookieDocumentation.cookieWithName("Content-Type").description("two"), + CookieDocumentation.cookieWithName("Etag").description("three"), + CookieDocumentation.cookieWithName("Cache-Control").description("five"), + CookieDocumentation.cookieWithName("Vary").description("six"))) + .document(this.operationBuilder.response().cookie("X-Test", "test") + .cookie("Content-Type", "application/json").cookie("Etag", "lskjadldj3ii32l2ij23") + .cookie("Cache-Control", "max-age=0").cookie("Vary", "User-Agent").build()); + assertThat(this.generatedSnippets.responseCookies()) + .is(tableWithHeader("Name", "Description").row("`X-Test`", "one").row("`Content-Type`", "two") + .row("`Etag`", "three").row("`Cache-Control`", "five").row("`Vary`", "six")); + } + + @Test + public void undocumentedResponseCookie() throws IOException { + new ResponseCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one"))) + .document(this.operationBuilder.response().cookie("X-Test", "test") + .cookie("Content-Type", "*/*").build()); + assertThat(this.generatedSnippets.responseCookies()) + .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + } + + @Test + public void responseCookiesWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-cookies")) + .willReturn(snippetResource("response-cookies-with-title")); + new ResponseCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))) + .document(this.operationBuilder + .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .response().cookie("X-Test", "test").build()); + assertThat(this.generatedSnippets.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")); + new ResponseCookiesSnippet(Arrays.asList( + CookieDocumentation.cookieWithName("X-Test").description("one").attributes(key("foo").value("alpha")), + CookieDocumentation.cookieWithName("Content-Type").description("two") + .attributes(key("foo").value("bravo")), + CookieDocumentation.cookieWithName("Etag").description("three") + .attributes(key("foo").value("charlie")))) + .document(this.operationBuilder + .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .response().cookie("X-Test", "test").cookie("Content-Type", "application/json") + .cookie("Etag", "lskjadldj3ii32l2ij23").build()); + assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo").row("Etag", "three", "charlie")); + } + + @Test + public void additionalDescriptors() throws IOException { + CookieDocumentation + .responseCookies(CookieDocumentation.cookieWithName("X-Test").description("one"), + CookieDocumentation.cookieWithName("Content-Type").description("two"), + CookieDocumentation.cookieWithName("Etag").description("three")) + .and(CookieDocumentation.cookieWithName("Cache-Control").description("five"), + CookieDocumentation.cookieWithName("Vary").description("six")) + .document(this.operationBuilder.response().cookie("X-Test", "test") + .cookie("Content-Type", "application/json").cookie("Etag", "lskjadldj3ii32l2ij23") + .cookie("Cache-Control", "max-age=0").cookie("Vary", "User-Agent").build()); + assertThat(this.generatedSnippets.responseCookies()) + .is(tableWithHeader("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 { + new ResponseCookiesSnippet( + Collections.singletonList(CookieDocumentation.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("|", "\\|"); + } + +} diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet new file mode 100644 index 00000000..62c9dad6 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#cookies}} +|{{name}} +|{{description}} +|{{foo}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet new file mode 100644 index 00000000..5e4a4af4 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#cookies}} +|{{name}} +|{{description}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet new file mode 100644 index 00000000..62c9dad6 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#cookies}} +|{{name}} +|{{description}} +|{{foo}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet new file mode 100644 index 00000000..5e4a4af4 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#cookies}} +|{{name}} +|{{description}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet new file mode 100644 index 00000000..8c741628 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#cookies}} +{{name}} | {{description}} | {{foo}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet new file mode 100644 index 00000000..e5e2c8bd --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#cookies}} +{{name}} | {{description}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet new file mode 100644 index 00000000..8c741628 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#cookies}} +{{name}} | {{description}} | {{foo}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet new file mode 100644 index 00000000..e5e2c8bd --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#cookies}} +{{name}} | {{description}} +{{/cookies}} \ No newline at end of file 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 index 8598f2b7..0071ce91 100644 --- 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 @@ -70,6 +70,14 @@ 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"); } 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/OperationBuilder.java index 9308255b..de29ad39 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/OperationBuilder.java @@ -22,8 +22,10 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.runners.model.Statement; @@ -42,6 +44,7 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; +import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; @@ -94,7 +97,6 @@ private void prepare(String operationName, File outputDirectory) { this.name = operationName; this.outputDirectory = outputDirectory; this.requestBuilder = null; - this.requestBuilder = null; this.attributes.clear(); } @@ -265,10 +267,12 @@ public final class OperationResponseBuilder { private HttpHeaders headers = new HttpHeaders(); + private Set cookies = new HashSet<>(); + private byte[] content = new byte[0]; private OperationResponse buildResponse() { - return new OperationResponseFactory().create(this.status, this.headers, this.content); + return new OperationResponseFactory().create(this.status, this.headers, this.content, this.cookies); } public OperationResponseBuilder status(int status) { @@ -281,6 +285,11 @@ public OperationResponseBuilder header(String name, String value) { return this; } + public OperationResponseBuilder cookie(String name, String value) { + this.cookies.add(new ResponseCookie(name, value)); + return this; + } + public OperationResponseBuilder content(byte[] content) { this.content = content; return this; 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 db0adc71..2ffd33de 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 @@ -16,6 +16,11 @@ package org.springframework.restdocs.mockmvc; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + import jakarta.servlet.http.Cookie; import org.springframework.http.HttpHeaders; @@ -23,6 +28,7 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.util.StringUtils; /** @@ -30,13 +36,28 @@ * {@link MockHttpServletResponse}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class MockMvcResponseConverter implements ResponseConverter { @Override public OperationResponse convert(MockHttpServletResponse mockResponse) { - return new OperationResponseFactory().create(mockResponse.getStatus(), extractHeaders(mockResponse), - mockResponse.getContentAsByteArray()); + HttpHeaders headers = extractHeaders(mockResponse); + Collection cookies = extractCookies(mockResponse, headers); + return new OperationResponseFactory().create(mockResponse.getStatus(), headers, + mockResponse.getContentAsByteArray(), cookies); + } + + private Collection extractCookies(MockHttpServletResponse mockRequest, HttpHeaders headers) { + if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { + return Collections.emptyList(); + } + List cookies = new ArrayList<>(); + for (Cookie servletCookie : mockRequest.getCookies()) { + cookies.add(new ResponseCookie(servletCookie.getName(), servletCookie.getValue())); + } + headers.remove(HttpHeaders.COOKIE); + return cookies; } private HttpHeaders extractHeaders(MockHttpServletResponse response) { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index a8e44f99..a1c1399b 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -16,6 +16,12 @@ package org.springframework.restdocs.restassured; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + import io.restassured.http.Header; import io.restassured.response.Response; @@ -23,19 +29,35 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.ResponseCookie; /** * A converter for creating an {@link OperationResponse} from a REST Assured * {@link Response}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class RestAssuredResponseConverter implements ResponseConverter { @Override public OperationResponse convert(Response response) { + HttpHeaders headers = extractHeaders(response); + Collection cookies = extractCookies(response, headers); return new OperationResponseFactory().create(response.getStatusCode(), extractHeaders(response), - extractContent(response)); + extractContent(response), cookies); + } + + private Collection extractCookies(Response response, HttpHeaders headers) { + if (response.getCookies() == null || response.getCookies().size() == 0) { + return Collections.emptyList(); + } + List cookies = new ArrayList<>(); + for (Map.Entry servletCookie : response.getCookies().entrySet()) { + cookies.add(new ResponseCookie(servletCookie.getKey(), servletCookie.getValue())); + } + headers.remove(HttpHeaders.COOKIE); + return cookies; } private HttpHeaders extractHeaders(Response response) { 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 df0a6084..af5aeefc 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 @@ -17,12 +17,14 @@ package org.springframework.restdocs.webtestclient; import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.util.StringUtils; @@ -36,8 +38,9 @@ class WebTestClientResponseConverter implements ResponseConverter cookies = extractCookies(result, result.getResponseHeaders()); return new OperationResponseFactory().create(result.getStatus().value(), extractHeaders(result), - result.getResponseBodyContent()); + result.getResponseBodyContent(), cookies); } private HttpHeaders extractHeaders(ExchangeResult result) { @@ -50,7 +53,7 @@ private HttpHeaders extractHeaders(ExchangeResult result) { return headers; } - private String generateSetCookieHeader(ResponseCookie cookie) { + private String generateSetCookieHeader(org.springframework.http.ResponseCookie cookie) { StringBuilder header = new StringBuilder(); header.append(cookie.getName()); header.append('='); @@ -71,12 +74,26 @@ private String generateSetCookieHeader(ResponseCookie cookie) { return header.toString(); } + private Collection extractCookies(ExchangeResult result, HttpHeaders headers) { + List cookieHeaders = headers.get(HttpHeaders.COOKIE); + if (cookieHeaders == null) { + return result.getResponseCookies().values().stream().flatMap(List::stream).map(this::createResponseCookie) + .collect(Collectors.toSet()); + } + headers.remove(HttpHeaders.COOKIE); + return cookieHeaders.stream().map(this::createResponseCookie).collect(Collectors.toList()); + } + private void appendIfAvailable(StringBuilder header, String value) { if (StringUtils.hasText(value)) { header.append(value); } } + private ResponseCookie createResponseCookie(org.springframework.http.ResponseCookie original) { + return new ResponseCookie(original.getName(), original.getValue()); + } + private void appendIfAvailable(StringBuilder header, String name, String value) { if (StringUtils.hasText(value)) { header.append(name); @@ -84,4 +101,9 @@ private void appendIfAvailable(StringBuilder header, String name, String value) } } + private ResponseCookie createResponseCookie(String header) { + String[] components = header.split("="); + return new ResponseCookie(components[0], components[1]); + } + } From d5522f5335e28cfccabdc95221c009b061382160 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 24 Mar 2022 17:32:42 +0000 Subject: [PATCH 059/198] Polish "Add support for documenting request and response cookies" See gh-592 --- .../docs/asciidoc/documenting-your-api.adoc | 59 ++++--- .../java/com/example/mockmvc/HttpCookies.java | 2 +- .../com/example/restassured/HttpCookies.java | 4 +- .../example/webtestclient/HttpCookies.java | 24 +-- .../cookies/AbstractCookiesSnippet.java | 99 ++++++----- .../restdocs/cookies/CookieDescriptor.java | 6 +- .../restdocs/cookies/CookieDocumentation.java | 154 ++++++++++++++---- .../cookies/RequestCookiesSnippet.java | 55 +++++-- .../cookies/ResponseCookiesSnippet.java | 47 ++++-- .../restdocs/cookies/package-info.java | 2 +- .../restdocs/operation/OperationResponse.java | 4 +- .../restdocs/operation/ResponseCookie.java | 4 +- .../operation/StandardOperationResponse.java | 2 +- .../RequestCookiesSnippetFailureTests.java | 21 ++- .../cookies/RequestCookiesSnippetTests.java | 111 +++++++------ .../ResponseCookiesSnippetFailureTests.java | 24 ++- .../cookies/ResponseCookiesSnippetTests.java | 114 +++++++------ .../testfixtures/GeneratedSnippets.java | 2 +- .../testfixtures/OperationBuilder.java | 2 +- .../mockmvc/MockMvcRequestConverter.java | 3 +- .../mockmvc/MockMvcResponseConverter.java | 13 +- .../MockMvcResponseConverterTests.java | 4 + .../RestAssuredRequestConverter.java | 1 + .../RestAssuredResponseConverter.java | 5 +- .../WebTestClientResponseConverter.java | 21 +-- .../WebTestClientResponseConverterTests.java | 10 +- 26 files changed, 482 insertions(+), 311 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 0f267476..9adae2ca 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -988,40 +988,38 @@ Each contains a table describing the headers. When documenting HTTP Headers, the test fails if a documented header is not found in the request or response. + [[documenting-your-api-http-cookies]] === HTTP Cookies -You can document the cookies in a request or response by using `requestCookies` and -`responseCookies`, respectively. The following examples show how to do so: +You can document the cookies in a request or response by using `requestCookies` and `responseCookies`, respectively. +The following examples show how to do so: -==== [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/HttpCookies.java[tags=cookies] ---- -<1> Configure Spring REST Docs to produce a snippet describing the request's cookies. - Uses the static `requestCookies` method on - `org.springframework.restdocs.cookies.CookieDocumentation`. -<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on - `org.springframework.restdocs.cookies.CookieDocumentation. -<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies` - method on `org.springframework.restdocs.cookies.CookieDocumentation`. -<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`. +<1> Make a GET request with a `JSESSIONID` cookie. +<2> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<3> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Produce a snippet describing the response's cookies. + Uses the static `responseCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. [source,java,indent=0,role="secondary"] .WebTestClient ---- include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies] ---- -<1> Configure Spring REST Docs to produce a snippet describing the request's cookies. +<1> Make a GET request with a `JSESSIONID` cookie. +<2> Configure Spring REST Docs to produce a snippet describing the request's cookies. Uses the static `requestCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. -<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on - `org.springframework.restdocs.cookies.CookieDocumentation. -<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies` - method on `org.springframework.restdocs.cookies.CookieDocumentation`. -<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`. +<3> Document the `JSESSIONID` cookie. + Uses the static `cookieWithName` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Produce a snippet describing the response's cookies. + Uses the static `responseCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured @@ -1029,20 +1027,19 @@ include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies] include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies] ---- <1> Configure Spring REST Docs to produce a snippet describing the request's cookies. - Uses the static `requestCookies` method on - `org.springframework.restdocs.cookies.CookieDocumentation`. -<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on - `org.springframework.restdocs.cookies.CookieDocumentation. -<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies` - method on `org.springframework.restdocs.cookies.CookieDocumentation`. -<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`. -==== - -The result is a snippet named `request-cookies.adoc` and a snippet named -`response-cookies.adoc`. Each contains a table describing the cookies. - -When documenting HTTP Cookies, the test fails if a documented cookie is not found in -the request or response. + Uses the static `requestCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<2> Document the `JSESSIONID` cookie. + Uses the static `cookieWithName` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<3> Produce a snippet describing the response's cookies. + Uses the static `responseCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Send a `JSESSIONID` cookie with the request. + +The result is a snippet named `request-cookies.adoc` and a snippet named `response-cookies.adoc`. +Each contains a table describing the cookies. + +When documenting HTTP Cookies, the test fails if a documented cookie is not found in the request or response. + + [[documenting-your-api-reusing-snippets]] === Reusing Snippets diff --git a/docs/src/test/java/com/example/mockmvc/HttpCookies.java b/docs/src/test/java/com/example/mockmvc/HttpCookies.java index aab39db7..7b1be451 100644 --- a/docs/src/test/java/com/example/mockmvc/HttpCookies.java +++ b/docs/src/test/java/com/example/mockmvc/HttpCookies.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * 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. diff --git a/docs/src/test/java/com/example/restassured/HttpCookies.java b/docs/src/test/java/com/example/restassured/HttpCookies.java index f6b76293..9171a8fb 100644 --- a/docs/src/test/java/com/example/restassured/HttpCookies.java +++ b/docs/src/test/java/com/example/restassured/HttpCookies.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * 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. @@ -29,7 +29,7 @@ public class HttpCookies { private RequestSpecification spec; - public void cookies() throws Exception { + public void cookies() { // tag::cookies[] RestAssured.given(this.spec).filter(document("cookies", requestCookies(// <1> cookieWithName("JSESSIONID").description("Saved session token")), // <2> diff --git a/docs/src/test/java/com/example/webtestclient/HttpCookies.java b/docs/src/test/java/com/example/webtestclient/HttpCookies.java index 8979520d..7d9bf36d 100644 --- a/docs/src/test/java/com/example/webtestclient/HttpCookies.java +++ b/docs/src/test/java/com/example/webtestclient/HttpCookies.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * 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. @@ -25,23 +25,17 @@ public class HttpCookies { - // @formatter:off - private WebTestClient webTestClient; - public void cookies() throws Exception { + public void cookies() { // tag::cookies[] - this.webTestClient - .get().uri("/people").cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1> - .exchange().expectStatus().isOk().expectBody() - .consumeWith(document("cookies", - requestCookies(// <2> - cookieWithName("JSESSIONID").description("Session token")), // <3> - responseCookies(// <4> - cookieWithName("JSESSIONID") - .description("Updated session token"), - cookieWithName("logged_in") - .description("User is logged in")))); + this.webTestClient.get().uri("/people").cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1> + .exchange().expectStatus().isOk().expectBody().consumeWith(document("cookies", requestCookies(// <2> + cookieWithName("JSESSIONID").description("Session token")), // <3> + responseCookies(// <4> + cookieWithName("JSESSIONID").description("Updated session token"), + cookieWithName("logged_in").description("User is logged in")))); // end::cookies[] } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java index 865ea397..19201586 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * 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. @@ -17,13 +17,16 @@ package org.springframework.restdocs.cookies; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Assert; @@ -31,17 +34,15 @@ * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that * document a RESTful resource's request or response cookies. * - * @author Andreas Evers * @author Clyde Stubbs - * @since 2.1 + * @author Andy Wilkinson + * @since 3.0 */ public abstract class AbstractCookiesSnippet extends TemplatedSnippet { - private List cookieDescriptors; + private final Map descriptorsByName = new LinkedHashMap<>(); - protected final boolean ignoreUndocumentedCookies; - - private String type; + private final boolean ignoreUndocumentedCookies; /** * Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named @@ -57,58 +58,53 @@ protected AbstractCookiesSnippet(String type, List descriptors boolean ignoreUndocumentedCookies) { super(type + "-cookies", attributes); for (CookieDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getName(), "The name of the cookie must not be null"); + Assert.notNull(descriptor.getName(), "Cookie descriptors must have a name"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), "The description of the cookie must not be null"); + Assert.notNull(descriptor.getDescription(), "The descriptor for cookie '" + descriptor.getName() + + "' must either have a description or be marked as ignored"); } + this.descriptorsByName.put(descriptor.getName(), descriptor); } - this.cookieDescriptors = descriptors; - this.type = type; this.ignoreUndocumentedCookies = ignoreUndocumentedCookies; } @Override protected Map createModel(Operation operation) { - validateCookieDocumentation(operation); + verifyCookieDescriptors(operation); Map model = new HashMap<>(); List> cookies = new ArrayList<>(); - model.put("cookies", cookies); - for (CookieDescriptor descriptor : this.cookieDescriptors) { - cookies.add(createModelForDescriptor(descriptor)); - } - return model; - } - - private void validateCookieDocumentation(Operation operation) { - List missingCookies = findMissingCookies(operation); - if (!missingCookies.isEmpty()) { - List names = new ArrayList<>(); - for (CookieDescriptor cookieDescriptor : missingCookies) { - names.add(cookieDescriptor.getName()); + for (CookieDescriptor descriptor : this.descriptorsByName.values()) { + if (!descriptor.isIgnored()) { + cookies.add(createModelForDescriptor(descriptor)); } - throw new SnippetException( - "Cookies with the following names were not found" + " in the " + this.type + ": " + names); } + model.put("cookies", cookies); + return model; } - /** - * Finds the cookies that are missing from the operation. A cookie is missing if it is - * described by one of the {@code cookieDescriptors} but is not present in the - * operation. - * @param operation the operation - * @return descriptors for the cookies that are missing from the operation - */ - protected List findMissingCookies(Operation operation) { - List missingCookies = new ArrayList<>(); + private void verifyCookieDescriptors(Operation operation) { Set actualCookies = extractActualCookies(operation); - for (CookieDescriptor cookieDescriptor : this.cookieDescriptors) { - if (!cookieDescriptor.isOptional() && !actualCookies.contains(cookieDescriptor.getName())) { - missingCookies.add(cookieDescriptor); + Set expectedCookies = new HashSet<>(); + for (Entry entry : this.descriptorsByName.entrySet()) { + if (!entry.getValue().isOptional()) { + expectedCookies.add(entry.getKey()); } } + Set undocumentedCookies; + if (this.ignoreUndocumentedCookies) { + undocumentedCookies = Collections.emptySet(); + } + else { + undocumentedCookies = new HashSet<>(actualCookies); + undocumentedCookies.removeAll(this.descriptorsByName.keySet()); + } + Set missingCookies = new HashSet<>(expectedCookies); + missingCookies.removeAll(actualCookies); - return missingCookies; + if (!undocumentedCookies.isEmpty() || !missingCookies.isEmpty()) { + verificationFailed(undocumentedCookies, missingCookies); + } } /** @@ -119,13 +115,30 @@ protected List findMissingCookies(Operation operation) { */ protected abstract Set extractActualCookies(Operation operation); + /** + * Called when the documented cookies do not match the actual cookies. + * @param undocumentedCookies the cookies that were found in the operation but were + * not documented + * @param missingCookies the cookies that were documented but were not found in the + * operation + */ + protected abstract void verificationFailed(Set undocumentedCookies, Set missingCookies); + /** * Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to * generate the documentation. * @return the cookie descriptors */ - protected final List getCookieDescriptors() { - return this.cookieDescriptors; + protected final Map getCookieDescriptors() { + return this.descriptorsByName; + } + + /** + * Returns whether or not this snippet ignores undocumented cookies. + * @return {@code true} if undocumented cookies are ignored, otherwise {@code false} + */ + protected final boolean isIgnoreUndocumentedCookies() { + return this.ignoreUndocumentedCookies; } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java index 51a4787a..97ee508c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * 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. @@ -21,9 +21,9 @@ /** * A description of a cookie found in a request or response. * - * @author Andreas Evers * @author Clyde Stubbs - * @since 2.1 + * @author Andy Wilkinson + * @since 3.0 * @see CookieDocumentation#cookieWithName(String) */ public class CookieDescriptor extends IgnorableDescriptor { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java index 0a88d13a..20c20f43 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * 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. @@ -25,11 +25,9 @@ /** * Static factory methods for documenting a RESTful API's request and response cookies. * - * @author Andreas Evers - * @author Andy Wilkinson - * @author Marcel Overdijk * @author Clyde Stubbs - * @since 2.1 + * @author Andy Wilkinson + * @since 3.0 */ public abstract class CookieDocumentation { @@ -51,8 +49,10 @@ public static CookieDescriptor cookieWithName(String name) { * Returns a new {@link Snippet} that will document the cookies of the API operation's * request. The cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional, and is not present in the - * request, a failure will occur. + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. * @param descriptors the descriptions of the request's cookies * @return the snippet that will document the request cookies * @see #cookieWithName(String) @@ -65,8 +65,10 @@ public static RequestCookiesSnippet requestCookies(CookieDescriptor... descripto * Returns a new {@link Snippet} that will document the cookies of the API operation's * request. The cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional, and is not present in the - * request, a failure will occur. + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. * @param descriptors the descriptions of the request's cookies * @return the snippet that will document the request cookies * @see #cookieWithName(String) @@ -75,13 +77,43 @@ public static RequestCookiesSnippet requestCookies(List descri return new RequestCookiesSnippet(descriptors); } + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(CookieDescriptor... descriptors) { + return relaxedRequestCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(List descriptors) { + return new RequestCookiesSnippet(descriptors, true); + } + /** * Returns a new {@link Snippet} that will document the cookies of the API * operations's request. The given {@code attributes} will be available during snippet * generation and the cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional, and is not present in the - * request, a failure will occur. + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. * @param attributes the attributes * @param descriptors the descriptions of the request's cookies * @return the snippet that will document the request cookies @@ -97,9 +129,10 @@ public static RequestCookiesSnippet requestCookies(Map attribute * operations's request. The given {@code attributes} will be available during snippet * generation and the cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional, and is not present in the - * request, a failure will occur. Any cookies present in the request that are not - * documented will result in an error. + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. * @param attributes the attributes * @param descriptors the descriptions of the request's cookies * @return the snippet that will document the request cookies @@ -107,7 +140,7 @@ public static RequestCookiesSnippet requestCookies(Map attribute */ public static RequestCookiesSnippet requestCookies(Map attributes, List descriptors) { - return new RequestCookiesSnippet(descriptors, attributes, false); + return new RequestCookiesSnippet(descriptors, attributes); } /** @@ -116,8 +149,24 @@ public static RequestCookiesSnippet requestCookies(Map attribute * generation and the cookies will be documented using the given {@code descriptors}. *

    * If a cookie is documented, is not marked as optional, and is not present in the - * request, a failure will occur. An undocumented cookie in the request will not - * generate an error. + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(Map attributes, + CookieDescriptor... descriptors) { + return relaxedRequestCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. * @param attributes the attributes * @param descriptors the descriptions of the request's cookies * @return the snippet that will document the request cookies @@ -132,8 +181,10 @@ public static RequestCookiesSnippet relaxedRequestCookies(Map at * Returns a new {@link Snippet} that will document the cookies of the API operation's * response. The cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional or ignored, and is not present - * in the request, a failure will occur. + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. * @param descriptors the descriptions of the response's cookies * @return the snippet that will document the response cookies * @see #cookieWithName(String) @@ -146,9 +197,10 @@ public static ResponseCookiesSnippet responseCookies(CookieDescriptor... descrip * Returns a new {@link Snippet} that will document the cookies of the API operation's * response. The cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional or ignored, and is not present - * in the request, a failure will occur. If a cookie is present in the response but is - * undocumented a failure will occur. + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. * @param descriptors the descriptions of the response's cookies * @return the snippet that will document the response cookies * @see #cookieWithName(String) @@ -161,15 +213,28 @@ public static ResponseCookiesSnippet responseCookies(List desc * Returns a new {@link Snippet} that will document the cookies of the API operation's * response. The cookies will be documented using the given {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional or ignored, and is not present - * in the request, a failure will occur. No failure will occur if a cookie is present - * but undocumented. + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(CookieDescriptor... descriptors) { + return relaxedResponseCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. * @param descriptors the descriptions of the response's cookies * @return the snippet that will document the response cookies * @see #cookieWithName(String) */ public static ResponseCookiesSnippet relaxedResponseCookies(List descriptors) { - return new ResponseCookiesSnippet(descriptors, null, true); + return new ResponseCookiesSnippet(descriptors, true); } /** @@ -178,9 +243,10 @@ public static ResponseCookiesSnippet relaxedResponseCookies(List - * If a cookie is documented, is not marked as optional, and is not present in the - * response, a failure will occur. If a cookie is present in the response but is - * undocumented a failure will occur. + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. * @param attributes the attributes * @param descriptors the descriptions of the response's cookies * @return the snippet that will document the response cookies @@ -197,9 +263,10 @@ public static ResponseCookiesSnippet responseCookies(Map attribu * snippet generation and the cookies will be documented using the given * {@code descriptors}. *

    - * If a cookie is documented, is not marked as optional, and is not present in the - * response, a failure will occur. If a cookie is present in the response but is - * undocumented a failure will occur. + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. * @param attributes the attributes * @param descriptors the descriptions of the response's cookies * @return the snippet that will document the response cookies @@ -207,7 +274,25 @@ public static ResponseCookiesSnippet responseCookies(Map attribu */ public static ResponseCookiesSnippet responseCookies(Map attributes, List descriptors) { - return new ResponseCookiesSnippet(descriptors, attributes, false); + return new ResponseCookiesSnippet(descriptors, attributes); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

    + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(Map attributes, + CookieDescriptor... descriptors) { + return relaxedResponseCookies(attributes, Arrays.asList(descriptors)); } /** @@ -217,8 +302,7 @@ public static ResponseCookiesSnippet responseCookies(Map attribu * {@code descriptors}. *

    * If a cookie is documented, is not marked as optional, and is not present in the - * response, a failure will occur. No failure will occur if a cookie is present but - * undocumented. + * response, a failure will occur. Any undocumented cookies will be ignored. * @param attributes the attributes * @param descriptors the descriptions of the response's cookies * @return the snippet that will document the response cookies diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java index 1a8d7ccc..e5f4d5d2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * 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. @@ -26,14 +26,14 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; /** * A {@link Snippet} that documents the cookies in a request. * - * @author Andreas Evers - * @author Andy Wilkinson * @author Clyde Stubbs - * @since 2.1 + * @author Andy Wilkinson + * @since 3.0 * @see CookieDocumentation#requestCookies(CookieDescriptor...) * @see CookieDocumentation#requestCookies(Map, CookieDescriptor...) */ @@ -50,15 +50,14 @@ protected RequestCookiesSnippet(List descriptors) { /** * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the - * request using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * request using the given {@code descriptors}. If {@code ignoreUndocumentedCookies} + * is {@code true}, undocumented cookies will be ignored and will not trigger a + * failure. * @param descriptors the descriptors - * @param attributes the additional attributes - * @param ignoreUndocumentedCookies if set undocumented cookies will be ignored + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored */ - protected RequestCookiesSnippet(List descriptors, Map attributes, - boolean ignoreUndocumentedCookies) { - super("request", descriptors, attributes, ignoreUndocumentedCookies); + protected RequestCookiesSnippet(List descriptors, boolean ignoreUndocumentedCookies) { + this(descriptors, null, ignoreUndocumentedCookies); } /** @@ -70,7 +69,20 @@ protected RequestCookiesSnippet(List descriptors, Map descriptors, Map attributes) { - super("request", descriptors, attributes, false); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected RequestCookiesSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super("request", descriptors, attributes, ignoreUndocumentedCookies); } @Override @@ -82,6 +94,21 @@ protected Set extractActualCookies(Operation operation) { return actualCookies; } + @Override + protected void verificationFailed(Set undocumentedCookies, Set missingCookies) { + String message = ""; + if (!undocumentedCookies.isEmpty()) { + message += "Cookies with the following names were not documented: " + undocumentedCookies; + } + if (!missingCookies.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Cookies with the following names were not found in the request: " + missingCookies; + } + throw new SnippetException(message); + } + /** * Returns a new {@code RequestCookiesSnippet} configured with this snippet's * attributes and its descriptors combined with the given @@ -101,9 +128,9 @@ public final RequestCookiesSnippet and(CookieDescriptor... additionalDescriptors * @return the new snippet */ public final RequestCookiesSnippet and(List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors()); + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors().values()); combinedDescriptors.addAll(additionalDescriptors); - return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), false); + return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), isIgnoreUndocumentedCookies()); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java index 99076115..f6a5176f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * 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. @@ -26,14 +26,14 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; /** * A {@link Snippet} that documents the cookies in a response. * - * @author Andreas Evers - * @author Andy Wilkinson * @author Clyde Stubbs - * @since 2.1 + * @author Andy Wilkinson + * @since 3.0 * @see CookieDocumentation#responseCookies(CookieDescriptor...) * @see CookieDocumentation#responseCookies(Map, CookieDescriptor...) */ @@ -50,14 +50,26 @@ protected ResponseCookiesSnippet(List descriptors) { /** * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the - * response using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. Undocumented cookies will cause a + * response using the given {@code descriptors}. If {@code ignoreUndocumentedCookies} + * is {@code true}, undocumented cookies will be ignored and will not trigger a * failure. * @param descriptors the descriptors + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected ResponseCookiesSnippet(List descriptors, boolean ignoreUndocumentedCookies) { + this(descriptors, null, ignoreUndocumentedCookies); + } + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented cookies will not be + * ignored. + * @param descriptors the descriptors * @param attributes the additional attributes */ protected ResponseCookiesSnippet(List descriptors, Map attributes) { - super("response", descriptors, attributes, false); + this(descriptors, attributes, false); } /** @@ -66,7 +78,7 @@ protected ResponseCookiesSnippet(List descriptors, Map descriptors, Map attributes, boolean ignoreUndocumentedCookies) { @@ -78,6 +90,21 @@ protected Set extractActualCookies(Operation operation) { return operation.getResponse().getCookies().stream().map(ResponseCookie::getName).collect(Collectors.toSet()); } + @Override + protected void verificationFailed(Set undocumentedCookies, Set missingCookies) { + String message = ""; + if (!undocumentedCookies.isEmpty()) { + message += "Cookies with the following names were not documented: " + undocumentedCookies; + } + if (!missingCookies.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Cookies with the following names were not found in the response: " + missingCookies; + } + throw new SnippetException(message); + } + /** * Returns a new {@code ResponseCookiesSnippet} configured with this snippet's * attributes and its descriptors combined with the given @@ -97,9 +124,9 @@ public final ResponseCookiesSnippet and(CookieDescriptor... additionalDescriptor * @return the new snippet */ public final ResponseCookiesSnippet and(List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors()); + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors().values()); combinedDescriptors.addAll(additionalDescriptors); - return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), this.ignoreUndocumentedCookies); + return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), isIgnoreUndocumentedCookies()); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java index d59b9452..afc01d60 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index 4778f84f..8490ade2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -71,7 +71,7 @@ default int getStatusCode() { * Returns the {@link ResponseCookie cookies} returned with the response. If no * cookies were returned an empty collection is returned. * @return the cookies, never {@code null} - * @since 2.1 + * @since 3.0 */ Collection getCookies(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java index 69ed9271..9cf39302 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * 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. @@ -20,7 +20,7 @@ * A representation of a Cookie returned in a response. * * @author Clyde Stubbs - * @since 2.1 + * @since 3.0 */ public final class ResponseCookie { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 73676e6a..5d9045af 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. 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 index c10ab68d..6498b2d5 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.restdocs.cookies; -import java.io.IOException; import java.util.Collections; import org.junit.Rule; @@ -32,8 +31,8 @@ * Tests for failures when rendering {@link RequestCookiesSnippet} due to missing or * undocumented cookies. * - * @author Andy Wilkinson * @author Clyde Stubbs + * @author Andy Wilkinson */ public class RequestCookiesSnippetFailureTests { @@ -41,20 +40,20 @@ public class RequestCookiesSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); @Test - public void missingRequestCookie() throws IOException { + public void missingRequestCookie() { assertThatExceptionOfType(SnippetException.class) .isThrownBy(() -> new RequestCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("Accept").description("one"))) + 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: [Accept]"); + .withMessage("Cookies with the following names were not found in the request: [JSESSIONID]"); } @Test - public void undocumentedRequestCookieAndMissingRequestHeader() throws IOException { - assertThatExceptionOfType(SnippetException.class).isThrownBy(() -> new RequestCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("Accept").description("one"))) - .document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test").build())) - .withMessageEndingWith("Cookies with the following names were not found" + " in the request: [Accept]"); + 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 bb2685f3..1fa41c0b 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-2018 the original author or authors. + * 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. @@ -32,15 +32,15 @@ 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; /** * Tests for {@link RequestCookiesSnippet}. * - * @author Andreas Evers - * @author Andy Wilkinson * @author Clyde Stubbs + * @author Andy Wilkinson */ public class RequestCookiesSnippetTests extends AbstractSnippetTests { @@ -50,27 +50,42 @@ public RequestCookiesSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithCookies() throws IOException { - new RequestCookiesSnippet(Arrays.asList(CookieDocumentation.cookieWithName("Session").description("one"), - CookieDocumentation.cookieWithName("User").description("two"), - CookieDocumentation.cookieWithName("Timeout").description("three"), - CookieDocumentation.cookieWithName("Preference").description("four"), - CookieDocumentation.cookieWithName("Connection").description("five"))) - .document(this.operationBuilder.request("http://localhost").cookie("Session", "test") - .cookie("User", "nobody").cookie("Timeout", "3600").cookie("Preference", "inverse") - .cookie("Connection", "secure").build()); + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two"))) + .document(this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true").build()); assertThat(this.generatedSnippets.requestCookies()) - .is(tableWithHeader("Name", "Description").row("`Session`", "one").row("`User`", "two") - .row("`Timeout`", "three").row("`Preference`", "four").row("`Connection`", "five")); + .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); } @Test - public void undocumentedRequestCookie() throws IOException { + public void ignoredRequestCookie() throws IOException { new RequestCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one"))) - .document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test") - .cookie("Second", "*/*").build()); + Arrays.asList(cookieWithName("tz").ignored(), cookieWithName("logged_in").description("two"))) + .document(this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true").build()); assertThat(this.generatedSnippets.requestCookies()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + .is(tableWithHeader("Name", "Description").row("`logged_in`", "two")); + } + + @Test + public void allUndocumentedCookiesCanBeIgnored() throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")), + true).document( + this.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")); + } + + @Test + public void missingOptionalCookie() 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")); } @Test @@ -78,12 +93,11 @@ public void requestCookiesWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-cookies")) .willReturn(snippetResource("request-cookies-with-title")); - new RequestCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")), + 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("X-Test", "test").build()); + .request("http://localhost").cookie("tz", "Europe%2FLondon").build()); assertThat(this.generatedSnippets.requestCookies()).contains("Custom title"); } @@ -92,44 +106,45 @@ public void requestCookiesWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-cookies")) .willReturn(snippetResource("request-cookies-with-extra-column")); - new RequestCookiesSnippet(Arrays.asList( - CookieDocumentation.cookieWithName("X-Test").description("one").attributes(key("foo").value("alpha")), - CookieDocumentation.cookieWithName("Accept-Encoding").description("two") - .attributes(key("foo").value("bravo")), - CookieDocumentation.cookieWithName("Accept").description("three") - .attributes(key("foo").value("charlie")))) + 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").cookie("X-Test", "test") - .cookie("Accept-Encoding", "gzip, deflate").cookie("Accept", "*/*").build()); + .request("http://localhost").cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true").build()); assertThat(this.generatedSnippets.requestCookies()).is(// - tableWithHeader("Name", "Description", "Foo").row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo").row("Accept", "three", "charlie")); + tableWithHeader("Name", "Description", "Foo").row("tz", "one", "alpha").row("logged_in", "two", + "bravo")); } @Test public void additionalDescriptors() throws IOException { - CookieDocumentation - .requestCookies(CookieDocumentation.cookieWithName("X-Test").description("one"), - CookieDocumentation.cookieWithName("Accept").description("two"), - CookieDocumentation.cookieWithName("Accept-Encoding").description("three"), - CookieDocumentation.cookieWithName("Accept-Language").description("four")) - .and(CookieDocumentation.cookieWithName("Cache-Control").description("five"), - CookieDocumentation.cookieWithName("Connection").description("six")) - .document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test") - .cookie("Accept", "*/*").cookie("Accept-Encoding", "gzip, deflate") - .cookie("Accept-Language", "en-US,en;q=0.5").cookie("Cache-Control", "max-age=0") - .cookie("Connection", "keep-alive").build()); - assertThat(this.generatedSnippets.requestCookies()).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")); + 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").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").row("`user_session`", "three")); } @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { + public void additionalDescriptorsWithRelaxedRequestCookies() throws IOException { new RequestCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.request("http://localhost").cookie("Foo|Bar", "baz").build()); + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")), + true).and(cookieWithName("user_session").description("three")) + .document(this.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") + .row("`logged_in`", "two").row("`user_session`", "three")); + } + + @Test + public void tableCellContentIsEscapedWhenNecessary() 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"))); } 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 index 100edc90..93dfe5be 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.restdocs.cookies; -import java.io.IOException; import java.util.Collections; import org.junit.Rule; @@ -27,13 +26,14 @@ 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 Andy Wilkinson * @author Clyde Stubbs + * @author Andy Wilkinson */ public class ResponseCookiesSnippetFailureTests { @@ -41,22 +41,20 @@ public class ResponseCookiesSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); @Test - public void missingResponseCookie() throws IOException { + public void missingResponseCookie() { assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseCookiesSnippet(Collections - .singletonList(CookieDocumentation.cookieWithName("Content-Type").description("one"))) + .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: [Content-Type]"); + .withMessage("Cookies with the following names were not found in the response: [JSESSIONID]"); } @Test - public void undocumentedResponseCookieAndMissingResponseHeader() throws IOException { + public void undocumentedResponseCookie() { assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new ResponseCookiesSnippet(Collections - .singletonList(CookieDocumentation.cookieWithName("Content-Type").description("one"))) - .document(this.operationBuilder.response().cookie("X-Test", "test").build())) - .withMessageEndingWith( - "Cookies with the following names were not found" + " in the response: [Content-Type]"); + .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 ec05d3d9..39a08328 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-2018 the original author or authors. + * 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. @@ -32,15 +32,15 @@ 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; /** * Tests for {@link ResponseCookiesSnippet}. * - * @author Andreas Evers - * @author Andy Wilkinson * @author Clyde Stubbs + * @author Andy Wilkinson */ public class ResponseCookiesSnippetTests extends AbstractSnippetTests { @@ -50,27 +50,41 @@ public ResponseCookiesSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithCookies() throws IOException { - new ResponseCookiesSnippet(Arrays.asList(CookieDocumentation.cookieWithName("X-Test").description("one"), - CookieDocumentation.cookieWithName("Content-Type").description("two"), - CookieDocumentation.cookieWithName("Etag").description("three"), - CookieDocumentation.cookieWithName("Cache-Control").description("five"), - CookieDocumentation.cookieWithName("Vary").description("six"))) - .document(this.operationBuilder.response().cookie("X-Test", "test") - .cookie("Content-Type", "application/json").cookie("Etag", "lskjadldj3ii32l2ij23") - .cookie("Cache-Control", "max-age=0").cookie("Vary", "User-Agent").build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one").row("`Content-Type`", "two") - .row("`Etag`", "three").row("`Cache-Control`", "five").row("`Vary`", "six")); + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"), + cookieWithName("user_session").description("two"))) + .document(this.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")); } @Test - public void undocumentedResponseCookie() throws IOException { - new ResponseCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one"))) - .document(this.operationBuilder.response().cookie("X-Test", "test") - .cookie("Content-Type", "*/*").build()); + public void ignoredResponseCookie() throws IOException { + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").ignored(), + cookieWithName("user_session").description("two"))) + .document(this.operationBuilder.response().cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh").build()); assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one")); + .is(tableWithHeader("Name", "Description").row("`user_session`", "two")); + } + + @Test + public void allUndocumentedResponseCookiesCanBeIgnored() throws IOException { + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"), + cookieWithName("user_session").description("two")), true) + .document(this.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")); + } + + @Test + public void missingOptionalResponseCookie() 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")); } @Test @@ -78,12 +92,11 @@ public void responseCookiesWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-cookies")) .willReturn(snippetResource("response-cookies-with-title")); - new ResponseCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")), + 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("X-Test", "test").build()); + .response().cookie("has_recent_activity", "true").build()); assertThat(this.generatedSnippets.responseCookies()).contains("Custom title"); } @@ -93,40 +106,45 @@ public void responseCookiesWithCustomDescriptorAttributes() throws IOException { given(resolver.resolveTemplateResource("response-cookies")) .willReturn(snippetResource("response-cookies-with-extra-column")); new ResponseCookiesSnippet(Arrays.asList( - CookieDocumentation.cookieWithName("X-Test").description("one").attributes(key("foo").value("alpha")), - CookieDocumentation.cookieWithName("Content-Type").description("two") - .attributes(key("foo").value("bravo")), - CookieDocumentation.cookieWithName("Etag").description("three") - .attributes(key("foo").value("charlie")))) - .document(this.operationBuilder - .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) - .response().cookie("X-Test", "test").cookie("Content-Type", "application/json") - .cookie("Etag", "lskjadldj3ii32l2ij23").build()); - assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo").row("Etag", "three", "charlie")); + 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().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")); } @Test public void additionalDescriptors() throws IOException { CookieDocumentation - .responseCookies(CookieDocumentation.cookieWithName("X-Test").description("one"), - CookieDocumentation.cookieWithName("Content-Type").description("two"), - CookieDocumentation.cookieWithName("Etag").description("three")) - .and(CookieDocumentation.cookieWithName("Cache-Control").description("five"), - CookieDocumentation.cookieWithName("Vary").description("six")) - .document(this.operationBuilder.response().cookie("X-Test", "test") - .cookie("Content-Type", "application/json").cookie("Etag", "lskjadldj3ii32l2ij23") - .cookie("Cache-Control", "max-age=0").cookie("Vary", "User-Agent").build()); - assertThat(this.generatedSnippets.responseCookies()) - .is(tableWithHeader("Name", "Description").row("`X-Test`", "one").row("`Content-Type`", "two") - .row("`Etag`", "three").row("`Cache-Control`", "five").row("`Vary`", "six")); + .responseCookies(cookieWithName("has_recent_activity").description("one"), + cookieWithName("user_session").description("two")) + .and(cookieWithName("color_theme").description("three")) + .document(this.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")); + } + + @Test + public void additionalDescriptorsWithRelaxedResponseCookies() throws IOException { + CookieDocumentation.relaxedResponseCookies(cookieWithName("has_recent_activity").description("one")) + .and(cookieWithName("color_theme").description("two")) + .document(this.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")); } @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - new ResponseCookiesSnippet( - Collections.singletonList(CookieDocumentation.cookieWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.response().cookie("Foo|Bar", "baz").build()); + 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"))); } 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 index 0071ce91..e619e02b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * 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. 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/OperationBuilder.java index de29ad39..a9299048 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/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * 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. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 2ab8fcd8..053df890 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -51,6 +51,7 @@ * {@link MockHttpServletRequest}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class MockMvcRequestConverter implements RequestConverter { 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 2ffd33de..56d44f7c 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-2019 the original author or authors. + * 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. @@ -43,20 +43,19 @@ class MockMvcResponseConverter implements ResponseConverter cookies = extractCookies(mockResponse, headers); + Collection cookies = extractCookies(mockResponse); return new OperationResponseFactory().create(mockResponse.getStatus(), headers, mockResponse.getContentAsByteArray(), cookies); } - private Collection extractCookies(MockHttpServletResponse mockRequest, HttpHeaders headers) { - if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { + private Collection extractCookies(MockHttpServletResponse mockResponse) { + if (mockResponse.getCookies() == null || mockResponse.getCookies().length == 0) { return Collections.emptyList(); } List cookies = new ArrayList<>(); - for (Cookie servletCookie : mockRequest.getCookies()) { - cookies.add(new ResponseCookie(servletCookie.getName(), servletCookie.getValue())); + for (Cookie cookie : mockResponse.getCookies()) { + cookies.add(new ResponseCookie(cookie.getName(), cookie.getValue())); } - headers.remove(HttpHeaders.COOKIE); return cookies; } 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 c1a28b62..40338091 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 @@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.ResponseCookie; import static org.assertj.core.api.Assertions.assertThat; @@ -58,6 +59,9 @@ public void responseWithCookie() { assertThat(operationResponse.getHeaders()).hasSize(1); assertThat(operationResponse.getHeaders()).containsEntry(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"); } @Test diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 9d50090a..f4e00b2e 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -48,6 +48,7 @@ * {@link FilterableRequestSpecification}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class RestAssuredRequestConverter implements RequestConverter { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index a1c1399b..09b95978 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -53,10 +53,9 @@ private Collection extractCookies(Response response, HttpHeaders return Collections.emptyList(); } List cookies = new ArrayList<>(); - for (Map.Entry servletCookie : response.getCookies().entrySet()) { - cookies.add(new ResponseCookie(servletCookie.getKey(), servletCookie.getValue())); + for (Map.Entry cookie : response.getCookies().entrySet()) { + cookies.add(new ResponseCookie(cookie.getKey(), cookie.getValue())); } - headers.remove(HttpHeaders.COOKIE); return cookies; } 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 af5aeefc..fc7d4147 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-2019 the original author or authors. + * 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. @@ -33,12 +33,13 @@ * {@link ExchangeResult}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class WebTestClientResponseConverter implements ResponseConverter { @Override public OperationResponse convert(ExchangeResult result) { - Collection cookies = extractCookies(result, result.getResponseHeaders()); + Collection cookies = extractCookies(result); return new OperationResponseFactory().create(result.getStatus().value(), extractHeaders(result), result.getResponseBodyContent(), cookies); } @@ -74,14 +75,9 @@ private String generateSetCookieHeader(org.springframework.http.ResponseCookie c return header.toString(); } - private Collection extractCookies(ExchangeResult result, HttpHeaders headers) { - List cookieHeaders = headers.get(HttpHeaders.COOKIE); - if (cookieHeaders == null) { - return result.getResponseCookies().values().stream().flatMap(List::stream).map(this::createResponseCookie) - .collect(Collectors.toSet()); - } - headers.remove(HttpHeaders.COOKIE); - return cookieHeaders.stream().map(this::createResponseCookie).collect(Collectors.toList()); + private Collection extractCookies(ExchangeResult result) { + return result.getResponseCookies().values().stream().flatMap(List::stream).map(this::createResponseCookie) + .collect(Collectors.toSet()); } private void appendIfAvailable(StringBuilder header, String value) { @@ -101,9 +97,4 @@ private void appendIfAvailable(StringBuilder header, String name, String value) } } - private ResponseCookie createResponseCookie(String header) { - String[] components = header.split("="); - return new ResponseCookie(components[0], components[1]); - } - } 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 ce3dfdf6..f420bcbf 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-2019 the original author or authors. + * 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. @@ -23,8 +23,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseCookie; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.server.RouterFunctions; @@ -62,7 +62,8 @@ public void responseWithCookie() { ExchangeResult result = WebTestClient .bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.ok() - .cookie(ResponseCookie.from("name", "value").domain("localhost").httpOnly(true).build()) + .cookie(org.springframework.http.ResponseCookie.from("name", "value") + .domain("localhost").httpOnly(true).build()) .build())) .configureClient().baseUrl("http://localhost").build().get().uri("/foo").exchange().expectBody() .returnResult(); @@ -70,6 +71,9 @@ public void responseWithCookie() { assertThat(response.getHeaders()).hasSize(1); assertThat(response.getHeaders()).containsEntry(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 63be118ed0b877edd58f474c630f589dfcae2a16 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 16:01:20 +0100 Subject: [PATCH 060/198] Reinstate missing section headings in the reference documentation Closes gh-833 --- docs/src/docs/asciidoc/configuration.adoc | 3 ++- docs/src/docs/asciidoc/customizing-requests-and-responses.adoc | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index ee8e4adf..24c23a47 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -127,7 +127,6 @@ include::{examples-dir}/com/example/webtestclient/CustomFormat.java[tags=custom- ---- include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-format] ---- -==== @@ -195,3 +194,5 @@ include::{examples-dir}/com/example/restassured/CustomDefaultOperationPreprocess ---- <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. + + diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index f267612f..59177d3d 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -24,13 +24,13 @@ include::{examples-dir}/com/example/webtestclient/PerTestPreprocessing.java[tags <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. +[source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/PerTestPreprocessing.java[tags=preprocessing] ---- <1> Apply a request preprocessor that removes the header named `Foo`. <2> Apply a response preprocessor that pretty prints its content. -==== Alternatively, you may want to apply the same preprocessors to every test. You can do so by using the `RestDocumentationConfigurer` API in your `@Before` method to configure the preprocessors. From f7bad388e20d8fd2266432c9c893af2fab0f754c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:23:56 +0100 Subject: [PATCH 061/198] Upgrade samples to Spring Framework 6.0.0.M5 See gh-822 --- samples/junit5/build.gradle | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- samples/web-test-client/build.gradle | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle index 97505238..9713f32b 100644 --- a/samples/junit5/build.gradle +++ b/samples/junit5/build.gradle @@ -32,7 +32,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") implementation 'org.springframework:spring-webmvc' testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index f01476be..df839e10 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -31,7 +31,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") implementation 'io.projectreactor.netty:reactor-netty-http:1.0.15' implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 1d98a1e9..2e4d2214 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { - implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 7e012c9f..bb8677f4 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -20,7 +20,7 @@ org.springframework spring-framework-bom - 6.0.0-M4 + 6.0.0-M5 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 9ae2cd05..baa6963c 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -31,7 +31,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index ef4f6153..3e9226cc 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -32,7 +32,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") implementation "org.springframework:spring-webmvc" testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" diff --git a/samples/web-test-client/build.gradle b/samples/web-test-client/build.gradle index bd96e7f0..34de7cff 100644 --- a/samples/web-test-client/build.gradle +++ b/samples/web-test-client/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - implementation platform("org.springframework:spring-framework-bom:6.0.0-M4") + implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-webflux' From 8c277ae8e4128a0b7b8d21beae7ec87b59b4a84a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:27:02 +0100 Subject: [PATCH 062/198] Upgrade samples to Spring Data 2022.0.0-M5 Closes gh-841 --- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 2e4d2214..ee274abe 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -27,7 +27,7 @@ ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' dependencies { implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M5") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index bb8677f4..07d11a3a 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -27,7 +27,7 @@ org.springframework.data spring-data-bom - 2022.0.0-M4 + 2022.0.0-M5 import pom diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index baa6963c..0f81243e 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -32,7 +32,7 @@ dependencies { asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M4") + implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M5") implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" From 71773f89b60fda30258c5bfdc268f72635892241 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:29:56 +0100 Subject: [PATCH 063/198] Upgrade samples to Spring HATEOAS 2.0.0-M5 Closes gh-842 --- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 0f81243e..74f810ae 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -39,7 +39,7 @@ dependencies { implementation "org.hibernate:hibernate-core-jakarta:5.6.9.Final" implementation "org.springframework:spring-webmvc" implementation "org.springframework.data:spring-data-jpa" - implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M3" + implementation "org.springframework.hateoas:spring-hateoas:2.0.0-M5" runtimeOnly 'com.h2database:h2:2.1.210' runtimeOnly 'org.atteo:evo-inflector:1.2.1' From 108e04bc875addc9cbb96bb714e3af187ce50c46 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:33:48 +0100 Subject: [PATCH 064/198] Upgrade to JMustache 1.15 Closes gh-834 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 35da3e4f..c9e18b07 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 org.gradle.parallel=true javaFormatVersion=0.0.34 -jmustacheVersion=1.12 +jmustacheVersion=1.15 From 7537e36f193191868934db68208be40add80d393 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:34:20 +0100 Subject: [PATCH 065/198] Upgrade to AsciidoctorJ 2.5.4 Closes gh-835 --- spring-restdocs-platform/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 4f3bb67e..2bdf18c9 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -14,7 +14,7 @@ dependencies { api("junit:junit:4.12") api("io.rest-assured:rest-assured:5.0.1") api("org.apache.pdfbox:pdfbox:2.0.7") - api("org.asciidoctor:asciidoctorj:2.5.2") + api("org.asciidoctor:asciidoctorj:2.5.4") api("org.asciidoctor:asciidoctorj-pdf:1.6.0") api("org.assertj:assertj-core:3.11.1") api("org.hamcrest:hamcrest-core:1.3") From 30812e2731da56fff20755bff8abf06fd7959cc8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:36:34 +0100 Subject: [PATCH 066/198] Upgrade to REST Assured 5.1.1 Closes gh-836 --- docs/src/docs/asciidoc/getting-started.adoc | 2 +- docs/src/docs/asciidoc/introduction.adoc | 2 +- samples/rest-assured/build.gradle | 2 +- spring-restdocs-platform/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8480f923..430d71d5 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -76,7 +76,7 @@ Spring REST Docs has the following minimum requirements: * Java 17 * Spring Framework 6 -Additionally, the `spring-restdocs-restassured` module requires REST Assured 5. +Additionally, the `spring-restdocs-restassured` module requires REST Assured 5.1. [[getting-started-build-configuration]] === Build configuration diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 55d38b15..eb40443b 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -9,7 +9,7 @@ To this end, Spring REST Docs uses https://asciidoctor.org[Asciidoctor] by defau Asciidoctor processes plain text and produces HTML, styled and laid out to suit your needs. If you prefer, you can also configure Spring REST Docs to use Markdown. -Spring REST Docs uses snippets produced by tests written with Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or https://rest-assured.io/[REST Assured 4]. +Spring REST Docs uses snippets produced by tests written with Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or https://rest-assured.io/[REST Assured 5]. This test-driven approach helps to guarantee the accuracy of your service's documentation. If a snippet is incorrect, the test that produces it fails. diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index df839e10..ab8aba31 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'org.springframework:spring-webflux' - testImplementation 'io.rest-assured:rest-assured:5.0.1' + testImplementation 'io.rest-assured:rest-assured:5.1.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' testImplementation "org.springframework.restdocs:spring-restdocs-restassured:$restdocsVersion" testImplementation 'org.springframework:spring-test' diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 2bdf18c9..fc89328d 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -12,7 +12,7 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api:5.0.0") api("jakarta.validation:jakarta.validation-api:3.0.0") api("junit:junit:4.12") - api("io.rest-assured:rest-assured:5.0.1") + api("io.rest-assured:rest-assured:5.1.1") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.4") api("org.asciidoctor:asciidoctorj-pdf:1.6.0") From 5ff5e147b33c7a3c1b2f829842ee4d19d2af702b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:37:13 +0100 Subject: [PATCH 067/198] Upgrade to AsciidoctorJ PDF 2.1.4 Closes gh-837 --- spring-restdocs-platform/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index fc89328d..c1d78f79 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -15,7 +15,7 @@ dependencies { api("io.rest-assured:rest-assured:5.1.1") api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.4") - api("org.asciidoctor:asciidoctorj-pdf:1.6.0") + api("org.asciidoctor:asciidoctorj-pdf:2.1.4") api("org.assertj:assertj-core:3.11.1") api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") From cd5fed45f4ea2c7a9b8a201cdd30878f4f1602bd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:37:56 +0100 Subject: [PATCH 068/198] Upgrade to AssertJ 3.23.1 Closes gh-838 --- spring-restdocs-platform/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index c1d78f79..4732262e 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -16,7 +16,7 @@ dependencies { api("org.apache.pdfbox:pdfbox:2.0.7") api("org.asciidoctor:asciidoctorj:2.5.4") api("org.asciidoctor:asciidoctorj-pdf:2.1.4") - api("org.assertj:assertj-core:3.11.1") + 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:7.0.4.Final") From cbfec7f26d5c2b5e66f8f2c4e09dfb9f7aa9818c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:38:27 +0100 Subject: [PATCH 069/198] Upgrade to Moneta 1.4.2 Closes gh-839 --- spring-restdocs-platform/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 4732262e..2e3d976d 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -20,7 +20,7 @@ dependencies { api("org.hamcrest:hamcrest-core:1.3") api("org.hamcrest:hamcrest-library:1.3") api("org.hibernate.validator:hibernate-validator:7.0.4.Final") - api("org.javamoney:moneta:1.1") + api("org.javamoney:moneta:1.4.2") api("org.jruby:jruby-complete:9.1.13.0") api("org.junit.jupiter:junit-jupiter-api:5.0.0") api("org.mockito:mockito-core:4.2.0") From 3a87d5f0945dc816180bb8c7a96d83c7dc2d2eef Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:38:53 +0100 Subject: [PATCH 070/198] Upgrade to Mockito 4.6.1 Closes gh-840 --- spring-restdocs-platform/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle index 2e3d976d..25dd9153 100644 --- a/spring-restdocs-platform/build.gradle +++ b/spring-restdocs-platform/build.gradle @@ -23,7 +23,7 @@ dependencies { api("org.javamoney:moneta:1.4.2") api("org.jruby:jruby-complete:9.1.13.0") api("org.junit.jupiter:junit-jupiter-api:5.0.0") - api("org.mockito:mockito-core:4.2.0") + api("org.mockito:mockito-core:4.6.1") } api(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.13.3")) api(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) From c3a193c6a29afbe8a74de3cea56c8badbe3e8b69 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Jul 2022 17:56:13 +0100 Subject: [PATCH 071/198] Polish HTTP cookies documentation See gh-592 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 9adae2ca..92cb7bd1 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -1037,7 +1037,13 @@ include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies] The result is a snippet named `request-cookies.adoc` and a snippet named `response-cookies.adoc`. Each contains a table describing the cookies. -When documenting HTTP Cookies, the test fails if a documented cookie is not found in the request or response. +When documenting HTTP cookies, the test fails if an undocumented cookie is found in the request or response. +Similarly, the test also fails if a documented cookie is not found and the cookie has not been marked as optional. +You can also document cookies in a relaxed mode, where any undocumented cookies do not cause a test failure. +To do so, use the `relaxedRequestCookies` and `relaxedResponseCookies` methods on `org.springframework.restdocs.cookies.CookieDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the cookies. +If you do not want to document a cookie, you can mark it as ignored. +Doing so prevents it from appearing in the generated snippet while avoiding the failure described earlier. From b4be34bf8efe1494709a30a64b5ceb6daa1ffbcc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 12 Oct 2022 10:17:01 +0100 Subject: [PATCH 072/198] Remove support for modifying request parameters Request parameters are a server-side constructor, specific to the servlet specification. Rather than modifying request parameters the query string of the URI or the form URL encoded content of the body should be modified instead. Closes gh-846 --- .../customizing-requests-and-responses.adoc | 7 - ...ametersModifyingOperationPreprocessor.java | 176 ------------------ .../operation/preprocess/Preprocessors.java | 10 - ...rsModifyingOperationPreprocessorTests.java | 147 --------------- 4 files changed, 340 deletions(-) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 59177d3d..b5151269 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -123,13 +123,6 @@ Any occurrences that match a regular expression are replaced. -[[customizing-requests-and-responses-preprocessors-modify-request-parameters]] -==== Modifying Request Parameters - -You can use `modifyParameters` on `Preprocessors` to add, set, and remove request parameters. - - - [[customizing-requests-and-responses-preprocessors-modify-uris]] ==== Modifying URIs diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java deleted file mode 100644 index 94321343..00000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java +++ /dev/null @@ -1,176 +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.operation.preprocess; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.util.Assert; - -/** - * An {@link OperationPreprocessor} that can be used to modify a request's - * {@link OperationRequest#getParameters()} by adding, setting, and removing parameters. - * - * @author Andy Wilkinson - * @since 1.1.0 - */ -public final class ParametersModifyingOperationPreprocessor extends OperationPreprocessorAdapter { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final List modifications = new ArrayList<>(); - - @Override - public OperationRequest preprocess(OperationRequest request) { - Parameters parameters = new Parameters(); - parameters.putAll(request.getParameters()); - for (Modification modification : this.modifications) { - modification.apply(parameters); - } - return this.requestFactory.createFrom(request, parameters); - } - - /** - * Adds a parameter with the given {@code name} and {@code value}. - * @param name the name - * @param value the value - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor add(String name, String value) { - this.modifications.add(new AddParameterModification(name, value)); - return this; - } - - /** - * Sets the parameter with the given {@code name} to have the given {@code values}. - * @param name the name - * @param values the values - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor set(String name, String... values) { - Assert.notEmpty(values, "At least one value must be provided"); - this.modifications.add(new SetParameterModification(name, Arrays.asList(values))); - return this; - } - - /** - * Removes the parameter with the given {@code name}. - * @param name the name of the parameter - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor remove(String name) { - this.modifications.add(new RemoveParameterModification(name)); - return this; - } - - /** - * Removes the given {@code value} from the parameter with the given {@code name}. - * @param name the name - * @param value the value - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor remove(String name, String value) { - this.modifications.add(new RemoveValueParameterModification(name, value)); - return this; - } - - private interface Modification { - - void apply(Parameters parameters); - - } - - private static final class AddParameterModification implements Modification { - - private final String name; - - private final String value; - - private AddParameterModification(String name, String value) { - this.name = name; - this.value = value; - } - - @Override - public void apply(Parameters parameters) { - parameters.add(this.name, this.value); - } - - } - - private static final class SetParameterModification implements Modification { - - private final String name; - - private final List values; - - private SetParameterModification(String name, List values) { - this.name = name; - this.values = values; - } - - @Override - public void apply(Parameters parameters) { - parameters.put(this.name, this.values); - } - - } - - private static final class RemoveParameterModification implements Modification { - - private final String name; - - private RemoveParameterModification(String name) { - this.name = name; - } - - @Override - public void apply(Parameters parameters) { - parameters.remove(this.name); - } - - } - - private static final class RemoveValueParameterModification implements Modification { - - private final String name; - - private final String value; - - private RemoveValueParameterModification(String name, String value) { - this.name = name; - this.value = value; - } - - @Override - public void apply(Parameters parameters) { - List values = parameters.get(this.name); - if (values != null) { - values.remove(this.value); - if (values.isEmpty()) { - parameters.remove(this.name); - } - } - } - - } - -} 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 e6bf13d9..ba741ff1 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 @@ -137,16 +137,6 @@ public static OperationPreprocessor replacePattern(Pattern pattern, String repla return new ContentModifyingOperationPreprocessor(new PatternReplacingContentModifier(pattern, replacement)); } - /** - * Returns a {@code ParametersModifyingOperationPreprocessor} that can then be - * configured to modify the parameters of the request. - * @return the preprocessor - * @since 1.1.0 - */ - public static ParametersModifyingOperationPreprocessor modifyParameters() { - return new ParametersModifyingOperationPreprocessor(); - } - /** * Returns a {@code HeadersModifyingOperationPreprocessor} that can then be configured * to modify the headers of the request or response. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java deleted file mode 100644 index 3331102c..00000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2014-2020 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.operation.preprocess; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ParametersModifyingOperationPreprocessor}. - * - * @author Andy Wilkinson - */ -public class ParametersModifyingOperationPreprocessorTests { - - private final ParametersModifyingOperationPreprocessor preprocessor = new ParametersModifyingOperationPreprocessor(); - - @Test - public void addNewParameter() { - Parameters parameters = new Parameters(); - OperationRequest request = this.preprocessor.add("a", "alpha").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha")); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080?a=alpha")); - } - - @Test - public void addValueToExistingParameter() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - OperationRequest request = this.preprocessor.add("a", "alpha").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("apple", "alpha")); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080?a=apple&a=alpha")); - } - - @Test - public void setNewParameter() { - Parameters parameters = new Parameters(); - OperationRequest request = this.preprocessor.set("a", "alpha", "avocado") - .preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "avocado")); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080?a=alpha&a=avocado")); - } - - @Test - public void setExistingParameter() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - OperationRequest request = this.preprocessor.set("a", "alpha", "avocado") - .preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "avocado")); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080?a=alpha&a=avocado")); - } - - @Test - public void removeNonExistentParameter() { - Parameters parameters = new Parameters(); - OperationRequest request = this.preprocessor.remove("a").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters().size()).isEqualTo(0); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080")); - } - - @Test - public void removeParameter() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - OperationRequest request = this.preprocessor.remove("a").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).isEmpty(); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080")); - } - - @Test - public void removeParameterValueForNonExistentParameter() { - Parameters parameters = new Parameters(); - OperationRequest request = this.preprocessor.remove("a", "apple").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).isEmpty(); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080")); - } - - @Test - public void removeParameterValueWithMultipleValues() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - parameters.add("a", "alpha"); - parameters.add("b", "bravo"); - OperationRequest request = this.preprocessor.remove("a", "apple").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha")); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080?a=alpha&b=bravo")); - } - - @Test - public void removeParameterValueWithSingleValueRemovesEntryEntirely() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - parameters.add("b", "bravo"); - OperationRequest request = this.preprocessor.remove("a", "apple").preprocess(createGetRequest(parameters)); - assertThat(request.getParameters()).doesNotContainKey("a"); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080?b=bravo")); - } - - @Test - public void whenParametersOfANonGetRequestAreModifiedThenTheQueryStringIsUnaffected() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - parameters.add("b", "bravo"); - OperationRequest request = this.preprocessor.remove("a", "apple").preprocess(createPostRequest(parameters)); - assertThat(request.getParameters()).doesNotContainKey("a"); - assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080")); - } - - private OperationRequest createGetRequest(Parameters parameters) { - return new OperationRequestFactory().create( - URI.create("http://localhost:8080" + (parameters.isEmpty() ? "" : "?" + parameters.toQueryString())), - HttpMethod.GET, new byte[0], new HttpHeaders(), parameters, - Collections.emptyList()); - } - - private OperationRequest createPostRequest(Parameters parameters) { - return new OperationRequestFactory().create(URI.create("http://localhost:8080"), HttpMethod.POST, new byte[0], - new HttpHeaders(), parameters, Collections.emptyList()); - } - -} From f5a629af34b461cd6a1ea691c47deba9b037c30b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 10 Oct 2022 15:28:30 +0100 Subject: [PATCH 073/198] Handle form and query parameters separately Previously, form and query parameters were handled together as request parameters. Howeer, request parameters are a server-side construct that's specific to the servlet specification. As such they're not appropriate for the client-side documentation that Spring REST Docs aims to produce. This commit replaces support for documenting request parameters with support for documenting query paramters found in the query string of the request's URI and for documenting form parameters found in the form URL encoded body of the request. Closes gh-832 --- .../docs/asciidoc/documenting-your-api.adoc | 81 +++-- .../com/example/mockmvc/FormParameters.java | 40 +++ ...stParameters.java => QueryParameters.java} | 19 +- .../example/restassured/FormParameters.java | 40 +++ ...stParameters.java => QueryParameters.java} | 21 +- ...estParameters.java => FormParameters.java} | 27 +- .../webtestclient/QueryParameters.java | 42 +++ .../restdocs/cli/CliOperationRequest.java | 16 - .../restdocs/cli/CurlRequestSnippet.java | 37 +-- .../restdocs/cli/HttpieRequestSnippet.java | 84 ++--- .../restdocs/http/HttpRequestSnippet.java | 39 +-- ...yStringParser.java => FormParameters.java} | 50 +-- .../restdocs/operation/OperationRequest.java | 10 +- .../operation/OperationRequestFactory.java | 48 +-- .../restdocs/operation/Parameters.java | 115 ------- .../restdocs/operation/QueryParameters.java | 90 +++++ .../operation/StandardOperationRequest.java | 13 +- .../UriModifyingOperationPreprocessor.java | 6 +- ...nippet.java => FormParametersSnippet.java} | 52 ++- .../request/QueryParametersSnippet.java | 136 ++++++++ .../request/RequestDocumentation.java | 314 +++++++++++++----- .../default-form-parameters.snippet | 9 + .../default-query-parameters.snippet | 9 + .../markdown/default-form-parameters.snippet | 5 + .../markdown/default-query-parameters.snippet | 5 + .../restdocs/cli/CurlRequestSnippetTests.java | 136 ++------ .../cli/HttpieRequestSnippetTests.java | 144 ++------ .../http/HttpRequestSnippetTests.java | 155 +-------- .../restdocs/operation/ParametersTests.java | 75 ----- .../operation/QueryStringParserTests.java | 93 ------ ...ntModifyingOperationPreprocessorTests.java | 8 +- ...rsModifyingOperationPreprocessorTests.java | 3 +- ...riModifyingOperationPreprocessorTests.java | 16 +- ...=> FormParametersSnippetFailureTests.java} | 22 +- ...s.java => FormParametersSnippetTests.java} | 124 ++++--- .../QueryParametersSnippetFailureTests.java | 68 ++++ .../request/QueryParametersSnippetTests.java | 177 ++++++++++ ...form-parameters-with-extra-column.snippet} | 0 ...m-parameters-with-optional-column.snippet} | 0 ...pet => form-parameters-with-title.snippet} | 0 ...query-parameters-with-extra-column.snippet | 10 + ...ry-parameters-with-optional-column.snippet | 10 + .../query-parameters-with-title.snippet | 10 + ...form-parameters-with-extra-column.snippet} | 0 ...m-parameters-with-optional-column.snippet} | 0 ...pet => form-parameters-with-title.snippet} | 0 ...query-parameters-with-extra-column.snippet | 5 + ...ry-parameters-with-optional-column.snippet | 5 + .../query-parameters-with-title.snippet | 6 + .../testfixtures/GeneratedSnippets.java | 8 +- .../testfixtures/OperationBuilder.java | 20 +- .../mockmvc/MockMvcRequestConverter.java | 172 +++++++--- .../mockmvc/MockMvcRequestConverterTests.java | 28 +- ...kMvcRestDocumentationIntegrationTests.java | 14 +- spring-restdocs-restassured/build.gradle | 1 + .../RestAssuredRequestConverter.java | 82 +++-- .../RestAssuredParameterBehaviorTests.java | 220 ++++++++++++ .../RestAssuredRequestConverterTests.java | 21 +- ...uredRestDocumentationIntegrationTests.java | 20 +- .../restdocs/restassured/TomcatServer.java | 37 +++ .../WebTestClientRequestConverter.java | 24 +- .../WebTestClientRequestConverterTests.java | 21 +- ...ientRestDocumentationIntegrationTests.java | 15 +- 63 files changed, 1716 insertions(+), 1342 deletions(-) create mode 100644 docs/src/test/java/com/example/mockmvc/FormParameters.java rename docs/src/test/java/com/example/mockmvc/{RequestParameters.java => QueryParameters.java} (67%) create mode 100644 docs/src/test/java/com/example/restassured/FormParameters.java rename docs/src/test/java/com/example/restassured/{RequestParameters.java => QueryParameters.java} (68%) rename docs/src/test/java/com/example/webtestclient/{RequestParameters.java => FormParameters.java} (64%) create mode 100644 docs/src/test/java/com/example/webtestclient/QueryParameters.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/{QueryStringParser.java => FormParameters.java} (55%) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/request/{RequestParametersSnippet.java => FormParametersSnippet.java} (62%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java rename spring-restdocs-core/src/test/java/org/springframework/restdocs/request/{RequestParametersSnippetFailureTests.java => FormParametersSnippetFailureTests.java} (68%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/request/{RequestParametersSnippetTests.java => FormParametersSnippetTests.java} (54%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java rename spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/{request-parameters-with-extra-column.snippet => form-parameters-with-extra-column.snippet} (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/{request-parameters-with-optional-column.snippet => form-parameters-with-optional-column.snippet} (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/{request-parameters-with-title.snippet => form-parameters-with-title.snippet} (100%) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet rename spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/{request-parameters-with-extra-column.snippet => form-parameters-with-extra-column.snippet} (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/{request-parameters-with-optional-column.snippet => form-parameters-with-optional-column.snippet} (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/{request-parameters-with-title.snippet => form-parameters-with-title.snippet} (100%) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 92cb7bd1..15d3a520 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -649,21 +649,20 @@ For example, the preceding code results in a snippet named `response-fields-bene -[[documenting-your-api-request-parameters]] -=== Request Parameters +[[documenting-your-api-query-parameters]] +=== Query Parameters -You can document a request's parameters by using `requestParameters`. -You can include request parameters in a `GET` request's query string. +You can document a request's query parameters by using `queryParameters`. The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/mockmvc/QueryParameters.java[tags=query-parameters] ---- <1> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. -<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. -Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's query parameters. +Uses the static `queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. @@ -671,11 +670,11 @@ Uses the static `parameterWithName` method on `org.springframework.restdocs.requ [source,java,indent=0,role="secondary"] .WebTestClient ---- -include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/webtestclient/QueryParameters.java[tags=query-parameters] ---- <1> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. -<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. -Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's query parameters. +Uses the static `queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. @@ -683,51 +682,77 @@ Uses the static `parameterWithName` method on `org.springframework.restdocs.requ [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/restassured/QueryParameters.java[tags=query-parameters] ---- -<1> Configure Spring REST Docs to produce a snippet describing the request's parameters. -Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<1> Configure Spring REST Docs to produce a snippet describing the request's query parameters. +Uses the static `queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <2> Document the `page` parameter. Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `per_page` parameter. <4> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. -You can also include request parameters as form data in the body of a POST request. +When documenting query parameters, the test fails if an undocumented query parameter is used in the request's query string. +Similarly, the test also fails if a documented query parameter is not found in the request's query string and the parameter has not been marked as optional. + +If you do not want to document a query parameter, you can mark it as ignored. +This prevents it from appearing in the generated snippet while avoiding the failure described above. + +You can also document query parameters in a relaxed mode where any undocumented parameters do not cause a test failure. +To do so, use the `relaxedQueryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the query parameters. + + + +[[documenting-your-api-form-parameters]] +=== Form Parameters + +You can document a request's form parameters by using `formParameters`. The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/mockmvc/FormParameters.java[tags=form-parameters] ---- -<1> Perform a `POST` request with a single parameter, `username`. +<1> Perform a `POST` request with a single form parameter, `username`. +<2> Configure Spring REST Docs to produce a snippet describing the request's form parameters. +Uses the static `formParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `username` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. [source,java,indent=0,role="secondary"] .WebTestClient ---- -include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/webtestclient/FormParameters.java[tags=form-parameters] ---- -<1> Perform a `POST` request with a single parameter, `username`. +<1> Perform a `POST` request with a single form parameter, `username`. +<2> Configure Spring REST Docs to produce a snippet describing the request's form parameters. +Uses the static `formParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `username` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/restassured/FormParameters.java[tags=form-parameters] ---- -<1> Configure the `username` parameter. -<2> Perform the `POST` request. +<1> Configure Spring REST Docs to produce a snippet describing the request's form parameters. +Uses the static `formParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the `username` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Perform a `POST` request with a single form parameter, `username`. -In all cases, the result is a snippet named `request-parameters.adoc` that contains a table describing the parameters that are supported by the resource. +In all cases, the result is a snippet named `form-parameters.adoc` that contains a table describing the form parameters that are supported by the resource. -When documenting request parameters, the test fails if an undocumented request parameter is used in the request. -Similarly, the test also fails if a documented request parameter is not found in the request and the request parameter has not been marked as optional. +When documenting form parameters, the test fails if an undocumented form parameter is used in the request body. +Similarly, the test also fails if a documented form parameter is not found in the request body and the form parameter has not been marked as optional. -If you do not want to document a request parameter, you can mark it as ignored. +If you do not want to document a form parameter, you can mark it as ignored. This prevents it from appearing in the generated snippet while avoiding the failure described above. -You can also document request parameters in a relaxed mode where any undocumented parameters do not cause a test failure. -To do so, use the `relaxedRequestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. -This can be useful when documenting a particular scenario where you only want to focus on a subset of the request parameters. +You can also document form parameters in a relaxed mode where any undocumented parameters do not cause a test failure. +To do so, use the `relaxedFormParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the form parameters. diff --git a/docs/src/test/java/com/example/mockmvc/FormParameters.java b/docs/src/test/java/com/example/mockmvc/FormParameters.java new file mode 100644 index 00000000..8000286f --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/FormParameters.java @@ -0,0 +1,40 @@ +/* + * 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 com.example.mockmvc; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.formParameters; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class FormParameters { + + private MockMvc mockMvc; + + public void postFormDataSnippet() throws Exception { + // tag::form-parameters[] + this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> + .andExpect(status().isCreated()).andDo(document("create-user", formParameters(// <2> + parameterWithName("username").description("The user's username") // <3> + ))); + // end::form-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/RequestParameters.java b/docs/src/test/java/com/example/mockmvc/QueryParameters.java similarity index 67% rename from docs/src/test/java/com/example/mockmvc/RequestParameters.java rename to docs/src/test/java/com/example/mockmvc/QueryParameters.java index 1f206a94..bd9d6b50 100644 --- a/docs/src/test/java/com/example/mockmvc/RequestParameters.java +++ b/docs/src/test/java/com/example/mockmvc/QueryParameters.java @@ -20,31 +20,22 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -public class RequestParameters { +public class QueryParameters { private MockMvc mockMvc; public void getQueryStringSnippet() throws Exception { - // tag::request-parameters-query-string[] + // tag::query-parameters[] this.mockMvc.perform(get("/users?page=2&per_page=100")) // <1> - .andExpect(status().isOk()).andDo(document("users", requestParameters(// <2> + .andExpect(status().isOk()).andDo(document("users", queryParameters(// <2> parameterWithName("page").description("The page to retrieve"), // <3> parameterWithName("per_page").description("Entries per page") // <4> ))); - // end::request-parameters-query-string[] - } - - public void postFormDataSnippet() throws Exception { - // tag::request-parameters-form-data[] - this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> - .andExpect(status().isCreated()).andDo(document("create-user", - requestParameters(parameterWithName("username").description("The user's username")))); - // end::request-parameters-form-data[] + // end::query-parameters[] } } diff --git a/docs/src/test/java/com/example/restassured/FormParameters.java b/docs/src/test/java/com/example/restassured/FormParameters.java new file mode 100644 index 00000000..e2310f05 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/FormParameters.java @@ -0,0 +1,40 @@ +/* + * 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 com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.formParameters; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class FormParameters { + + private RequestSpecification spec; + + public void postFormDataSnippet() { + // tag::form-parameters[] + RestAssured.given(this.spec).filter(document("create-user", formParameters(// <1> + parameterWithName("username").description("The user's username")))) // <2> + .formParam("username", "Tester").when().post("/users") // <3> + .then().assertThat().statusCode(is(200)); + // end::form-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/QueryParameters.java similarity index 68% rename from docs/src/test/java/com/example/restassured/RequestParameters.java rename to docs/src/test/java/com/example/restassured/QueryParameters.java index a6189771..7656e4e7 100644 --- a/docs/src/test/java/com/example/restassured/RequestParameters.java +++ b/docs/src/test/java/com/example/restassured/QueryParameters.java @@ -21,32 +21,21 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -public class RequestParameters { +public class QueryParameters { private RequestSpecification spec; public void getQueryStringSnippet() { - // tag::request-parameters-query-string[] - RestAssured.given(this.spec).filter(document("users", requestParameters(// <1> + // tag::query-parameters[] + RestAssured.given(this.spec).filter(document("users", queryParameters(// <1> parameterWithName("page").description("The page to retrieve"), // <2> parameterWithName("per_page").description("Entries per page")))) // <3> .when().get("/users?page=2&per_page=100") // <4> .then().assertThat().statusCode(is(200)); - // end::request-parameters-query-string[] - } - - public void postFormDataSnippet() { - // tag::request-parameters-form-data[] - RestAssured.given(this.spec) - .filter(document("create-user", - requestParameters(parameterWithName("username").description("The user's username")))) - .formParam("username", "Tester") // <1> - .when().post("/users") // <2> - .then().assertThat().statusCode(is(200)); - // end::request-parameters-form-data[] + // end::query-parameters[] } } diff --git a/docs/src/test/java/com/example/webtestclient/RequestParameters.java b/docs/src/test/java/com/example/webtestclient/FormParameters.java similarity index 64% rename from docs/src/test/java/com/example/webtestclient/RequestParameters.java rename to docs/src/test/java/com/example/webtestclient/FormParameters.java index de0cb239..0bfdbc0c 100644 --- a/docs/src/test/java/com/example/webtestclient/RequestParameters.java +++ b/docs/src/test/java/com/example/webtestclient/FormParameters.java @@ -21,37 +21,26 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserters; +import static org.springframework.restdocs.request.RequestDocumentation.formParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; -public class RequestParameters { +public class FormParameters { // @formatter:off private WebTestClient webTestClient; - public void getQueryStringSnippet() { - // tag::request-parameters-query-string[] - this.webTestClient.get().uri("/users?page=2&per_page=100") // <1> - .exchange().expectStatus().isOk().expectBody() - .consumeWith(document("users", requestParameters(// <2> - parameterWithName("page").description("The page to retrieve"), // <3> - parameterWithName("per_page").description("Entries per page") // <4> - ))); - // end::request-parameters-query-string[] - } - public void postFormDataSnippet() { - // tag::request-parameters-form-data[] + // tag::form-parameters[] MultiValueMap formData = new LinkedMultiValueMap<>(); formData.add("username", "Tester"); this.webTestClient.post().uri("/users").body(BodyInserters.fromFormData(formData)) // <1> - .exchange().expectStatus().isCreated().expectBody() - .consumeWith(document("create-user", requestParameters( - parameterWithName("username").description("The user's username") - ))); - // end::request-parameters-form-data[] + .exchange().expectStatus().isCreated().expectBody() + .consumeWith(document("create-user", formParameters(// <2> + parameterWithName("username").description("The user's username") // <3> + ))); + // end::form-parameters[] } } diff --git a/docs/src/test/java/com/example/webtestclient/QueryParameters.java b/docs/src/test/java/com/example/webtestclient/QueryParameters.java new file mode 100644 index 00000000..347c700b --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/QueryParameters.java @@ -0,0 +1,42 @@ +/* + * 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 com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class QueryParameters { + + // @formatter:off + + private WebTestClient webTestClient; + + public void getQueryStringSnippet() { + // tag::query-parameters[] + this.webTestClient.get().uri("/users?page=2&per_page=100") // <1> + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("users", queryParameters(// <2> + parameterWithName("page").description("The page to retrieve"), // <3> + parameterWithName("per_page").description("Entries per page") // <4> + ))); + // end::query-parameters[] + } + +} 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 74f5979b..d3b2f9f2 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 @@ -24,13 +24,11 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.Base64Utils; @@ -65,15 +63,6 @@ String getBasicAuthCredentials() { return null; } - Parameters getNonPartParameters() { - Parameters parameters = getParameters(); - Parameters nonPartParameters = new Parameters(); - nonPartParameters.putAll(parameters); - Set partNames = getParts().stream().map(OperationRequestPart::getName).collect(Collectors.toSet()); - nonPartParameters.keySet().removeAll(partNames); - return nonPartParameters; - } - @Override public byte[] getContent() { return this.delegate.getContent(); @@ -115,11 +104,6 @@ public HttpMethod getMethod() { return this.delegate.getMethod(); } - @Override - public Parameters getParameters() { - return this.delegate.getParameters(); - } - @Override public Collection getParts() { return this.delegate.getParts(); 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 82b0e36f..358db25a 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 @@ -22,12 +22,11 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.http.HttpMethod; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -82,21 +81,9 @@ protected Map createModel(Operation operation) { private String getUrl(Operation operation) { OperationRequest request = operation.getRequest(); - Parameters uniqueParameters = request.getParameters().getUniqueParameters(operation.getRequest().getUri()); - if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { - return String.format("'%s%s%s'", request.getUri(), - StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", uniqueParameters.toQueryString()); - } return String.format("'%s'", request.getUri()); } - private boolean includeParametersInUri(OperationRequest request) { - HttpMethod method = request.getMethod(); - return (method != HttpMethod.PUT && method != HttpMethod.POST && method != HttpMethod.PATCH) - || (request.getContent().length > 0 && !MediaType.APPLICATION_FORM_URLENCODED - .isCompatibleWith(request.getHeaders().getContentType())); - } - private String getOptions(Operation operation) { StringBuilder builder = new StringBuilder(); writeIncludeHeadersInOutputOption(builder); @@ -147,6 +134,10 @@ private void writeHttpMethod(OperationRequest request, StringBuilder builder) { private void writeHeaders(CliOperationRequest request, List lines) { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { + if (StringUtils.hasText(request.getContentAsString()) && HttpHeaders.CONTENT_TYPE.equals(entry.getKey()) + && MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { + continue; + } lines.add(String.format("-H '%s: %s'", entry.getKey(), header)); } } @@ -176,24 +167,6 @@ private void writeContent(CliOperationRequest request, List lines) { if (StringUtils.hasText(content)) { lines.add(String.format("-d '%s'", content)); } - else if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getNonPartParameters().entrySet()) { - for (String value : entry.getValue()) { - lines.add(String.format("-F '%s=%s'", entry.getKey(), value)); - } - } - } - else if (request.isPutOrPost()) { - writeContentUsingParameters(request, lines); - } - } - - private void writeContentUsingParameters(OperationRequest request, List lines) { - Parameters uniqueParameters = request.getParameters().getUniqueParameters(request.getUri()); - String queryString = uniqueParameters.toQueryString(); - if (StringUtils.hasText(queryString)) { - lines.add(String.format("-d '%s'", queryString)); - } } } 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 314a8997..30912429 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 @@ -25,12 +25,11 @@ import java.util.Map.Entry; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.FormParameters; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -85,6 +84,9 @@ protected Map createModel(Operation operation) { } private Object getContentStandardIn(CliOperationRequest request) { + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())) { + return ""; + } String content = request.getContentAsString(); if (StringUtils.hasText(content)) { return String.format("echo '%s' | ", content); @@ -102,21 +104,15 @@ private String getOptions(CliOperationRequest request) { } private String getUrl(OperationRequest request) { - Parameters uniqueParameters = request.getParameters().getUniqueParameters(request.getUri()); - if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { - return String.format("'%s%s%s'", request.getUri(), - StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", uniqueParameters.toQueryString()); - } return String.format("'%s'", request.getUri()); } private String getRequestItems(CliOperationRequest request) { List lines = new ArrayList<>(); - writeFormDataIfNecessary(request, lines); writeHeaders(request, lines); writeCookies(request, lines); - writeParametersIfNecessary(request, lines); + writeFormDataIfNecessary(request, lines); return this.commandFormatter.format(lines); } @@ -125,24 +121,11 @@ private void writeOptions(OperationRequest request, PrintWriter writer) { if (!request.getParts().isEmpty()) { writer.print("--multipart "); } - else if (!request.getParameters().getUniqueParameters(request.getUri()).isEmpty() - && !includeParametersInUri(request) && includeParametersAsFormOptions(request)) { + else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())) { writer.print("--form "); } } - private boolean includeParametersInUri(OperationRequest request) { - HttpMethod method = request.getMethod(); - return (method != HttpMethod.PUT && method != HttpMethod.POST && method != HttpMethod.PATCH) - || (request.getContent().length > 0 && !MediaType.APPLICATION_FORM_URLENCODED - .isCompatibleWith(request.getHeaders().getContentType())); - } - - private boolean includeParametersAsFormOptions(OperationRequest request) { - return request.getMethod() != HttpMethod.GET && (request.getContent().length == 0 - || !MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())); - } - private void writeUserOptionIfNecessary(CliOperationRequest request, PrintWriter writer) { String credentials = request.getBasicAuthCredentials(); if (credentials != null) { @@ -155,23 +138,33 @@ private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer } private void writeFormDataIfNecessary(OperationRequest request, List lines) { - for (OperationRequestPart part : request.getParts()) { - StringBuilder oneLine = new StringBuilder(); - oneLine.append(String.format("'%s'", part.getName())); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - oneLine.append(String.format("='%s'", part.getContentAsString())); - } - else { - oneLine.append(String.format("@'%s'", part.getSubmittedFileName())); - } + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())) { + FormParameters.from(request).forEach( + (key, values) -> values.forEach((value) -> lines.add(String.format("'%s=%s'", key, value)))); + } + else { + for (OperationRequestPart part : request.getParts()) { + StringBuilder oneLine = new StringBuilder(); + oneLine.append(String.format("'%s'", part.getName())); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + oneLine.append(String.format("='%s'", part.getContentAsString())); + } + else { + oneLine.append(String.format("@'%s'", part.getSubmittedFileName())); + } - lines.add(oneLine.toString()); + lines.add(oneLine.toString()); + } } } private void writeHeaders(OperationRequest request, List lines) { HttpHeaders headers = request.getHeaders(); for (Entry> entry : headers.entrySet()) { + if (entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && headers.getContentType().isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) { + continue; + } for (String header : entry.getValue()) { // HTTPie adds Content-Type automatically with --form if (!request.getParts().isEmpty() && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) @@ -189,29 +182,4 @@ private void writeCookies(OperationRequest request, List lines) { } } - private void writeParametersIfNecessary(CliOperationRequest request, List lines) { - if (StringUtils.hasText(request.getContentAsString())) { - return; - } - if (!request.getParts().isEmpty()) { - writeContentUsingParameters(request.getNonPartParameters(), lines); - } - else if (request.isPutOrPost()) { - writeContentUsingParameters(request.getParameters().getUniqueParameters(request.getUri()), lines); - } - } - - private void writeContentUsingParameters(Parameters parameters, List lines) { - for (Map.Entry> entry : parameters.entrySet()) { - if (entry.getValue().isEmpty()) { - lines.add(String.format("'%s='", entry.getKey())); - } - else { - for (String value : entry.getValue()) { - lines.add(String.format("'%s=%s'", entry.getKey(), value)); - } - } - } - } - } 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 59d75af1..b375b626 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 @@ -23,8 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; -import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -32,7 +30,6 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -78,15 +75,6 @@ protected Map createModel(Operation operation) { private String getPath(OperationRequest request) { String path = request.getUri().getRawPath(); String queryString = request.getUri().getRawQuery(); - Parameters uniqueParameters = request.getParameters().getUniqueParameters(request.getUri()); - if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { - if (StringUtils.hasText(queryString)) { - queryString = queryString + "&" + uniqueParameters.toQueryString(); - } - else { - queryString = uniqueParameters.toQueryString(); - } - } if (StringUtils.hasText(queryString)) { path = path + "?" + queryString; } @@ -133,14 +121,7 @@ private String getRequestBody(OperationRequest request) { writer.printf("%n%s", content); } else if (isPutOrPost(request)) { - if (request.getParts().isEmpty()) { - String queryString = request.getParameters().getUniqueParameters(request.getUri()).toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.println(); - writer.print(queryString); - } - } - else { + if (!request.getParts().isEmpty()) { writeParts(request, writer); } } @@ -153,23 +134,6 @@ private boolean isPutOrPost(OperationRequest request) { private void writeParts(OperationRequest request, PrintWriter writer) { writer.println(); - Set partNames = request.getParts().stream().map(OperationRequestPart::getName) - .collect(Collectors.toSet()); - for (Entry> parameter : request.getParameters().entrySet()) { - if (!partNames.contains(parameter.getKey())) { - if (parameter.getValue().isEmpty()) { - writePartBoundary(writer); - writePart(parameter.getKey(), "", null, null, writer); - } - else { - for (String value : parameter.getValue()) { - writePartBoundary(writer); - writePart(parameter.getKey(), value, null, null, writer); - writer.println(); - } - } - } - } for (OperationRequestPart part : request.getParts()) { writePartBoundary(writer); writePart(part, writer); @@ -206,7 +170,6 @@ private void writeMultipartEnd(PrintWriter writer) { private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null && isPutOrPost(request) - && !request.getParameters().getUniqueParameters(request.getUri()).isEmpty() && !includeParametersInUri(request); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java similarity index 55% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java index 49d637dd..5cabb84e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -17,36 +17,46 @@ package org.springframework.restdocs.operation; import java.io.UnsupportedEncodingException; -import java.net.URI; import java.net.URLDecoder; import java.util.LinkedList; import java.util.List; import java.util.Scanner; +import org.springframework.util.LinkedMultiValueMap; + /** - * A parser for the query string of a URI. + * A request's form parameters, derived from its form URL encoded body content. * * @author Andy Wilkinson + * @since 3.0.0 */ -public class QueryStringParser { +public final class FormParameters extends LinkedMultiValueMap { + + private FormParameters() { + + } /** - * Parses the query string of the given {@code uri} and returns the resulting - * {@link Parameters}. - * @param uri the uri to parse - * @return the parameters parsed from the query string + * Extracts the form parameters from the body of the given {@code request}. If the + * request has no body content, an empty {@code FormParameters} is returned, rather + * than {@code null}. + * @param request the request + * @return the form parameters extracted from the body content */ - public Parameters parse(URI uri) { - String query = uri.getRawQuery(); - if (query != null) { - return parse(query); + public static FormParameters from(OperationRequest request) { + return of(request.getContentAsString()); + } + + private static FormParameters of(String bodyContent) { + if (bodyContent == null || bodyContent.length() == 0) { + return new FormParameters(); } - return new Parameters(); + return parse(bodyContent); } - private Parameters parse(String query) { - Parameters parameters = new Parameters(); - try (Scanner scanner = new Scanner(query)) { + private static FormParameters parse(String bodyContent) { + FormParameters parameters = new FormParameters(); + try (Scanner scanner = new Scanner(bodyContent)) { scanner.useDelimiter("&"); while (scanner.hasNext()) { processParameter(scanner.next(), parameters); @@ -55,7 +65,7 @@ private Parameters parse(String query) { return parameters; } - private void processParameter(String parameter, Parameters parameters) { + private static void processParameter(String parameter, FormParameters parameters) { String[] components = parameter.split("="); if (components.length > 0 && components.length < 3) { if (components.length == 2) { @@ -64,7 +74,7 @@ private void processParameter(String parameter, Parameters parameters) { parameters.add(decode(name), decode(value)); } else { - List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList()); + List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); values.add(""); } } @@ -73,12 +83,12 @@ private void processParameter(String parameter, Parameters parameters) { } } - private String decode(String encoded) { + private static String decode(String encoded) { try { return URLDecoder.decode(encoded, "UTF-8"); } catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + encoded + " using UTF-8", ex); + throw new IllegalStateException("Unable to URL decode " + encoded + " using UTF-8", ex); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index c8b6fb35..7e36c2f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * 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. @@ -58,14 +58,6 @@ public interface OperationRequest { */ HttpMethod getMethod(); - /** - * Returns the request's parameters. For a {@code GET} request, the parameters are - * derived from the query string. For a {@code POST} request, the parameters are - * derived form the request's body. - * @return the parameters - */ - Parameters getParameters(); - /** * Returns the request's parts, provided that it is a multipart request. If not, then * an empty {@link Collection} is returned. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 69eb7c2a..8d91c356 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * 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. @@ -17,7 +17,6 @@ package org.springframework.restdocs.operation; import java.net.URI; -import java.net.URISyntaxException; import java.util.Collection; import java.util.Collections; @@ -39,15 +38,15 @@ public class OperationRequestFactory { * @param method the request method * @param content the content of the request * @param headers the request's headers - * @param parameters the request's parameters * @param parts the request's parts * @param cookies the request's cookies * @return the {@code OperationRequest} + * @since 3.0.0 */ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, - Parameters parameters, Collection parts, Collection cookies) { - return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, uri, content), parameters, - parts, cookies); + Collection parts, Collection cookies) { + return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, uri, content), + (parts != null) ? parts : Collections.emptyList(), cookies); } /** @@ -58,13 +57,13 @@ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpH * @param method the request method * @param content the content of the request * @param headers the request's headers - * @param parameters the request's parameters * @param parts the request's parts * @return the {@code OperationRequest} + * @since 3.0.0 */ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, - Parameters parameters, Collection parts) { - return create(uri, method, content, headers, parameters, parts, Collections.emptyList()); + Collection parts) { + return create(uri, method, content, headers, parts, Collections.emptyList()); } /** @@ -77,8 +76,7 @@ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpH */ public OperationRequest createFrom(OperationRequest original, byte[] newContent) { return new StandardOperationRequest(original.getUri(), original.getMethod(), newContent, - getUpdatedHeaders(original.getHeaders(), newContent), original.getParameters(), original.getParts(), - original.getCookies()); + getUpdatedHeaders(original.getHeaders(), newContent), original.getParts(), original.getCookies()); } /** @@ -90,33 +88,7 @@ public OperationRequest createFrom(OperationRequest original, byte[] newContent) */ public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), newHeaders, - original.getParameters(), original.getParts(), original.getCookies()); - } - - /** - * Creates a new {@code OperationRequest} based on the given {@code original} but with - * the given {@code newParameters} applied. The query string of a {@code GET} request - * will be updated to reflect the new parameters. - * @param original the original request - * @param newParameters the new parameters - * @return the new request with the parameters applied - */ - public OperationRequest createFrom(OperationRequest original, Parameters newParameters) { - URI uri = (original.getMethod() == HttpMethod.GET) ? updateQueryString(original.getUri(), newParameters) - : original.getUri(); - return new StandardOperationRequest(uri, original.getMethod(), original.getContent(), original.getHeaders(), - newParameters, original.getParts(), original.getCookies()); - } - - private URI updateQueryString(URI originalUri, Parameters parameters) { - try { - return new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(), - originalUri.getPort(), originalUri.getPath(), - parameters.isEmpty() ? null : parameters.toQueryString(), originalUri.getFragment()); - } - catch (URISyntaxException ex) { - throw new RuntimeException(ex); - } + original.getParts(), original.getCookies()); } private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, byte[] content) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java deleted file mode 100644 index 0e3558e5..00000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ /dev/null @@ -1,115 +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.operation; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLEncoder; -import java.util.List; -import java.util.Map; - -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.StringUtils; - -/** - * The parameters received in a request. - * - * @author Andy Wilkinson - */ -@SuppressWarnings("serial") -public class Parameters extends LinkedMultiValueMap { - - /** - * Converts the parameters to a query string suitable for use in a URI or the body of - * a form-encoded request. - * @return the query string - */ - public String toQueryString() { - StringBuilder sb = new StringBuilder(); - for (Map.Entry> entry : entrySet()) { - if (entry.getValue().isEmpty()) { - append(sb, entry.getKey()); - } - else { - for (String value : entry.getValue()) { - append(sb, entry.getKey(), value); - } - } - } - return sb.toString(); - } - - /** - * Returns a new {@code Parameters} containing only the parameters that do no appear - * in the query string of the given {@code uri}. - * @param uri the uri - * @return the unique parameters - */ - public Parameters getUniqueParameters(URI uri) { - Parameters queryStringParameters = new QueryStringParser().parse(uri); - Parameters uniqueParameters = new Parameters(); - - for (Map.Entry> parameter : entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Map.Entry> parameter, Parameters queryStringParameters, - Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - - private static void append(StringBuilder sb, String key) { - append(sb, key, ""); - } - - private static void append(StringBuilder sb, String key, String value) { - doAppend(sb, urlEncodeUTF8(key) + "=" + urlEncodeUTF8(value)); - } - - private static void doAppend(StringBuilder sb, String toAppend) { - if (sb.length() > 0) { - sb.append("&"); - } - sb.append(toAppend); - } - - private static String urlEncodeUTF8(String s) { - if (!StringUtils.hasLength(s)) { - return ""; - } - try { - return URLEncoder.encode(s, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8", ex); - } - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java new file mode 100644 index 00000000..17f7fc1c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java @@ -0,0 +1,90 @@ +/* + * 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.operation; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +import org.springframework.util.LinkedMultiValueMap; + +/** + * A request's query parameters, derived from its URI's query string. + * + * @author Andy Wilkinson + * @since 3.0.0 + */ +public final class QueryParameters extends LinkedMultiValueMap { + + private QueryParameters() { + + } + + /** + * Extracts the query parameters from the query string of the given {@code request}. + * If the request has no query string, an empty {@code QueryParameters} is returned, + * rather than {@code null}. + * @param request the request + * @return the query parameters extracted from the request's query string + */ + public static QueryParameters from(OperationRequest request) { + return from(request.getUri().getRawQuery()); + } + + private static QueryParameters from(String queryString) { + if (queryString == null || queryString.length() == 0) { + return new QueryParameters(); + } + return parse(queryString); + } + + private static QueryParameters parse(String query) { + QueryParameters parameters = new QueryParameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private static void processParameter(String parameter, QueryParameters parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); + values.add(""); + } + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + } + } + + private static String decode(String encoded) { + return URLDecoder.decode(encoded, StandardCharsets.UTF_8); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 204ab28f..bdf65934 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -32,8 +32,6 @@ class StandardOperationRequest extends AbstractOperationMessage implements Opera private HttpMethod method; - private Parameters parameters; - private Collection parts; private URI uri; @@ -48,16 +46,14 @@ class StandardOperationRequest extends AbstractOperationMessage implements Opera * @param method the method * @param content the content * @param headers the headers - * @param parameters the parameters * @param parts the parts * @param cookies the cookies */ - StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, + StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Collection parts, Collection cookies) { super(content, headers); this.uri = uri; this.method = method; - this.parameters = parameters; this.parts = parts; this.cookies = cookies; } @@ -67,11 +63,6 @@ public HttpMethod getMethod() { return this.method; } - @Override - public Parameters getParameters() { - return this.parameters; - } - @Override public Collection getParts() { return Collections.unmodifiableCollection(this.parts); 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 231f4e2c..ef2f2223 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 @@ -134,9 +134,9 @@ public OperationRequest preprocess(OperationRequest request) { HttpHeaders modifiedHeaders = modify(request.getHeaders()); modifiedHeaders.set(HttpHeaders.HOST, modifiedUri.getHost() + ((modifiedUri.getPort() != -1) ? ":" + modifiedUri.getPort() : "")); - return this.contentModifyingDelegate.preprocess(new OperationRequestFactory().create( - uriBuilder.build(true).toUri(), request.getMethod(), request.getContent(), modifiedHeaders, - request.getParameters(), modify(request.getParts()), request.getCookies())); + return this.contentModifyingDelegate + .preprocess(new OperationRequestFactory().create(uriBuilder.build(true).toUri(), request.getMethod(), + request.getContent(), modifiedHeaders, modify(request.getParts()), request.getCookies())); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/FormParametersSnippet.java similarity index 62% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/FormParametersSnippet.java index b1a1afc1..8d5401d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/FormParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * 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. @@ -22,36 +22,33 @@ import java.util.Map; import java.util.Set; +import org.springframework.restdocs.operation.FormParameters; import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; /** - * A {@link Snippet} that documents the request parameters supported by a RESTful - * resource. - *

    - * Request parameters are sent as part of the query string or as POSTed form data. + * A {@link Snippet} that documents the form parameters supported by a RESTful resource. * * @author Andy Wilkinson - * @see OperationRequest#getParameters() - * @see RequestDocumentation#requestParameters(ParameterDescriptor...) - * @see RequestDocumentation#requestParameters(Map, ParameterDescriptor...) + * @since 3.0.0 + * @see RequestDocumentation#formParameters(ParameterDescriptor...) + * @see RequestDocumentation#formParameters(Map, ParameterDescriptor...) */ -public class RequestParametersSnippet extends AbstractParametersSnippet { +public class FormParametersSnippet extends AbstractParametersSnippet { /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. Undocumented parameters will * trigger a failure. * @param descriptors the parameter descriptors */ - protected RequestParametersSnippet(List descriptors) { + protected FormParametersSnippet(List descriptors) { this(descriptors, null, false); } /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. If * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will * be ignored and will not trigger a failure. @@ -59,24 +56,24 @@ protected RequestParametersSnippet(List descriptors) { * @param ignoreUndocumentedParameters whether undocumented parameters should be * ignored */ - protected RequestParametersSnippet(List descriptors, boolean ignoreUndocumentedParameters) { + protected FormParametersSnippet(List descriptors, boolean ignoreUndocumentedParameters) { this(descriptors, null, ignoreUndocumentedParameters); } /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. Undocumented parameters will * trigger a failure. * @param descriptors the parameter descriptors * @param attributes the additional attributes */ - protected RequestParametersSnippet(List descriptors, Map attributes) { + protected FormParametersSnippet(List descriptors, Map attributes) { this(descriptors, attributes, false); } /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. If * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will @@ -86,54 +83,53 @@ protected RequestParametersSnippet(List descriptors, Map descriptors, Map attributes, + protected FormParametersSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedParameters) { - super("request-parameters", descriptors, attributes, ignoreUndocumentedParameters); + super("form-parameters", descriptors, attributes, ignoreUndocumentedParameters); } @Override protected void verificationFailed(Set undocumentedParameters, Set missingParameters) { String message = ""; if (!undocumentedParameters.isEmpty()) { - message += "Request parameters with the following names were not documented: " + undocumentedParameters; + message += "Form parameters with the following names were not documented: " + undocumentedParameters; } if (!missingParameters.isEmpty()) { if (message.length() > 0) { message += ". "; } - message += "Request parameters with the following names were not found in the request: " - + missingParameters; + message += "Form parameters with the following names were not found in the request: " + missingParameters; } throw new SnippetException(message); } @Override protected Set extractActualParameters(Operation operation) { - return operation.getRequest().getParameters().keySet(); + return FormParameters.from(operation.getRequest()).keySet(); } /** - * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * Returns a new {@code FormParametersSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + public FormParametersSnippet and(ParameterDescriptor... additionalDescriptors) { return and(Arrays.asList(additionalDescriptors)); } /** - * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * Returns a new {@code FormParametersSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestParametersSnippet and(List additionalDescriptors) { + public FormParametersSnippet and(List additionalDescriptors) { List combinedDescriptors = new ArrayList<>(getParameterDescriptors().values()); combinedDescriptors.addAll(additionalDescriptors); - return new RequestParametersSnippet(combinedDescriptors, this.getAttributes(), + return new FormParametersSnippet(combinedDescriptors, this.getAttributes(), this.isIgnoreUndocumentedParameters()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java new file mode 100644 index 00000000..2485c2f3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java @@ -0,0 +1,136 @@ +/* + * 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.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.QueryParameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the query parameters supported by a RESTful resource. + * + * @author Andy Wilkinson + * @since 3.0.0 + * @see RequestDocumentation#queryParameters(ParameterDescriptor...) + * @see RequestDocumentation#queryParameters(Map, ParameterDescriptor...) + */ +public class QueryParametersSnippet extends AbstractParametersSnippet { + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. + * @param descriptors the parameter descriptors + */ + protected QueryParametersSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected QueryParametersSnippet(List descriptors, boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); + } + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + */ + protected QueryParametersSnippet(List descriptors, Map attributes) { + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected QueryParametersSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { + super("query-parameters", descriptors, attributes, ignoreUndocumentedParameters); + } + + @Override + protected void verificationFailed(Set undocumentedParameters, Set missingParameters) { + String message = ""; + if (!undocumentedParameters.isEmpty()) { + message += "Query parameters with the following names were not documented: " + undocumentedParameters; + } + if (!missingParameters.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Query parameters with the following names were not found in the request: " + missingParameters; + } + throw new SnippetException(message); + } + + @Override + protected Set extractActualParameters(Operation operation) { + return QueryParameters.from(operation.getRequest()).keySet(); + } + + /** + * Returns a new {@code QueryParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public QueryParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code QueryParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public QueryParametersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getParameterDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); + return new QueryParametersSnippet(combinedDescriptors, this.getAttributes(), + this.isIgnoreUndocumentedParameters()); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 3c051fbc..221006b8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * 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. @@ -203,159 +203,321 @@ public static PathParametersSnippet relaxedPathParameters(Map at } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

    - * If a parameter is present in the request, but is not documented by one of the + * If a query parameter is present in the request, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * parameter is documented, is not marked as optional, and is not present in the + * query parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

    - * If you do not want to document a request parameter, a parameter descriptor can be + * If you do not want to document a query parameter, a parameter descriptor can be * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from * appearing in the generated snippet while avoiding the failure described above. - * @param descriptors the descriptions of the request's parameters + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters(ParameterDescriptor... descriptors) { - return requestParameters(Arrays.asList(descriptors)); + public static QueryParametersSnippet queryParameters(ParameterDescriptor... descriptors) { + return queryParameters(Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

    - * If a parameter is present in the request, but is not documented by one of the + * If a query parameter is present in the request, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * parameter is documented, is not marked as optional, and is not present in the + * query parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

    - * If you do not want to document a request parameter, a parameter descriptor can be + * If you do not want to document a query parameter, a parameter descriptor can be * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from * appearing in the generated snippet while avoiding the failure described above. - * @param descriptors the descriptions of the request's parameters + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters(List descriptors) { - return new RequestParametersSnippet(descriptors); + public static QueryParametersSnippet queryParameters(List descriptors) { + return new QueryParametersSnippet(descriptors); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

    - * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. - * @param descriptors the descriptions of the request's parameters + * If a query parameter is documented, is not marked as optional, and is not present + * in the response, a failure will occur. Any undocumented query parameters will be + * ignored. + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters(ParameterDescriptor... descriptors) { - return relaxedRequestParameters(Arrays.asList(descriptors)); + public static QueryParametersSnippet relaxedQueryParameters(ParameterDescriptor... descriptors) { + return relaxedQueryParameters(Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

    * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. - * @param descriptors the descriptions of the request's parameters + * response, a failure will occur. Any undocumented query parameters will be ignored. + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters(List descriptors) { - return new RequestParametersSnippet(descriptors, true); + public static QueryParametersSnippet relaxedQueryParameters(List descriptors) { + return new QueryParametersSnippet(descriptors, true); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the query parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the query parameters will be documented using the given + * {@code descriptors} . *

    - * If a parameter is present in the request, but is not documented by one of the + * If a query parameter is present in the request, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * parameter is documented, is not marked as optional, and is not present in the + * query parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

    - * If you do not want to document a request parameter, a parameter descriptor can be + * If you do not want to document a query parameter, a parameter descriptor can be * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters(Map attributes, + public static QueryParametersSnippet queryParameters(Map attributes, ParameterDescriptor... descriptors) { - return requestParameters(attributes, Arrays.asList(descriptors)); + return queryParameters(attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the query parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the query parameters will be documented using the given + * {@code descriptors} . *

    - * If a parameter is present in the request, but is not documented by one of the + * If a query parameter is present in the request, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * parameter is documented, is not marked as optional, and is not present in the + * query parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

    - * If you do not want to document a request parameter, a parameter descriptor can be + * If you do not want to document a query parameter, a parameter descriptor can be * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters(Map attributes, + public static QueryParametersSnippet queryParameters(Map attributes, List descriptors) { - return new RequestParametersSnippet(descriptors, attributes); + return new QueryParametersSnippet(descriptors, attributes); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the query parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the query parameters will be documented using the given + * {@code descriptors} . *

    - * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. + * If a query parameter is documented, is not marked as optional, and is not present + * in the response, a failure will occur. Any undocumented query parameters will be + * ignored. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters(Map attributes, + public static QueryParametersSnippet relaxedQueryParameters(Map attributes, ParameterDescriptor... descriptors) { - return relaxedRequestParameters(attributes, Arrays.asList(descriptors)); + return relaxedQueryParameters(attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the query parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the query parameters will be documented using the given + * {@code descriptors} . + *

    + * If a query parameter is documented, is not marked as optional, and is not present + * in the response, a failure will occur. Any undocumented query parameters will be + * ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 + */ + public static QueryParametersSnippet relaxedQueryParameters(Map attributes, + List descriptors) { + return new QueryParametersSnippet(descriptors, attributes, true); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. + *

    + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

    + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet formParameters(ParameterDescriptor... descriptors) { + return formParameters(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. + *

    + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

    + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet formParameters(List descriptors) { + return new FormParametersSnippet(descriptors); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. + *

    + * If a form parameter is documented, is not marked as optional, and is not present in + * the response, a failure will occur. Any undocumented form parameters will be + * ignored. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet relaxedFormParameters(ParameterDescriptor... descriptors) { + return relaxedFormParameters(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. *

    * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. + * response, a failure will occur. Any undocumented form parameters will be ignored. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet relaxedFormParameters(List descriptors) { + return new FormParametersSnippet(descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the form parameters will be documented using the given + * {@code descriptors} . + *

    + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

    + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 + */ + public static FormParametersSnippet formParameters(Map attributes, + ParameterDescriptor... descriptors) { + return formParameters(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the form parameters will be documented using the given + * {@code descriptors} . + *

    + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

    + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param attributes the attributes + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 + */ + public static FormParametersSnippet formParameters(Map attributes, + List descriptors) { + return new FormParametersSnippet(descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the form parameters will be documented using the given + * {@code descriptors} . + *

    + * If a form parameter is documented, is not marked as optional, and is not present in + * the response, a failure will occur. Any undocumented form parameters will be + * ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 + */ + public static FormParametersSnippet relaxedFormParameters(Map attributes, + ParameterDescriptor... descriptors) { + return relaxedFormParameters(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the form parameters will be documented using the given + * {@code descriptors} . + *

    + * If a form parameter is documented, is not marked as optional, and is not present in + * the response, a failure will occur. Any undocumented form parameters will be + * ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters(Map attributes, + public static FormParametersSnippet relaxedFormParameters(Map attributes, List descriptors) { - return new RequestParametersSnippet(descriptors, attributes, true); + return new FormParametersSnippet(descriptors, attributes, true); } /** @@ -482,7 +644,6 @@ public static RequestPartsSnippet requestParts(Map attributes, * @param attributes the attributes * @param descriptors the descriptions of the request's parts * @return the snippet - * @see OperationRequest#getParameters() */ public static RequestPartsSnippet relaxedRequestParts(Map attributes, RequestPartDescriptor... descriptors) { @@ -499,7 +660,6 @@ public static RequestPartsSnippet relaxedRequestParts(Map attrib * @param attributes the attributes * @param descriptors the descriptions of the request's parts * @return the snippet - * @see OperationRequest#getParameters() */ public static RequestPartsSnippet relaxedRequestParts(Map attributes, List descriptors) { diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet new file mode 100644 index 00000000..9e6f6888 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet new file mode 100644 index 00000000..9e6f6888 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet new file mode 100644 index 00000000..681daaa8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet @@ -0,0 +1,5 @@ +Parameter | Description +--------- | ----------- +{{#parameters}} +`{{name}}` | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet new file mode 100644 index 00000000..681daaa8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet @@ -0,0 +1,5 @@ +Parameter | Description +--------- | ----------- +{{#parameters}} +`{{name}}` | {{description}} +{{/parameters}} \ No newline at end of file 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 856be874..a661c897 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 @@ -57,14 +57,6 @@ public void getRequest() throws IOException { .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X GET")); } - @Test - public void getRequestWithParameter() throws IOException { - new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").param("a", "alpha").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha' -i -X GET")); - } - @Test public void nonGetRequest() throws IOException { new CurlRequestSnippet(this.commandFormatter) @@ -89,30 +81,6 @@ public void getRequestWithQueryString() throws IOException { .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?param=value' -i -X GET")); } - @Test - public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo?param=value").param("param", "value").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?param=value' -i -X GET")); - } - - @Test - public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder - .request("http://localhost/foo?a=alpha").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X GET")); - } - - @Test - public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X GET")); - } - @Test public void getRequestWithQueryStringWithNoValue() throws IOException { new CurlRequestSnippet(this.commandFormatter) @@ -140,7 +108,16 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithOneParameter() throws IOException { new CurlRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("POST").param("k1", "v1").build()); + 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'")); + } + + @Test + public void postRequestWithOneParameterAndExplicitContentType() throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(this.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'")); } @@ -148,7 +125,7 @@ public void postRequestWithOneParameter() throws IOException { @Test public void postRequestWithOneParameterWithNoValue() throws IOException { new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").param("k1").build()); + .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='")); } @@ -156,7 +133,7 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { @Test public void postRequestWithMultipleParameters() throws IOException { new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); + .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'")); } @@ -164,52 +141,24 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { new CurlRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("POST").param("k1", "a&b").build()); + 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'")); } @Test - public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder - .request("http://localhost/foo?a=alpha").method("POST").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - } - - @Test - public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); - } - - @Test - public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - } - - @Test - public void postRequestWithOverlappingParametersAndFormUrlEncodedBody() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("POST").content("a=alpha&b=bravo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo' -i -X POST " - + "-H 'Content-Type: application/x-www-form-urlencoded' " + "-d 'a=alpha&b=bravo'")); + public void postRequestWithJsonData() throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(this.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\"}'")); } @Test public void putRequestWithOneParameter() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("PUT").param("k1", "v1").build()); + 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'")); } @@ -217,7 +166,7 @@ public void putRequestWithOneParameter() throws IOException { @Test public void putRequestWithMultipleParameters() throws IOException { new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("PUT").param("k1", "v1").param("k1", "v1-bis").param("k2", "v2").build()); + .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'")); } @@ -225,7 +174,7 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { new CurlRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("PUT").param("k1", "a&b").build()); + 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'")); } @@ -287,29 +236,6 @@ public void multipartPost() throws IOException { assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); } - @Test - public void multipartPostWithParameters() throws IOException { - new CurlRequestSnippet(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").and() - .param("a", "apple", "avocado").param("b", "banana").build()); - String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " - + "'Content-Type: multipart/form-data' -F " - + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); - } - - @Test - public void multipartPostWithOverlappingPartsAndParameters() throws IOException { - new CurlRequestSnippet(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").and() - .part("a", "apple".getBytes()).and().param("a", "apple").build()); - String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " - + "'Content-Type: multipart/form-data' -F 'image=@documents/images/example.png' -F 'a=apple'"; - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); - } - @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") @@ -329,22 +255,6 @@ public void customAttributes() throws IOException { + " -H 'Content-Type: application/json' -H 'a: alpha'")); } - @Test - public void postWithContentAndParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .param("a", "alpha").method("POST").param("b", "bravo").content("Some content").build()); - assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash") - .withContent("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X POST -d 'Some content'")); - } - - @Test - public void deleteWithParameters() throws IOException { - new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("DELETE").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.curlRequest()) - .is(codeBlock("bash").withContent("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X DELETE")); - } - @Test public void deleteWithQueryString() throws IOException { new CurlRequestSnippet(this.commandFormatter).document( 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 7be6272c..88835917 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 @@ -58,14 +58,6 @@ public void getRequest() throws IOException { .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo'")); } - @Test - public void getRequestWithParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").param("a", "alpha").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?a=alpha'")); - } - @Test public void nonGetRequest() throws IOException { new HttpieRequestSnippet(this.commandFormatter) @@ -90,30 +82,6 @@ public void getRequestWithQueryString() throws IOException { .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?param=value'")); } - @Test - public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo?param=value").param("param", "value").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?param=value'")); - } - - @Test - public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder - .request("http://localhost/foo?a=alpha").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - } - - @Test - public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - } - @Test public void getRequestWithQueryStringWithNoValue() throws IOException { new HttpieRequestSnippet(this.commandFormatter) @@ -140,16 +108,18 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithOneParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("POST").param("k1", "v1").build()); + new HttpieRequestSnippet(this.commandFormatter).document(this.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'")); } @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo").method("POST").param("k1").build()); + new HttpieRequestSnippet(this.commandFormatter).document(this.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='")); } @@ -157,60 +127,26 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { @Test public void postRequestWithMultipleParameters() throws IOException { new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash") - .withContent("$ http --form POST 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + .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'")); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("POST").param("k1", "a&b").build()); + new HttpieRequestSnippet(this.commandFormatter).document(this.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'")); } - @Test - public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder - .request("http://localhost/foo?a=alpha").method("POST").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - } - - @Test - public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); - } - - @Test - public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter) - .document(this.operationBuilder.request("http://localhost/foo?a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - } - - @Test - public void postRequestWithOverlappingParametersAndFormUrlEncodedBody() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("POST").content("a=alpha&b=bravo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ echo 'a=alpha&b=bravo' | http POST 'http://localhost/foo' " - + "'Content-Type:application/x-www-form-urlencoded'")); - } - @Test public void putRequestWithOneParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("PUT").param("k1", "v1").build()); + new HttpieRequestSnippet(this.commandFormatter).document(this.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'")); } @@ -218,15 +154,17 @@ public void putRequestWithOneParameter() throws IOException { @Test public void putRequestWithMultipleParameters() throws IOException { new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("PUT").param("k1", "v1").param("k1", "v1-bis").param("k2", "v2").build()); + .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'")); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document( - this.operationBuilder.request("http://localhost/foo").method("PUT").param("k1", "a&b").build()); + new HttpieRequestSnippet(this.commandFormatter).document(this.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'")); } @@ -291,30 +229,6 @@ public void multipartPost() throws IOException { assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); } - @Test - public void multipartPostWithParameters() 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").and() - .param("a", "apple", "avocado").param("b", "banana").build()); - String expectedContent = "$ http --multipart POST 'http://localhost/upload'" - + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + " 'b=banana'"; - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); - } - - @Test - public void multipartPostWithOverlappingPartsAndParameters() 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").and() - .part("a", "apple".getBytes()).and().param("a", "apple").build()); - String expectedContent = "$ http --multipart POST 'http://localhost/upload'" - + " 'image'@'documents/images/example.png' 'a'='apple'"; - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); - } - @Test public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") @@ -334,22 +248,6 @@ public void customAttributes() throws IOException { + " 'Content-Type:application/json' 'a:alpha'")); } - @Test - public void postWithContentAndParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("a", "alpha").param("b", "bravo").content("Some content").build()); - assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash") - .withContent("$ echo 'Some content' | http POST " + "'http://localhost/foo?a=alpha&b=bravo'")); - } - - @Test - public void deleteWithParameters() throws IOException { - new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") - .method("DELETE").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpieRequest()) - .is(codeBlock("bash").withContent("$ http DELETE 'http://localhost/foo?a=alpha&b=bravo'")); - } - @Test public void deleteWithQueryString() throws IOException { new HttpieRequestSnippet(this.commandFormatter).document( 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 4bdb908d..62055b08 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 @@ -58,9 +58,9 @@ public void getRequest() throws IOException { } @Test - public void getRequestWithParameters() throws IOException { - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo").header("Alpha", "a").param("b", "bravo").build()); + public void getRequestWithQueryParameters() 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")); } @@ -96,22 +96,6 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .is(httpRequest(RequestMethod.GET, "/foo?bar").header(HttpHeaders.HOST, "localhost")); } - @Test - public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo").header(HttpHeaders.HOST, "localhost")); - } - - @Test - public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo").header(HttpHeaders.HOST, "localhost")); - } - @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; @@ -123,59 +107,16 @@ public void postRequestWithContent() throws IOException { } @Test - public void postRequestWithContentAndParameters() throws IOException { + public void postRequestWithContentAndQueryParameters() throws IOException { String content = "Hello, world"; - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo").method("POST") - .param("a", "alpha").content(content).build()); + 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)); } - @Test - public void postRequestWithContentAndDisjointQueryStringAndParameters() throws IOException { - String content = "Hello, world"; - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").content(content).build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha").header(HttpHeaders.HOST, "localhost") - .content(content).header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - } - - @Test - public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() throws IOException { - String content = "Hello, world"; - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").content(content).build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha").header(HttpHeaders.HOST, "localhost") - .content(content).header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - } - - @Test - public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() throws IOException { - String content = "Hello, world"; - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?b=bravo&a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").content(content).build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha").header(HttpHeaders.HOST, "localhost") - .content(content).header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - } - - @Test - public void postRequestWithOverlappingParametersAndFormUrlEncodedBody() throws IOException { - String content = "a=alpha&b=bravo"; - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo").method("POST").content("a=alpha&b=bravo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - } - @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; @@ -187,24 +128,6 @@ public void postRequestWithCharset() throws IOException { .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length).content(japaneseContent)); } - @Test - public void postRequestWithParameter() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo").method("POST") - .param("b&r", "baz").param("a", "alpha").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo").header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "application/x-www-form-urlencoded").content("b%26r=baz&a=alpha")); - } - - @Test - public void postRequestWithParameterWithNoValue() throws IOException { - new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo").method("POST").param("bar").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.POST, "/foo").header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "application/x-www-form-urlencoded").content("bar=")); - } - @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; @@ -215,23 +138,6 @@ public void putRequestWithContent() throws IOException { .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void putRequestWithParameter() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo").method("PUT") - .param("b&r", "baz").param("a", "alpha").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.PUT, "/foo").header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "application/x-www-form-urlencoded").content("b%26r=baz&a=alpha")); - } - - @Test - public void putRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") - .method("PUT").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.PUT, "/foo?a=alpha&b=bravo").header(HttpHeaders.HOST, "localhost")); - } - @Test public void multipartPost() throws IOException { new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload").method("POST") @@ -256,47 +162,6 @@ public void multipartPostWithFilename() throws IOException { .header(HttpHeaders.HOST, "localhost").content(expectedContent)); } - @Test - public void multipartPostWithParameters() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE).param("a", "apple", "avocado") - .param("b", "banana").part("image", "<< data >>".getBytes()).build()); - String param1Part = createPart(String.format("Content-Disposition: form-data; " + "name=a%n%napple"), false); - String param2Part = createPart(String.format("Content-Disposition: form-data; " + "name=a%n%navocado"), false); - String param3Part = createPart(String.format("Content-Disposition: form-data; " + "name=b%n%nbanana"), false); - String filePart = createPart(String.format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); - String expectedContent = param1Part + param2Part + param3Part + filePart; - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/upload") - .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - } - - @Test - public void multipartPostWithOverlappingPartsAndParameters() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE).param("a", "apple") - .part("a", "apple".getBytes()).and().part("image", "<< data >>".getBytes()).build()); - String paramPart = createPart(String.format("Content-Disposition: form-data; " + "name=a%n%napple"), false); - String filePart = createPart(String.format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); - String expectedContent = paramPart + filePart; - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/upload") - .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - } - - @Test - public void multipartPostWithParameterWithNoValue() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE).param("a") - .part("image", "<< data >>".getBytes()).build()); - String paramPart = createPart(String.format("Content-Disposition: form-data; " + "name=a%n"), false); - String filePart = createPart(String.format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); - String expectedContent = paramPart + filePart; - assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.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").method("POST") @@ -328,14 +193,6 @@ public void requestWithCustomSnippetAttributes() throws IOException { assertThat(this.generatedSnippets.httpRequest()).contains("Title for the request"); } - @Test - public void deleteWithParameters() throws IOException { - new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/foo").method("DELETE") - .param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.httpRequest()) - .is(httpRequest(RequestMethod.DELETE, "/foo?a=alpha&b=bravo").header("Host", "localhost")); - } - @Test public void deleteWithQueryString() throws IOException { new HttpRequestSnippet().document( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java deleted file mode 100644 index a6fefcd8..00000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2014-2018 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.operation; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link Parameters}. - * - * @author Andy Wilkinson - */ -public class ParametersTests { - - private final Parameters parameters = new Parameters(); - - @Test - public void queryStringForNoParameters() { - assertThat(this.parameters.toQueryString()).isEqualTo(""); - } - - @Test - public void queryStringForSingleParameter() { - this.parameters.add("a", "b"); - assertThat(this.parameters.toQueryString()).isEqualTo("a=b"); - } - - @Test - public void queryStringForSingleParameterWithMultipleValues() { - this.parameters.add("a", "b"); - this.parameters.add("a", "c"); - assertThat(this.parameters.toQueryString()).isEqualTo("a=b&a=c"); - } - - @Test - public void queryStringForMutipleParameters() { - this.parameters.add("a", "alpha"); - this.parameters.add("b", "bravo"); - assertThat(this.parameters.toQueryString()).isEqualTo("a=alpha&b=bravo"); - } - - @Test - public void queryStringForParameterWithEmptyValue() { - this.parameters.add("a", ""); - assertThat(this.parameters.toQueryString()).isEqualTo("a="); - } - - @Test - public void queryStringForParameterWithNullValue() { - this.parameters.add("a", null); - assertThat(this.parameters.toQueryString()).isEqualTo("a="); - } - - @Test - public void queryStringForParameterThatRequiresEncoding() { - this.parameters.add("a", "alpha&bravo"); - assertThat(this.parameters.toQueryString()).isEqualTo("a=alpha%26bravo"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java deleted file mode 100644 index 0cb99f12..00000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java +++ /dev/null @@ -1,93 +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.operation; - -import java.net.URI; -import java.util.Arrays; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link QueryStringParser}. - * - * @author Andy Wilkinson - */ -public class QueryStringParserTests { - - private final QueryStringParser queryStringParser = new QueryStringParser(); - - @Test - public void noParameters() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost")); - assertThat(parameters.size()).isEqualTo(0); - } - - @Test - public void singleParameter() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost?a=alpha")); - assertThat(parameters.size()).isEqualTo(1); - assertThat(parameters).containsEntry("a", Arrays.asList("alpha")); - } - - @Test - public void multipleParameters() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost?a=alpha&b=bravo&c=charlie")); - assertThat(parameters.size()).isEqualTo(3); - assertThat(parameters).containsEntry("a", Arrays.asList("alpha")); - assertThat(parameters).containsEntry("b", Arrays.asList("bravo")); - assertThat(parameters).containsEntry("c", Arrays.asList("charlie")); - } - - @Test - public void multipleParametersWithSameKey() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost?a=apple&a=avocado")); - assertThat(parameters.size()).isEqualTo(1); - assertThat(parameters).containsEntry("a", Arrays.asList("apple", "avocado")); - } - - @Test - public void encoded() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost?a=al%26%3Dpha")); - assertThat(parameters.size()).isEqualTo(1); - assertThat(parameters).containsEntry("a", Arrays.asList("al&=pha")); - } - - @Test - public void malformedParameter() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.queryStringParser.parse(URI.create("http://localhost?a=apple=avocado"))) - .withMessage("The parameter 'a=apple=avocado' is malformed"); - } - - @Test - public void emptyParameter() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost?a=")); - assertThat(parameters.size()).isEqualTo(1); - assertThat(parameters).containsEntry("a", Arrays.asList("")); - } - - @Test - public void emptyAndNotEmptyParameter() { - Parameters parameters = this.queryStringParser.parse(URI.create("http://localhost?a=&a=alpha")); - assertThat(parameters.size()).isEqualTo(1); - assertThat(parameters).containsEntry("a", Arrays.asList("", "alpha")); - } - -} 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 57b54c98..7e26b6da 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-2019 the original author or authors. + * 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. @@ -30,7 +30,6 @@ import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; import static org.assertj.core.api.Assertions.assertThat; @@ -59,8 +58,7 @@ public byte[] modifyContent(byte[] originalContent, MediaType mediaType) { @Test public void modifyRequestContent() { OperationRequest request = this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - "content".getBytes(), new HttpHeaders(), new Parameters(), - Collections.emptyList()); + "content".getBytes(), new HttpHeaders(), Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); assertThat(preprocessed.getContent()).isEqualTo("modified".getBytes()); } @@ -78,7 +76,7 @@ public void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentLength(7); OperationRequest request = this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - "content".getBytes(), httpHeaders, new Parameters(), Collections.emptyList()); + "content".getBytes(), httpHeaders, Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); assertThat(preprocessed.getHeaders().getContentLength()).isEqualTo(8L); } 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 9028c564..c33153f4 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 @@ -31,7 +31,6 @@ import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; import static org.assertj.core.api.Assertions.assertThat; @@ -149,7 +148,7 @@ private OperationRequest createRequest(Consumer headersCustomizer) headersCustomizer.accept(headers); } return new OperationRequestFactory().create(URI.create("http://localhost:8080"), HttpMethod.GET, new byte[0], - headers, new Parameters(), Collections.emptyList()); + headers, Collections.emptyList()); } private OperationResponse createResponse() { 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 f20d1507..d0353540 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 @@ -32,7 +32,6 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; import static org.assertj.core.api.Assertions.assertThat; @@ -304,41 +303,40 @@ public void modifiedUriDoesNotGetDoubleEncoded() { public 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(), new Parameters(), Collections.emptyList(), - cookies); + new byte[0], new HttpHeaders(), Collections.emptyList(), cookies); OperationRequest processed = this.preprocessor.preprocess(request); assertThat(processed.getCookies().size()).isEqualTo(1); } private OperationRequest createRequestWithUri(String uri) { return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], new HttpHeaders(), - new Parameters(), Collections.emptyList()); + Collections.emptyList()); } private OperationRequest createRequestWithContent(String content) { return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, content.getBytes(), - new HttpHeaders(), new Parameters(), Collections.emptyList()); + new HttpHeaders(), Collections.emptyList()); } private OperationRequest createRequestWithHeader(String name, String value) { HttpHeaders headers = new HttpHeaders(); headers.add(name, value); return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], headers, - new Parameters(), Collections.emptyList()); + Collections.emptyList()); } private OperationRequest createRequestWithPartWithHeader(String name, String value) { HttpHeaders headers = new HttpHeaders(); headers.add(name, value); return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], - new HttpHeaders(), new Parameters(), + new HttpHeaders(), Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", new byte[0], headers))); } private OperationRequest createRequestWithPartWithContent(String content) { return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], - new HttpHeaders(), new Parameters(), Arrays.asList(new OperationRequestPartFactory().create("part", - "fileName", content.getBytes(), new HttpHeaders()))); + new HttpHeaders(), Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", + content.getBytes(), new HttpHeaders()))); } private OperationResponse createResponseWithContent(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java similarity index 68% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java index b6921c44..b6e495d0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetFailureTests.java @@ -30,12 +30,12 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; /** - * Tests for failures when rendering {@link RequestParametersSnippet} due to missing or - * undocumented request parameters. + * Tests for failures when rendering {@link FormParametersSnippet} due to missing or + * undocumented form parameters. * * @author Andy Wilkinson */ -public class RequestParametersSnippetFailureTests { +public class FormParametersSnippetFailureTests { @Rule public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor()); @@ -43,25 +43,25 @@ public class RequestParametersSnippetFailureTests { @Test public void undocumentedParameter() { assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestParametersSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost").param("a", "alpha").build())) - .withMessage("Request parameters with the following names were not documented: [a]"); + .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 RequestParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) .document(this.operationBuilder.request("http://localhost").build())) - .withMessage("Request parameters with the following names were not found in the request: [a]"); + .withMessage("Form parameters with the following names were not found in the request: [a]"); } @Test public void undocumentedAndMissingParameters() { assertThatExceptionOfType(SnippetException.class) - .isThrownBy(() -> new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost").param("b", "bravo").build())) - .withMessage("Request parameters with the following names were not documented: [b]. Request parameters" + .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/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java similarity index 54% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java index 92857a27..1bf258a8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * 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. @@ -36,138 +36,134 @@ import static org.springframework.restdocs.snippet.Attributes.key; /** - * Tests for {@link RequestParametersSnippet}. + * Tests for {@link FormParametersSnippet}. * * @author Andy Wilkinson */ -public class RequestParametersSnippetTests extends AbstractSnippetTests { +public class FormParametersSnippetTests extends AbstractSnippetTests { - public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) { + public FormParametersSnippetTests(String name, TemplateFormat templateFormat) { super(name, templateFormat); } @Test - public void requestParameters() throws IOException { - new RequestParametersSnippet( + public void formParameters() throws IOException { + new FormParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()) + .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")); } @Test - public void requestParameterWithNoValue() throws IOException { - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost").param("a").build()); - assertThat(this.generatedSnippets.requestParameters()) + public void formParameterWithNoValue() 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")); } @Test - public void ignoredRequestParameter() throws IOException { - new RequestParametersSnippet( + public void ignoredFormParameter() throws IOException { + new FormParametersSnippet( Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()) + .document(this.operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(this.generatedSnippets.formParameters()) .is(tableWithHeader("Parameter", "Description").row("`b`", "two")); } @Test - public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { - new RequestParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true).document( - this.operationBuilder.request("http://localhost").param("a", "bravo").param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()) + public void allUndocumentedFormParametersCanBeIgnored() 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")); } @Test - public void missingOptionalRequestParameter() throws IOException { - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), + public void missingOptionalFormParameter() throws IOException { + new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost").param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()) + .document(this.operationBuilder.request("http://localhost").content("b=bravo").build()); + assertThat(this.generatedSnippets.formParameters()) .is(tableWithHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); } @Test - public void presentOptionalRequestParameter() throws IOException { - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())) - .document(this.operationBuilder.request("http://localhost").param("a", "one").build()); - assertThat(this.generatedSnippets.requestParameters()) + public void presentOptionalFormParameter() 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")); } @Test - public void requestParametersWithCustomAttributes() throws IOException { + public void formParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")) - .willReturn(snippetResource("request-parameters-with-title")); - new RequestParametersSnippet( + given(resolver.resolveTemplateResource("form-parameters")) + .willReturn(snippetResource("form-parameters-with-title")); + 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").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()).contains("The title"); + .request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(this.generatedSnippets.formParameters()).contains("The title"); } @Test - public void requestParametersWithCustomDescriptorAttributes() throws IOException { + public void formParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")) - .willReturn(snippetResource("request-parameters-with-extra-column")); - new RequestParametersSnippet( + given(resolver.resolveTemplateResource("form-parameters")) + .willReturn(snippetResource("form-parameters-with-extra-column")); + 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").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()).is( + .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")); } @Test - public void requestParametersWithOptionalColumn() throws IOException { + public void formParametersWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")) - .willReturn(snippetResource("request-parameters-with-optional-column")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), + given(resolver.resolveTemplateResource("form-parameters")) + .willReturn(snippetResource("form-parameters-with-optional-column")); + 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").param("a", "alpha").param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()) - .is(tableWithHeader("Parameter", "Optional", "Description").row("a", "true", "one").row("b", "false", - "two")); + .request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(this.generatedSnippets.formParameters()).is(tableWithHeader("Parameter", "Optional", "Description") + .row("a", "true", "one").row("b", "false", "two")); } @Test public void additionalDescriptors() throws IOException { - RequestDocumentation.requestParameters(parameterWithName("a").description("one")) - .and(parameterWithName("b").description("two")).document(this.operationBuilder - .request("http://localhost").param("a", "bravo").param("b", "bravo").build()); - assertThat(this.generatedSnippets.requestParameters()) + 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")); } @Test - public void additionalDescriptorsWithRelaxedRequestParameters() throws IOException { - RequestDocumentation.relaxedRequestParameters(parameterWithName("a").description("one")) - .and(parameterWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost").param("a", "bravo").param("b", "bravo") - .param("c", "undocumented").build()); - assertThat(this.generatedSnippets.requestParameters()) + public void additionalDescriptorsWithRelaxedFormParameters() 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")); } @Test - public void requestParametersWithEscapedContent() throws IOException { - RequestDocumentation.requestParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder.request("http://localhost").param("Foo|Bar", "baz").build()); - assertThat(this.generatedSnippets.requestParameters()).is(tableWithHeader("Parameter", "Description") + public void formParametersWithEscapedContent() 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"))); } 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 new file mode 100644 index 00000000..567bab8c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetFailureTests.java @@ -0,0 +1,68 @@ +/* + * 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.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 new file mode 100644 index 00000000..0d7be6df --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java @@ -0,0 +1,177 @@ +/* + * 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.request; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link QueryParametersSnippet}. + * + * @author Andy Wilkinson + */ +public class QueryParametersSnippetTests extends AbstractSnippetTests { + + public QueryParametersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void queryParameters() 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")); + } + + @Test + public void queryParameterWithNoValue() 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")); + } + + @Test + public void ignoredQueryParameter() 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")); + } + + @Test + public void allUndocumentedQueryParametersCanBeIgnored() 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")); + } + + @Test + public void missingOptionalQueryParameter() 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")); + } + + @Test + public void presentOptionalQueryParameter() 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")); + } + + @Test + public void queryParametersWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("query-parameters")) + .willReturn(snippetResource("query-parameters-with-title")); + 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"); + } + + @Test + public void queryParametersWithCustomDescriptorAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("query-parameters")) + .willReturn(snippetResource("query-parameters-with-extra-column")); + 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")); + } + + @Test + public void queryParametersWithOptionalColumn() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("query-parameters")) + .willReturn(snippetResource("query-parameters-with-optional-column")); + 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").row("b", "false", "two")); + } + + @Test + public void additionalDescriptors() 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")); + } + + @Test + public void additionalDescriptorsWithRelaxedQueryParameters() 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")); + } + + @Test + public void queryParametersWithEscapedContent() 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"))); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) { + return input; + } + return input.replace("|", "\\|"); + } + +} diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-optional-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-optional-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet new file mode 100644 index 00000000..fb97f89b --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Description|Foo + +{{#parameters}} +|{{name}} +|{{description}} +|{{foo}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet new file mode 100644 index 00000000..70847ba1 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Optional|Description + +{{#parameters}} +|{{name}} +|{{optional}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet new file mode 100644 index 00000000..611254aa --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-optional-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-optional-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet new file mode 100644 index 00000000..260502b6 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet @@ -0,0 +1,5 @@ +Parameter | Description | Foo +--------- | ----------- | --- +{{#parameters}} +{{name}} | {{description}} | {{foo}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet new file mode 100644 index 00000000..2f9cd077 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet @@ -0,0 +1,5 @@ +Parameter | Optional | Description +--------- | -------- | ----------- +{{#parameters}} +{{name}} | {{optional}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet new file mode 100644 index 00000000..b63b6235 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file 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 index e619e02b..a131cfc3 100644 --- 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 @@ -110,8 +110,12 @@ public String pathParameters() { return snippet("path-parameters"); } - public String requestParameters() { - return snippet("request-parameters"); + public String queryParameters() { + return snippet("query-parameters"); + } + + public String formParameters() { + return snippet("form-parameters"); } public String snippet(String name) { 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/OperationBuilder.java index a9299048..a546cbd7 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/OperationBuilder.java @@ -20,7 +20,6 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -42,7 +41,6 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.restdocs.operation.StandardOperation; @@ -147,8 +145,6 @@ public final class OperationRequestBuilder { private HttpHeaders headers = new HttpHeaders(); - private Parameters parameters = new Parameters(); - private List partBuilders = new ArrayList<>(); private Collection cookies = new ArrayList<>(); @@ -162,8 +158,8 @@ private OperationRequest buildRequest() { for (OperationRequestPartBuilder builder : this.partBuilders) { parts.add(builder.buildPart()); } - return new OperationRequestFactory().create(this.requestUri, this.method, this.content, this.headers, - this.parameters, parts, this.cookies); + return new OperationRequestFactory().create(this.requestUri, this.method, this.content, this.headers, parts, + this.cookies); } public Operation build() { @@ -185,18 +181,6 @@ public OperationRequestBuilder content(byte[] content) { return this; } - public OperationRequestBuilder param(String name, String... values) { - if (values.length > 0) { - for (String value : values) { - this.parameters.add(name, value); - } - } - else { - this.parameters.put(name, Collections.emptyList()); - } - return this; - } - public OperationRequestBuilder header(String name, String value) { this.headers.add(name, value); return this; diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 053df890..580bc763 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -17,14 +17,18 @@ package org.springframework.restdocs.mockmvc; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Scanner; import jakarta.servlet.ServletException; import jakarta.servlet.http.Part; @@ -39,10 +43,11 @@ import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -55,36 +60,88 @@ */ class MockMvcRequestConverter implements RequestConverter { - private static final String SCHEME_HTTP = "http"; - - private static final String SCHEME_HTTPS = "https"; - - private static final int STANDARD_PORT_HTTP = 80; - - private static final int STANDARD_PORT_HTTPS = 443; - @Override public OperationRequest convert(MockHttpServletRequest mockRequest) { try { HttpHeaders headers = extractHeaders(mockRequest); - Parameters parameters = extractParameters(mockRequest); List parts = extractParts(mockRequest); Collection cookies = extractCookies(mockRequest, headers); - String queryString = mockRequest.getQueryString(); - if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { - queryString = parameters.toQueryString(); - } - return new OperationRequestFactory().create( - URI.create( - getRequestUri(mockRequest) + (StringUtils.hasText(queryString) ? "?" + queryString : "")), - HttpMethod.valueOf(mockRequest.getMethod()), mockRequest.getContentAsByteArray(), headers, - parameters, parts, cookies); + return new OperationRequestFactory().create(getRequestUri(mockRequest), + HttpMethod.valueOf(mockRequest.getMethod()), getRequestContent(mockRequest, headers), headers, + parts, cookies); } catch (Exception ex) { throw new ConversionException(ex); } } + private URI getRequestUri(MockHttpServletRequest mockRequest) { + String queryString = ""; + if (mockRequest.getQueryString() != null) { + queryString = mockRequest.getQueryString(); + } + else if ("GET".equals(mockRequest.getMethod()) || mockRequest.getContentLengthLong() > 0) { + queryString = urlEncodedParameters(mockRequest); + } + StringBuffer requestUrlBuffer = mockRequest.getRequestURL(); + if (queryString.length() > 0) { + requestUrlBuffer.append("?").append(queryString.toString()); + } + return URI.create(requestUrlBuffer.toString()); + } + + private String urlEncodedParameters(MockHttpServletRequest mockRequest) { + StringBuilder parameters = new StringBuilder(); + MultiValueMap queryParameters = parse(mockRequest.getQueryString()); + for (String name : IterableEnumeration.of(mockRequest.getParameterNames())) { + if (!queryParameters.containsKey(name)) { + String[] values = mockRequest.getParameterValues(name); + if (values.length == 0) { + append(parameters, name); + } + else { + for (String value : values) { + append(parameters, name, value); + } + } + } + } + return parameters.toString(); + } + + private byte[] getRequestContent(MockHttpServletRequest mockRequest, HttpHeaders headers) { + byte[] content = mockRequest.getContentAsByteArray(); + if ("GET".equals(mockRequest.getMethod())) { + return content; + } + MediaType contentType = headers.getContentType(); + if (contentType == null || MediaType.APPLICATION_FORM_URLENCODED.includes(contentType)) { + Map parameters = mockRequest.getParameterMap(); + if (!parameters.isEmpty() && (content == null || content.length == 0)) { + StringBuilder contentBuilder = new StringBuilder(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap queryParameters = parse(mockRequest.getQueryString()); + mockRequest.getParameterMap().forEach((name, values) -> { + List queryParameterValues = queryParameters.get(name); + if (values.length == 0) { + if (queryParameterValues == null) { + append(contentBuilder, name); + } + } + else { + for (String value : values) { + if (queryParameterValues == null || !queryParameterValues.contains(value)) { + append(contentBuilder, name, value); + } + } + } + }); + return contentBuilder.toString().getBytes(StandardCharsets.UTF_8); + } + } + return content; + } + private Collection extractCookies(MockHttpServletRequest mockRequest, HttpHeaders headers) { if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { return Collections.emptyList(); @@ -158,16 +215,6 @@ private HttpHeaders extractHeaders(Part part) { return partHeaders; } - private Parameters extractParameters(MockHttpServletRequest servletRequest) { - Parameters parameters = new Parameters(); - for (String name : IterableEnumeration.of(servletRequest.getParameterNames())) { - for (String value : servletRequest.getParameterValues(name)) { - parameters.add(name, value); - } - } - return parameters; - } - private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) { HttpHeaders headers = new HttpHeaders(); for (String headerName : IterableEnumeration.of(servletRequest.getHeaderNames())) { @@ -178,21 +225,62 @@ private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) { return headers; } - private boolean isNonStandardPort(MockHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTPS); + private static void append(StringBuilder sb, String key) { + append(sb, key, ""); } - private String getRequestUri(MockHttpServletRequest request) { - StringWriter uriWriter = new StringWriter(); - PrintWriter printer = new PrintWriter(uriWriter); + private static void append(StringBuilder sb, String key, String value) { + doAppend(sb, urlEncode(key) + "=" + urlEncode(value)); + } - printer.printf("%s://%s", request.getScheme(), request.getServerName()); - if (isNonStandardPort(request)) { - printer.printf(":%d", request.getServerPort()); + private static void doAppend(StringBuilder sb, String toAppend) { + if (sb.length() > 0) { + sb.append("&"); } - printer.print(request.getRequestURI()); - return uriWriter.toString(); + sb.append(toAppend); + } + + private static String urlEncode(String s) { + if (!StringUtils.hasLength(s)) { + return ""; + } + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + + private static MultiValueMap parse(String query) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + if (!StringUtils.hasLength(query)) { + return parameters; + } + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private static void processParameter(String parameter, MultiValueMap parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); + values.add(""); + } + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + } + } + + private static String decode(String encoded) { + return URLDecoder.decode(encoded, StandardCharsets.US_ASCII); } } 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 a963f936..05264f58 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 @@ -127,31 +127,35 @@ public 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")); - assertThat(request.getParameters().size()).isEqualTo(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("br&vo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void getRequestWithQueryStringPopulatesParameters() { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo?a=alpha&b=bravo")); + public 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")); - assertThat(request.getParameters().size()).isEqualTo(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("bravo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void postRequestWithParameters() { + public 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")); assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); - assertThat(request.getParameters().size()).isEqualTo(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("br&vo")); + assertThat(request.getContentAsString()).isEqualTo("a=alpha&a=apple&b=br%26vo"); + assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED); + } + + @Test + public 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")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEqualTo("a=apple&b=br%26vo"); + assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED); } @Test 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 fdd858bf..55007b96 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 @@ -92,7 +92,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -178,7 +178,7 @@ public void curlSnippetWithCookies() throws Exception { public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/?foo=bar").param("a", "alpha").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("curl-snippet-with-query-string")); assertThat(new File("build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc")) .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") @@ -238,11 +238,11 @@ public void httpieSnippetWithCookies() throws Exception { public void httpieSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/?foo=bar").param("a", "alpha").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("httpie-snippet-with-query-string")); assertThat(new File("build/generated-snippets/httpie-snippet-with-query-string/httpie-request.adoc")) .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") - .withContent(String.format("$ http " + "--form POST 'http://localhost:8080/?foo=bar' \\%n" + .withContent(String.format("$ http --form POST 'http://localhost:8080/?foo=bar' \\%n" + " 'Accept:application/json' \\%n 'a=alpha'")))); } @@ -281,13 +281,13 @@ public void pathParametersSnippet() throws Exception { } @Test - public void requestParametersSnippet() throws Exception { + public void queryParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) - .andDo(document("links", requestParameters(parameterWithName("foo").description("The description")))); + .andDo(document("links", queryParameters(parameterWithName("foo").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "request-parameters.adoc"); + "http-response.adoc", "curl-request.adoc", "query-parameters.adoc"); } @Test diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index dbcd51ba..41df373b 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -21,4 +21,5 @@ dependencies { testImplementation("org.assertj:assertj-core") testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.mockito:mockito-core") + testImplementation("ch.qos.logback:logback-classic:1.4.1") } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index f4e00b2e..091d51ec 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -20,9 +20,12 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import io.restassured.http.Cookie; @@ -37,11 +40,11 @@ import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * A converter for creating an {@link OperationRequest} from a REST Assured @@ -56,7 +59,7 @@ class RestAssuredRequestConverter implements RequestConverter extractCookies(FilterableRequestSpecification requestSpec) { @@ -68,7 +71,36 @@ private Collection extractCookies(FilterableRequestSpecification } private byte[] extractContent(FilterableRequestSpecification requestSpec) { - return convertContent(requestSpec.getBody()); + Object body = requestSpec.getBody(); + if (body != null) { + return convertContent(body); + } + StringBuilder parameters = new StringBuilder(); + if ("POST".equals(requestSpec.getMethod())) { + appendParameters(parameters, requestSpec.getRequestParams()); + } + if (!"GET".equals(requestSpec.getMethod())) { + appendParameters(parameters, requestSpec.getFormParams()); + } + return parameters.toString().getBytes(StandardCharsets.ISO_8859_1); + } + + private void appendParameters(StringBuilder content, Map parameters) { + for (Entry entry : parameters.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof Iterable) { + for (Object v : (Iterable) value) { + append(content, name, v.toString()); + } + } + else if (value != null) { + append(content, name, value.toString()); + } + else { + append(content, name); + } + } } private byte[] convertContent(Object content) { @@ -131,28 +163,6 @@ private boolean isAllMediaTypesAcceptHeader(Header header) { return HttpHeaders.ACCEPT.equals(header.getName()) && "*/*".equals(header.getValue()); } - private Parameters extractParameters(FilterableRequestSpecification requestSpec) { - Parameters parameters = new Parameters(); - for (Entry entry : requestSpec.getQueryParams().entrySet()) { - if (entry.getValue() instanceof Collection) { - Collection queryParams = ((Collection) entry.getValue()); - for (Object queryParam : queryParams) { - parameters.add(entry.getKey(), queryParam.toString()); - } - } - else { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - } - for (Entry entry : requestSpec.getRequestParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - for (Entry entry : requestSpec.getFormParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - return parameters; - } - private Collection extractParts(FilterableRequestSpecification requestSpec) { List parts = new ArrayList<>(); for (MultiPartSpecification multiPartSpec : requestSpec.getMultiPartParams()) { @@ -165,4 +175,26 @@ private Collection extractParts(FilterableRequestSpecifica return parts; } + private static void append(StringBuilder sb, String key) { + append(sb, key, ""); + } + + private static void append(StringBuilder sb, String key, String value) { + doAppend(sb, urlEncode(key) + "=" + urlEncode(value)); + } + + private static void doAppend(StringBuilder sb, String toAppend) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(toAppend); + } + + private static String urlEncode(String s) { + if (!StringUtils.hasLength(s)) { + return ""; + } + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + } 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 new file mode 100644 index 00000000..85a1b21b --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java @@ -0,0 +1,220 @@ +/* + * 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.restassured; + +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.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to verify that the understanding of REST Assured's parameter handling behavior is + * correct. + * + * @author Andy Wilkinson + */ +public class RestAssuredParameterBehaviorTests { + + private static final MediaType APPLICATION_FORM_URLENCODED_ISO_8859_1 = MediaType + .parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=ISO-8859-1"); + + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); + + private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); + + private OperationRequest request; + + private RequestSpecification spec = RestAssured.given().port(tomcat.getPort()) + .filter((request, response, context) -> { + this.request = this.factory.convert(request); + return context.next(request, response); + }); + + @Test + public void queryParameterOnGet() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").get("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.GET); + } + + @Test + public void queryParameterOnHead() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").head("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.HEAD); + } + + @Test + public void queryParameterOnPost() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").post("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.POST); + } + + @Test + public void queryParameterOnPut() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").put("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PUT); + } + + @Test + public void queryParameterOnPatch() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").patch("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PATCH); + } + + @Test + public void queryParameterOnDelete() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").delete("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.DELETE); + } + + @Test + public void queryParameterOnOptions() { + this.spec.queryParam("a", "alpha", "apple").queryParam("b", "bravo").options("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.OPTIONS); + } + + @Test + public 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").get("/query-parameter").then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.GET); + } + + @Test + public void formParamOnHead() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").head("/form-url-encoded").then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.HEAD); + } + + @Test + public void formParamOnPost() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").post("/form-url-encoded").then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.POST); + } + + @Test + public void formParamOnPut() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").put("/form-url-encoded").then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.PUT); + } + + @Test + public void formParamOnPatch() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").patch("/form-url-encoded").then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.PATCH); + } + + @Test + public void formParamOnDelete() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").delete("/form-url-encoded").then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.DELETE); + } + + @Test + public void formParamOnOptions() { + this.spec.formParam("a", "alpha", "apple").formParam("b", "bravo").options("/form-url-encoded").then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.OPTIONS); + } + + private OperationRequestAssert assertThatRequest(OperationRequest request) { + return new OperationRequestAssert(request); + } + + private static final class OperationRequestAssert extends AbstractAssert { + + private OperationRequestAssert(OperationRequest actual) { + super(actual, OperationRequestAssert.class); + } + + private void isFormUrlEncodedWithMethod(HttpMethod method) { + assertThat(this.actual.getMethod()).isEqualTo(method); + assertThat(this.actual.getUri().getRawQuery()).isNull(); + assertThat(this.actual.getContentAsString()).isEqualTo("a=alpha&a=apple&b=bravo"); + assertThat(this.actual.getHeaders().getContentType()).isEqualTo(APPLICATION_FORM_URLENCODED_ISO_8859_1); + } + + private void hasQueryParametersWithMethod(HttpMethod method) { + assertThat(this.actual.getMethod()).isEqualTo(method); + assertThat(this.actual.getUri().getRawQuery()).isEqualTo("a=alpha&a=apple&b=bravo"); + assertThat(this.actual.getContentAsString()).isEmpty(); + } + + } + +} 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 ba17a0b5..ae8054b5 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 @@ -21,7 +21,6 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.net.URI; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -79,8 +78,7 @@ public void queryStringParameters() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).queryParam("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); + assertThat(request.getUri().getRawQuery()).isEqualTo("foo=bar"); } @Test @@ -88,26 +86,15 @@ public void queryStringFromUrlParameters() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/?foo=bar&foo=qix"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Arrays.asList("bar", "qix")); + assertThat(request.getUri().getRawQuery()).isEqualTo("foo=bar&foo=qix"); } @Test - public void formParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).formParam("foo", "bar"); - requestSpec.get("/"); - OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); - } - - @Test - public void requestParameters() { + public void paramOnGetRequestIsMappedToQueryString() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).param("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters()).hasSize(1); - assertThat(request.getParameters()).containsEntry("foo", Collections.singletonList("bar")); + assertThat(request.getUri().getRawQuery()).isEqualTo("foo=bar"); } @Test 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 18bacf8e..37aa74c3 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 @@ -68,7 +68,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; @@ -143,7 +143,7 @@ public void curlSnippetWithQueryStringOnPost() { .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") .withContent(String.format("$ curl " + "'http://localhost:" + tomcat.getPort() + "/?foo=bar' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" - + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'a=alpha'")))); + + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'foo=bar&a=alpha'")))); } @Test @@ -166,13 +166,13 @@ public void pathParametersSnippet() { } @Test - public void requestParametersSnippet() { + public void queryParametersSnippet() { given().port(tomcat.getPort()).filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-parameters", - requestParameters(parameterWithName("foo").description("The description")))) + .filter(document("query-parameters", + queryParameters(parameterWithName("foo").description("The description")))) .accept("application/json").param("foo", "bar").get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parameters"), "http-request.adoc", - "http-response.adoc", "curl-request.adoc", "request-parameters.adoc"); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/query-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "query-parameters.adoc"); } @Test @@ -385,8 +385,10 @@ private Condition content(final Condition delegate) { @Override public boolean matches(File value) { try { - return delegate.matches(FileCopyUtils - .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8))); + String copyToString = FileCopyUtils + .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8)); + System.out.println(copyToString); + return delegate.matches(copyToString); } catch (IOException ex) { fail("Failed to read '" + value + "'", ex); 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 38e5fbb3..55e3badb 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 @@ -17,6 +17,7 @@ package org.springframework.restdocs.restassured; import java.io.IOException; +import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -33,6 +34,9 @@ import org.apache.catalina.startup.Tomcat; import org.junit.rules.ExternalResource; +import org.springframework.http.MediaType; +import org.springframework.util.FileCopyUtils; + /** * {@link ExternalResource} that starts and stops a Tomcat server. * @@ -53,6 +57,10 @@ protected void before() throws LifecycleException { 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(); } @@ -121,4 +129,33 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } + private static final class QueryParameterServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (!req.getQueryString().equals("a=alpha&a=apple&b=bravo")) { + throw new ServletException("Incorrect query string"); + } + resp.setStatus(200); + } + + } + + private static final class FormUrlEncodedServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (!MediaType.APPLICATION_FORM_URLENCODED + .isCompatibleWith(MediaType.parseMediaType(req.getContentType()))) { + throw new ServletException("Incorrect Content-Type"); + } + String content = FileCopyUtils.copyToString(new InputStreamReader(req.getInputStream())); + if (!"a=alpha&a=apple&b=bravo".equals(content)) { + throw new ServletException("Incorrect body content"); + } + resp.setStatus(200); + } + + } + } diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java index 4afabb82..30ebbaec 100644 --- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java @@ -30,9 +30,7 @@ import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpInputMessage; -import org.springframework.http.codec.FormHttpMessageReader; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.FilePart; @@ -42,14 +40,11 @@ import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.QueryStringParser; import org.springframework.restdocs.operation.RequestConverter; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * A {@link RequestConverter} for creating an {@link OperationRequest} derived from an @@ -59,18 +54,11 @@ */ class WebTestClientRequestConverter implements RequestConverter { - private static final ResolvableType FORM_DATA_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, - String.class, String.class); - - private final QueryStringParser queryStringParser = new QueryStringParser(); - - private final FormHttpMessageReader formDataReader = new FormHttpMessageReader(); - @Override public OperationRequest convert(ExchangeResult result) { HttpHeaders headers = extractRequestHeaders(result); return new OperationRequestFactory().create(result.getUrl(), result.getMethod(), result.getRequestBodyContent(), - headers, extractParameters(result), extractRequestParts(result), extractCookies(headers)); + headers, extractRequestParts(result), extractCookies(headers)); } private HttpHeaders extractRequestHeaders(ExchangeResult result) { @@ -80,16 +68,6 @@ private HttpHeaders extractRequestHeaders(ExchangeResult result) { return extracted; } - private Parameters extractParameters(ExchangeResult result) { - Parameters parameters = new Parameters(); - parameters.addAll(this.queryStringParser.parse(result.getUrl())); - if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(result.getRequestHeaders().getContentType())) { - parameters.addAll(this.formDataReader - .readMono(FORM_DATA_TYPE, new ExchangeResultReactiveHttpInputMessage(result), null).block()); - } - return parameters; - } - private List extractRequestParts(ExchangeResult result) { HttpMessageReader partHttpMessageReader = new DefaultPartHttpMessageReader(); return new MultipartHttpMessageReader(partHttpMessageReader) 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 d416c084..ac6e32dc 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 @@ -17,6 +17,7 @@ package org.springframework.restdocs.webtestclient; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.junit.Test; @@ -103,15 +104,12 @@ public void httpsRequestWithCustomPort() { } @Test - public void getRequestWithQueryStringPopulatesParameters() { + public void getRequestWithQueryString() { ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) .configureClient().baseUrl("http://localhost").build().get().uri("/foo?a=alpha&b=bravo").exchange() .expectBody().returnResult(); OperationRequest request = this.converter.convert(result); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&b=bravo")); - assertThat(request.getParameters()).hasSize(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("bravo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @@ -128,9 +126,9 @@ public void postRequestWithFormDataParameters() { OperationRequest request = this.converter.convert(result); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); - assertThat(request.getParameters()).hasSize(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("br&vo")); + assertThat(request.getContentAsString()).isEqualTo("a=alpha&a=apple&b=br%26vo"); + assertThat(request.getHeaders().getContentType()) + .isEqualTo(new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8)); } @Test @@ -144,9 +142,6 @@ public void postRequestWithQueryStringParameters() { OperationRequest request = this.converter.convert(result); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); - assertThat(request.getParameters()).hasSize(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("br&vo")); } @Test @@ -162,9 +157,9 @@ public void postRequestWithQueryStringAndFormDataParameters() { OperationRequest request = this.converter.convert(result); assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&b=br%26vo")); assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); - assertThat(request.getParameters()).hasSize(2); - assertThat(request.getParameters()).containsEntry("a", Arrays.asList("alpha", "apple")); - assertThat(request.getParameters()).containsEntry("b", Arrays.asList("br&vo")); + assertThat(request.getContentAsString()).isEqualTo("a=apple"); + assertThat(request.getHeaders().getContentType()) + .isEqualTo(new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8)); } @Test 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 57b7ef30..87662a43 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 @@ -64,7 +64,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; @@ -122,12 +122,11 @@ public void pathParametersSnippet() { } @Test - public void requestParametersSnippet() { - this.webTestClient.get().uri("/?a=alpha&b=bravo").exchange().expectStatus().isOk().expectBody() - .consumeWith(document("request-parameters", - requestParameters(parameterWithName("a").description("Alpha description"), - parameterWithName("b").description("Bravo description")))); - assertThat(new File("build/generated-snippets/request-parameters/request-parameters.adoc")) + public void queryParametersSnippet() { + this.webTestClient.get().uri("/?a=alpha&b=bravo").exchange().expectStatus().isOk().expectBody().consumeWith( + document("query-parameters", queryParameters(parameterWithName("a").description("Alpha description"), + parameterWithName("b").description("Bravo description")))); + assertThat(new File("build/generated-snippets/query-parameters/query-parameters.adoc")) .has(content(tableWithHeader(TemplateFormats.asciidoctor(), "Parameter", "Description") .row("`a`", "Alpha description").row("`b`", "Bravo description"))); } @@ -204,7 +203,7 @@ private void assertExpectedSnippetFilesExist(File directory, String... snippets) } private Condition content(final Condition delegate) { - return new Condition() { + return new Condition<>() { @Override public boolean matches(File value) { From 03b61ab075bd205fd0b7eb097104478860ddd640 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 12 Oct 2022 11:26:01 +0100 Subject: [PATCH 074/198] Use Framework's HttpStatusCode to model response's status Closes gh-848 --- .../restdocs/http/HttpResponseSnippet.java | 15 +++++---------- .../restdocs/operation/OperationResponse.java | 14 +++----------- .../operation/OperationResponseFactory.java | 14 ++++++++------ .../operation/StandardOperationResponse.java | 14 +++++--------- .../UriModifyingOperationPreprocessor.java | 2 +- .../restdocs/RestDocumentationGeneratorTests.java | 8 +++++--- .../config/RestDocumentationConfigurerTests.java | 2 +- .../restdocs/http/HttpResponseSnippetTests.java | 9 +++++---- .../hypermedia/ContentTypeLinkExtractorTests.java | 8 ++++---- .../hypermedia/LinkExtractorsPayloadTests.java | 4 ++-- ...ontentModifyingOperationPreprocessorTests.java | 2 +- ...eadersModifyingOperationPreprocessorTests.java | 2 +- .../UriModifyingOperationPreprocessorTests.java | 4 ++-- .../restdocs/testfixtures/OperationBuilder.java | 5 +++-- .../mockmvc/MockMvcResponseConverter.java | 3 ++- .../mockmvc/MockMvcResponseConverterTests.java | 4 ++-- .../restassured/RestAssuredResponseConverter.java | 5 +++-- .../RestAssuredResponseConverterTests.java | 4 ++-- .../WebTestClientResponseConverter.java | 2 +- .../WebTestClientResponseConverterTests.java | 3 ++- 20 files changed, 58 insertions(+), 66 deletions(-) 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 7e4a9c11..ec56d397 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-2019 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import java.util.Map.Entry; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.Snippet; @@ -59,15 +60,9 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); model.put("responseBody", responseBody(response)); model.put("headers", headers(response)); - HttpStatus status = response.getStatus(); - if (status != null) { - model.put("statusCode", status.value()); - model.put("statusReason", status.getReasonPhrase()); - } - else { - model.put("statusCode", response.getStatusCode()); - model.put("statusReason", ""); - } + HttpStatusCode status = response.getStatus(); + model.put("statusCode", status.value()); + model.put("statusReason", (status instanceof HttpStatus) ? ((HttpStatus) status).getReasonPhrase() : ""); return model; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index 8490ade2..344b4e9a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -19,7 +19,7 @@ import java.util.Collection; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; /** * The response that was received as part of performing an operation on a RESTful service. @@ -33,17 +33,9 @@ public interface OperationResponse { /** * Returns the status of the response. - * @return the status or {@code null} if the status is unknown to {@link HttpStatus} + * @return the status, never {@code null} */ - HttpStatus getStatus(); - - /** - * Returns the status code of the response. - * @return the status code - */ - default int getStatusCode() { - throw new UnsupportedOperationException(); - } + HttpStatusCode getStatus(); /** * Returns the headers in the response. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index 125f744b..b9c749ab 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; /** * A factory for creating {@link OperationResponse OperationResponses}. @@ -37,8 +38,9 @@ public class OperationResponseFactory { * @param headers the request's headers * @param content the content of the request * @return the {@code OperationResponse} + * @since 3.0.0 */ - public OperationResponse create(int status, HttpHeaders headers, byte[] content) { + public OperationResponse create(HttpStatusCode status, HttpHeaders headers, byte[] content) { return new StandardOperationResponse(status, augmentHeaders(headers, content), content, Collections.emptyList()); } @@ -52,9 +54,9 @@ public OperationResponse create(int status, HttpHeaders headers, byte[] content) * @param content the content of the request * @param cookies the cookies * @return the {@code OperationResponse} - * @since 3.0 + * @since 3.0.0 */ - public OperationResponse create(int status, HttpHeaders headers, byte[] content, + public OperationResponse create(HttpStatusCode status, HttpHeaders headers, byte[] content, Collection cookies) { return new StandardOperationResponse(status, augmentHeaders(headers, content), content, cookies); } @@ -69,8 +71,8 @@ public OperationResponse create(int status, HttpHeaders headers, byte[] content, * @return the new response with the new content */ public OperationResponse createFrom(OperationResponse original, byte[] newContent) { - return new StandardOperationResponse(original.getStatusCode(), - getUpdatedHeaders(original.getHeaders(), newContent), newContent, original.getCookies()); + return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders(original.getHeaders(), newContent), + newContent, original.getCookies()); } /** @@ -81,7 +83,7 @@ public OperationResponse createFrom(OperationResponse original, byte[] newConten * @return the new response with the new headers */ public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { - return new StandardOperationResponse(original.getStatusCode(), newHeaders, original.getContent(), + return new StandardOperationResponse(original.getStatus(), newHeaders, original.getContent(), original.getCookies()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 5d9045af..667d03ca 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -19,7 +19,7 @@ import java.util.Collection; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; /** * Standard implementation of {@link OperationResponse}. @@ -29,7 +29,7 @@ */ class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse { - private final int status; + private final HttpStatusCode status; private Collection cookies; @@ -41,19 +41,15 @@ class StandardOperationResponse extends AbstractOperationMessage implements Oper * @param content the content of the response * @param cookies any cookies included in the response */ - StandardOperationResponse(int status, HttpHeaders headers, byte[] content, Collection cookies) { + StandardOperationResponse(HttpStatusCode status, HttpHeaders headers, byte[] content, + Collection cookies) { super(content, headers); this.status = status; this.cookies = cookies; } @Override - public HttpStatus getStatus() { - return HttpStatus.resolve(this.status); - } - - @Override - public int getStatusCode() { + public HttpStatusCode getStatus() { return this.status; } 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 ef2f2223..ab293f32 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 @@ -141,7 +141,7 @@ public OperationRequest preprocess(OperationRequest request) { @Override public OperationResponse preprocess(OperationResponse response) { - return this.contentModifyingDelegate.preprocess(new OperationResponseFactory().create(response.getStatusCode(), + return this.contentModifyingDelegate.preprocess(new OperationResponseFactory().create(response.getStatus(), modify(response.getHeaders()), response.getContent(), response.getCookies())); } 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 c84ec96e..8c3df612 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-2019 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import org.mockito.Mockito; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -69,7 +70,8 @@ public class RestDocumentationGeneratorTests { private final OperationRequest operationRequest = new OperationRequestFactory() .create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(), null, null); - private final OperationResponse operationResponse = new OperationResponseFactory().create(0, null, null); + private final OperationResponse operationResponse = new OperationResponseFactory().create(HttpStatus.OK, null, + null); private final Snippet snippet = mock(Snippet.class); @@ -197,7 +199,7 @@ private static OperationRequest createRequest() { } private static OperationResponse createResponse() { - return new OperationResponseFactory().create(0, null, null); + return new OperationResponseFactory().create(HttpStatus.OK, null, null); } } 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 2141dee8..e9545b75 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 @@ -223,7 +223,7 @@ public void customDefaultOperationResponsePreprocessor() { .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR); HttpHeaders headers = new HttpHeaders(); headers.add("Foo", "value"); - OperationResponse response = new OperationResponseFactory().create(HttpStatus.OK.value(), headers, null); + OperationResponse response = new OperationResponseFactory().create(HttpStatus.OK, headers, null); assertThat(preprocessor.preprocess(response).getHeaders()).doesNotContainKey("Foo"); } 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 30287183..0b255af8 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-2019 the original author or authors. + * 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. @@ -22,6 +22,7 @@ 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; @@ -55,8 +56,7 @@ public void basicResponse() throws IOException { @Test public void nonOkResponse() throws IOException { - new HttpResponseSnippet() - .document(this.operationBuilder.response().status(HttpStatus.BAD_REQUEST.value()).build()); + new HttpResponseSnippet().document(this.operationBuilder.response().status(HttpStatus.BAD_REQUEST).build()); assertThat(this.generatedSnippets.httpResponse()).is(httpResponse(HttpStatus.BAD_REQUEST)); } @@ -99,7 +99,8 @@ public void responseWithCustomSnippetAttributes() throws IOException { @Test public void responseWithCustomStatus() throws IOException { - new HttpResponseSnippet().document(this.operationBuilder.response().status(215).build()); + new HttpResponseSnippet() + .document(this.operationBuilder.response().status(HttpStatusCode.valueOf(215)).build()); assertThat(this.generatedSnippets.httpResponse()).is(httpResponse(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 e987a5af..7544b315 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-2019 the original author or authors. + * 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. @@ -49,7 +49,7 @@ public class ContentTypeLinkExtractorTests { public void extractionFailsWithNullContentType() throws IOException { this.thrown.expect(IllegalStateException.class); new ContentTypeLinkExtractor() - .extractLinks(this.responseFactory.create(HttpStatus.OK.value(), new HttpHeaders(), null)); + .extractLinks(this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), null)); } @Test @@ -59,7 +59,7 @@ public void extractorCalledWithMatchingContextType() throws IOException { extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); - OperationResponse response = this.responseFactory.create(HttpStatus.OK.value(), httpHeaders, null); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); } @@ -71,7 +71,7 @@ public void extractorCalledWithCompatibleContextType() throws IOException { extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar")); - OperationResponse response = this.responseFactory.create(HttpStatus.OK.value(), httpHeaders, null); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); } 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 1eb99f19..62238011 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-2019 the original author or authors. + * 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. @@ -106,7 +106,7 @@ private void assertLinks(List expectedLinks, Map> actua } private OperationResponse createResponse(String contentName) throws IOException { - return this.responseFactory.create(HttpStatus.OK.value(), null, + return this.responseFactory.create(HttpStatus.OK, null, FileCopyUtils.copyToByteArray(getPayloadFile(contentName))); } 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 7e26b6da..dbbee922 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 @@ -65,7 +65,7 @@ public void modifyRequestContent() { @Test public void modifyResponseContent() { - OperationResponse response = this.responseFactory.create(HttpStatus.OK.value(), new HttpHeaders(), + OperationResponse response = this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), "content".getBytes()); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getContent()).isEqualTo("modified".getBytes()); 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 c33153f4..c6ce70f0 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 @@ -160,7 +160,7 @@ private OperationResponse createResponse(Consumer headersCustomizer if (headersCustomizer != null) { headersCustomizer.accept(headers); } - return new OperationResponseFactory().create(HttpStatus.OK.value(), headers, new byte[0]); + return new OperationResponseFactory().create(HttpStatus.OK, headers, new byte[0]); } } 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 d0353540..4ac459c2 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 @@ -340,13 +340,13 @@ private OperationRequest createRequestWithPartWithContent(String content) { } private OperationResponse createResponseWithContent(String content) { - return this.responseFactory.create(HttpStatus.OK.value(), new HttpHeaders(), content.getBytes()); + return this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), content.getBytes()); } private OperationResponse createResponseWithHeader(String name, String value) { HttpHeaders headers = new HttpHeaders(); headers.add(name, value); - return this.responseFactory.create(HttpStatus.OK.value(), headers, new byte[0]); + return this.responseFactory.create(HttpStatus.OK, headers, new byte[0]); } } 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/OperationBuilder.java index a546cbd7..e3d3d40c 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/OperationBuilder.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.mustache.Mustache; @@ -247,7 +248,7 @@ public OperationRequestPartBuilder header(String name, String value) { */ public final class OperationResponseBuilder { - private int status = HttpStatus.OK.value(); + private HttpStatusCode status = HttpStatus.OK; private HttpHeaders headers = new HttpHeaders(); @@ -259,7 +260,7 @@ private OperationResponse buildResponse() { return new OperationResponseFactory().create(this.status, this.headers, this.content, this.cookies); } - public OperationResponseBuilder status(int status) { + public OperationResponseBuilder status(HttpStatusCode status) { this.status = status; return this; } 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 56d44f7c..e3caca60 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 @@ -24,6 +24,7 @@ import jakarta.servlet.http.Cookie; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; @@ -44,7 +45,7 @@ class MockMvcResponseConverter implements ResponseConverter cookies = extractCookies(mockResponse); - return new OperationResponseFactory().create(mockResponse.getStatus(), headers, + return new OperationResponseFactory().create(HttpStatusCode.valueOf(mockResponse.getStatus()), headers, mockResponse.getContentAsByteArray(), cookies); } 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 40338091..1d208b43 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 @@ -24,6 +24,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.ResponseCookie; @@ -69,8 +70,7 @@ public void responseWithCustomStatus() { MockHttpServletResponse response = new MockHttpServletResponse(); response.setStatus(600); OperationResponse operationResponse = this.factory.convert(response); - assertThat(operationResponse.getStatus()).isNull(); - assertThat(operationResponse.getStatusCode()).isEqualTo(600); + assertThat(operationResponse.getStatus()).isEqualTo(HttpStatusCode.valueOf(600)); } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index 09b95978..a049ea1a 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -26,6 +26,7 @@ import io.restassured.response.Response; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.ResponseConverter; @@ -44,8 +45,8 @@ class RestAssuredResponseConverter implements ResponseConverter { public OperationResponse convert(Response response) { HttpHeaders headers = extractHeaders(response); Collection cookies = extractCookies(response, headers); - return new OperationResponseFactory().create(response.getStatusCode(), extractHeaders(response), - extractContent(response), cookies); + return new OperationResponseFactory().create(HttpStatusCode.valueOf(response.getStatusCode()), + extractHeaders(response), extractContent(response), cookies); } private Collection extractCookies(Response response, HttpHeaders headers) { 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 c3f79015..3760c656 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 @@ -21,6 +21,7 @@ import io.restassured.response.ResponseBody; import org.junit.Test; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.operation.OperationResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -45,8 +46,7 @@ public void responseWithCustomStatus() { given(response.getBody()).willReturn(body); given(body.asByteArray()).willReturn(new byte[0]); OperationResponse operationResponse = this.converter.convert(response); - assertThat(operationResponse.getStatus()).isNull(); - assertThat(operationResponse.getStatusCode()).isEqualTo(600); + assertThat(operationResponse.getStatus()).isEqualTo(HttpStatusCode.valueOf(600)); } } 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 fc7d4147..f74c8c12 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 @@ -40,7 +40,7 @@ class WebTestClientResponseConverter implements ResponseConverter cookies = extractCookies(result); - return new OperationResponseFactory().create(result.getStatus().value(), extractHeaders(result), + return new OperationResponseFactory().create(result.getStatus(), extractHeaders(result), result.getResponseBodyContent(), cookies); } 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 3c7ad67a..622e0708 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 @@ -22,6 +22,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.ResponseCookie; @@ -83,7 +84,7 @@ public void responseWithNonStandardStatusCode() { .configureClient().baseUrl("http://localhost").build().get().uri("/foo").exchange().expectBody() .returnResult(); OperationResponse response = this.converter.convert(result); - assertThat(response.getStatusCode()).isEqualTo(210); + assertThat(response.getStatus()).isEqualTo(HttpStatusCode.valueOf(210)); } } From 4807bec4b78b58536177775494025f1020021fcb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 15 Oct 2022 16:13:25 +0100 Subject: [PATCH 075/198] Remove samples in favor of those in separate repository Closes gh-852 --- build.gradle | 46 +- buildSrc/build.gradle | 1 - .../samples/SampleBuildConfigurer.groovy | 164 - .../build/samples/SamplesExtension.groovy | 49 - .../build/samples/SamplesPlugin.groovy | 31 - .../gradle-plugins/samples.properties | 1 - ci/scripts/build-pr-project.sh | 2 +- ci/scripts/build-project.sh | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 6 +- docs/src/docs/asciidoc/getting-started.adoc | 60 +- docs/src/docs/asciidoc/index.adoc | 2 +- samples/junit5/build.gradle | 60 - .../junit5/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/junit5/gradlew | 185 - samples/junit5/gradlew.bat | 89 - samples/junit5/settings.gradle | 0 samples/junit5/src/docs/asciidoc/index.adoc | 22 - .../junit5/SampleJUnit5Application.java | 36 - .../junit5/SampleJUnit5ApplicationTests.java | 60 - samples/rest-assured/build.gradle | 67 - .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/rest-assured/gradlew | 185 - samples/rest-assured/gradlew.bat | 89 - samples/rest-assured/settings.gradle | 0 .../rest-assured/src/docs/asciidoc/index.adoc | 26 - .../SampleRestAssuredApplication.java | 71 - .../SampleRestAssuredApplicationTests.java | 77 - samples/rest-notes-slate/README.md | 24 - samples/rest-notes-slate/build.gradle | 70 - .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/rest-notes-slate/gradlew | 185 - samples/rest-notes-slate/gradlew.bat | 89 - samples/rest-notes-slate/settings.gradle | 0 samples/rest-notes-slate/slate/.dockerignore | 12 - samples/rest-notes-slate/slate/.gitignore | 27 - samples/rest-notes-slate/slate/CHANGELOG.md | 320 - samples/rest-notes-slate/slate/Dockerfile | 26 - samples/rest-notes-slate/slate/Gemfile | 13 - samples/rest-notes-slate/slate/Gemfile.lock | 145 - samples/rest-notes-slate/slate/LICENSE | 201 - samples/rest-notes-slate/slate/README.md | 82 - samples/rest-notes-slate/slate/Vagrantfile | 47 - samples/rest-notes-slate/slate/config.rb | 63 - samples/rest-notes-slate/slate/deploy.sh | 226 - .../slate/font-selection.json | 148 - .../slate/lib/monokai_sublime_slate.rb | 95 - .../rest-notes-slate/slate/lib/multilang.rb | 16 - .../slate/lib/nesting_unique_head.rb | 22 - .../rest-notes-slate/slate/lib/toc_data.rb | 31 - .../rest-notes-slate/slate/lib/unique_head.rb | 24 - samples/rest-notes-slate/slate/slate.sh | 248 - .../slate/source/api-guide.html.md.erb | 206 - .../slate/source/fonts/slate.eot | Bin 1876 -> 0 bytes .../slate/source/fonts/slate.svg | 14 - .../slate/source/fonts/slate.ttf | Bin 1720 -> 0 bytes .../slate/source/fonts/slate.woff | Bin 1796 -> 0 bytes .../slate/source/fonts/slate.woff2 | Bin 796 -> 0 bytes .../slate/source/images/logo.png | Bin 22317 -> 0 bytes .../slate/source/images/navbar.png | Bin 96 -> 0 bytes .../slate/source/includes/_errors.md | 22 - .../slate/source/index.html.md | 245 - .../slate/source/javascripts/all.js | 2 - .../slate/source/javascripts/all_nosearch.js | 27 - .../slate/source/javascripts/app/_copy.js | 15 - .../slate/source/javascripts/app/_lang.js | 171 - .../slate/source/javascripts/app/_search.js | 102 - .../slate/source/javascripts/app/_toc.js | 122 - .../slate/source/javascripts/lib/_energize.js | 169 - .../javascripts/lib/_imagesloaded.min.js | 7 - .../javascripts/lib/_jquery.highlight.js | 108 - .../slate/source/javascripts/lib/_jquery.js | 10881 ---------------- .../slate/source/javascripts/lib/_lunr.js | 3475 ----- .../slate/source/layouts/layout.erb | 137 - .../slate/source/stylesheets/_icon-font.scss | 38 - .../slate/source/stylesheets/_normalize.scss | 427 - .../slate/source/stylesheets/_rtl.scss | 140 - .../slate/source/stylesheets/_variables.scss | 103 - .../slate/source/stylesheets/print.css.scss | 153 - .../slate/source/stylesheets/screen.css.scss | 633 - .../com/example/notes/ErrorController.java | 45 - .../src/main/java/com/example/notes/Note.java | 75 - .../com/example/notes/NoteRepository.java | 23 - .../com/example/notes/RestNotesSlate.java | 73 - .../src/main/java/com/example/notes/Tag.java | 65 - .../java/com/example/notes/TagRepository.java | 23 - .../com/example/notes/ApiDocumentation.java | 348 - .../.mvn/wrapper/maven-wrapper.jar | Bin 49519 -> 0 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 - samples/rest-notes-spring-data-rest/mvnw | 234 - samples/rest-notes-spring-data-rest/mvnw.cmd | 145 - samples/rest-notes-spring-data-rest/pom.xml | 222 - .../src/main/asciidoc/api-guide.adoc | 228 - .../main/asciidoc/getting-started-guide.adoc | 175 - .../com/example/notes/ErrorController.java | 44 - .../src/main/java/com/example/notes/Note.java | 75 - .../com/example/notes/NoteRepository.java | 23 - .../notes/RestNotesSpringDataRest.java | 73 - .../src/main/java/com/example/notes/Tag.java | 65 - .../java/com/example/notes/TagRepository.java | 23 - .../src/main/resources/application.properties | 1 - .../com/example/notes/ApiDocumentation.java | 353 - .../notes/GettingStartedDocumentation.java | 198 - .../rest-notes-spring-hateoas/build.gradle | 72 - .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/rest-notes-spring-hateoas/gradlew | 185 - samples/rest-notes-spring-hateoas/gradlew.bat | 89 - .../rest-notes-spring-hateoas/settings.gradle | 0 .../src/docs/asciidoc/api-guide.adoc | 229 - .../docs/asciidoc/getting-started-guide.adoc | 173 - .../com/example/notes/ErrorController.java | 45 - .../com/example/notes/IndexController.java | 38 - .../src/main/java/com/example/notes/Note.java | 73 - .../java/com/example/notes/NoteInput.java | 58 - .../com/example/notes/NotePatchInput.java | 55 - .../com/example/notes/NoteRepository.java | 30 - .../NoteRepresentationModelAssembler.java | 67 - .../com/example/notes/NotesController.java | 145 - .../com/example/notes/NullOrNotBlank.java | 46 - .../notes/ResourceDoesNotExistException.java | 22 - .../notes/RestNotesControllerAdvice.java | 44 - .../example/notes/RestNotesSpringHateoas.java | 73 - .../src/main/java/com/example/notes/Tag.java | 63 - .../main/java/com/example/notes/TagInput.java | 38 - .../java/com/example/notes/TagPatchInput.java | 36 - .../java/com/example/notes/TagRepository.java | 25 - .../TagRepresentationModelAssembler.java | 64 - .../com/example/notes/TagsController.java | 102 - .../com/example/notes/ApiDocumentation.java | 409 - .../notes/GettingStartedDocumentation.java | 205 - .../example/notes/NullOrNotBlankTests.java | 68 - .../ConstraintDescriptions.properties | 2 - .../restdocs/templates/request-fields.snippet | 11 - samples/testng/build.gradle | 58 - .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/testng/gradlew | 185 - samples/testng/gradlew.bat | 89 - samples/testng/settings.gradle | 0 samples/testng/src/docs/asciidoc/index.adoc | 22 - .../testng/SampleTestNgApplication.java | 36 - .../testng/SampleTestNgApplicationTests.java | 68 - samples/web-test-client/build.gradle | 59 - .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - samples/web-test-client/gradlew | 185 - samples/web-test-client/gradlew.bat | 89 - samples/web-test-client/settings.gradle | 0 .../src/docs/asciidoc/index.adoc | 33 - .../SampleWebTestClientApplication.java | 37 - .../SampleWebTestClientApplicationTests.java | 60 - 154 files changed, 9 insertions(+), 26563 deletions(-) delete mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SampleBuildConfigurer.groovy delete mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesExtension.groovy delete mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesPlugin.groovy delete mode 100644 buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties delete mode 100644 samples/junit5/build.gradle delete mode 100644 samples/junit5/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/junit5/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/junit5/gradlew delete mode 100644 samples/junit5/gradlew.bat delete mode 100644 samples/junit5/settings.gradle delete mode 100644 samples/junit5/src/docs/asciidoc/index.adoc delete mode 100644 samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java delete mode 100644 samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java delete mode 100644 samples/rest-assured/build.gradle delete mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/rest-assured/gradlew delete mode 100644 samples/rest-assured/gradlew.bat delete mode 100644 samples/rest-assured/settings.gradle delete mode 100644 samples/rest-assured/src/docs/asciidoc/index.adoc delete mode 100644 samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java delete mode 100644 samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java delete mode 100644 samples/rest-notes-slate/README.md delete mode 100644 samples/rest-notes-slate/build.gradle delete mode 100644 samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/rest-notes-slate/gradlew delete mode 100644 samples/rest-notes-slate/gradlew.bat delete mode 100644 samples/rest-notes-slate/settings.gradle delete mode 100644 samples/rest-notes-slate/slate/.dockerignore delete mode 100644 samples/rest-notes-slate/slate/.gitignore delete mode 100644 samples/rest-notes-slate/slate/CHANGELOG.md delete mode 100644 samples/rest-notes-slate/slate/Dockerfile delete mode 100644 samples/rest-notes-slate/slate/Gemfile delete mode 100644 samples/rest-notes-slate/slate/Gemfile.lock delete mode 100644 samples/rest-notes-slate/slate/LICENSE delete mode 100644 samples/rest-notes-slate/slate/README.md delete mode 100644 samples/rest-notes-slate/slate/Vagrantfile delete mode 100644 samples/rest-notes-slate/slate/config.rb delete mode 100755 samples/rest-notes-slate/slate/deploy.sh delete mode 100755 samples/rest-notes-slate/slate/font-selection.json delete mode 100644 samples/rest-notes-slate/slate/lib/monokai_sublime_slate.rb delete mode 100644 samples/rest-notes-slate/slate/lib/multilang.rb delete mode 100644 samples/rest-notes-slate/slate/lib/nesting_unique_head.rb delete mode 100644 samples/rest-notes-slate/slate/lib/toc_data.rb delete mode 100644 samples/rest-notes-slate/slate/lib/unique_head.rb delete mode 100755 samples/rest-notes-slate/slate/slate.sh delete mode 100644 samples/rest-notes-slate/slate/source/api-guide.html.md.erb delete mode 100644 samples/rest-notes-slate/slate/source/fonts/slate.eot delete mode 100644 samples/rest-notes-slate/slate/source/fonts/slate.svg delete mode 100644 samples/rest-notes-slate/slate/source/fonts/slate.ttf delete mode 100644 samples/rest-notes-slate/slate/source/fonts/slate.woff delete mode 100644 samples/rest-notes-slate/slate/source/fonts/slate.woff2 delete mode 100644 samples/rest-notes-slate/slate/source/images/logo.png delete mode 100644 samples/rest-notes-slate/slate/source/images/navbar.png delete mode 100644 samples/rest-notes-slate/slate/source/includes/_errors.md delete mode 100644 samples/rest-notes-slate/slate/source/index.html.md delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/all.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_copy.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_lang.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_search.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_toc.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js delete mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js delete mode 100644 samples/rest-notes-slate/slate/source/layouts/layout.erb delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_normalize.scss delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_rtl.scss delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_variables.scss delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/print.css.scss delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss delete mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/ErrorController.java delete mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/Note.java delete mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java delete mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java delete mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java delete mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java delete mode 100644 samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java delete mode 100644 samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar delete mode 100644 samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties delete mode 100755 samples/rest-notes-spring-data-rest/mvnw delete mode 100644 samples/rest-notes-spring-data-rest/mvnw.cmd delete mode 100644 samples/rest-notes-spring-data-rest/pom.xml delete mode 100644 samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc delete mode 100644 samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc delete mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/ErrorController.java delete mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java delete mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java delete mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java delete mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java delete mode 100644 samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java delete mode 100644 samples/rest-notes-spring-data-rest/src/main/resources/application.properties delete mode 100644 samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java delete mode 100644 samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java delete mode 100644 samples/rest-notes-spring-hateoas/build.gradle delete mode 100644 samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/rest-notes-spring-hateoas/gradlew delete mode 100644 samples/rest-notes-spring-hateoas/gradlew.bat delete mode 100644 samples/rest-notes-spring-hateoas/settings.gradle delete mode 100644 samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc delete mode 100644 samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ErrorController.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepresentationModelAssembler.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepresentationModelAssembler.java delete mode 100644 samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java delete mode 100644 samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java delete mode 100644 samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java delete mode 100644 samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java delete mode 100644 samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties delete mode 100644 samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet delete mode 100644 samples/testng/build.gradle delete mode 100644 samples/testng/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/testng/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/testng/gradlew delete mode 100644 samples/testng/gradlew.bat delete mode 100644 samples/testng/settings.gradle delete mode 100644 samples/testng/src/docs/asciidoc/index.adoc delete mode 100644 samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java delete mode 100644 samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java delete mode 100644 samples/web-test-client/build.gradle delete mode 100644 samples/web-test-client/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/web-test-client/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/web-test-client/gradlew delete mode 100644 samples/web-test-client/gradlew.bat delete mode 100644 samples/web-test-client/settings.gradle delete mode 100644 samples/web-test-client/src/docs/asciidoc/index.adoc delete mode 100644 samples/web-test-client/src/main/java/com/example/webtestclient/SampleWebTestClientApplication.java delete mode 100644 samples/web-test-client/src/test/java/com/example/webtestclient/SampleWebTestClientApplicationTests.java diff --git a/build.gradle b/build.gradle index 0076f29d..b8f3fa28 100644 --- a/build.gradle +++ b/build.gradle @@ -18,12 +18,10 @@ allprojects { } } -apply plugin: "samples" apply plugin: "io.spring.nohttp" apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" nohttp { - source.exclude "samples/rest-notes-slate/slate/**" source.exclude "buildSrc/.gradle/**" source.exclude "**/build/**" source.exclude "**/target/**" @@ -114,44 +112,6 @@ subprojects { subproject -> } -samples { - dependOn "spring-restdocs-core:publishToMavenLocal" - dependOn "spring-restdocs-mockmvc:publishToMavenLocal" - dependOn "spring-restdocs-restassured:publishToMavenLocal" - dependOn "spring-restdocs-webtestclient:publishToMavenLocal" - dependOn "spring-restdocs-asciidoctor:publishToMavenLocal" - - restNotesSpringHateoas { - workingDir "$projectDir/samples/rest-notes-spring-hateoas" - } - - restNotesSpringDataRest { - workingDir "$projectDir/samples/rest-notes-spring-data-rest" - } - - testNg { - workingDir "$projectDir/samples/testng" - } - - restAssured { - workingDir "$projectDir/samples/rest-assured" - } - - webTestClient { - workingDir "$projectDir/samples/web-test-client" - } - - slate { - workingDir "$projectDir/samples/rest-notes-slate" - build false - } - - junit5 { - workingDir "$projectDir/samples/junit5" - } - -} - task api (type: Javadoc) { group = "Documentation" description = "Generates aggregated Javadoc API documentation." @@ -179,7 +139,7 @@ task api (type: Javadoc) { } } -task docsZip(type: Zip, dependsOn: [":docs:asciidoctor", ":api", ":restNotesSpringHateoasGradle"]) { +task docsZip(type: Zip, dependsOn: [":docs:asciidoctor", ":api"]) { group = "Distribution" archiveBaseName = "spring-restdocs" archiveClassifier = "docs" @@ -193,10 +153,6 @@ task docsZip(type: Zip, dependsOn: [":docs:asciidoctor", ":api", ":restNotesSpri from(api) { into "api" } - - from(file("samples/rest-notes-spring-hateoas/build/docs/asciidoc")) { - into "samples/restful-notes" - } } publishing { diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 400a425c..6746f180 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,4 +1,3 @@ plugins { id "java-gradle-plugin" - id "groovy-gradle-plugin" } diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SampleBuildConfigurer.groovy deleted file mode 100644 index 530f8ae1..00000000 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SampleBuildConfigurer.groovy +++ /dev/null @@ -1,164 +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.build.samples - -import org.gradle.api.GradleException -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.tasks.Exec -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.GradleBuild - -class SampleBuildConfigurer { - - private final String name - - private String workingDir - - private boolean build = true - - SampleBuildConfigurer(String name) { - this.name = name - } - - void workingDir(String workingDir) { - this.workingDir = workingDir - } - - void build(boolean build) { - this.build = build - } - - Task createTask(Project project, Object... dependencies) { - File sampleDir = new File(this.workingDir).absoluteFile - - Task sampleBuild = project.tasks.create name - sampleBuild.description = "Builds the ${name} sample" - sampleBuild.group = "Build" - - if (new File(sampleDir, 'build.gradle').isFile()) { - Task gradleVersionsUpdate = createGradleVersionsUpdate(project) - sampleBuild.dependsOn gradleVersionsUpdate - if (build) { - Task gradleBuild = createGradleBuild(project, sampleDir, dependencies) - Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'build/docs/asciidoc')) - verifyIncludesTask.dependsOn gradleBuild - sampleBuild.dependsOn verifyIncludesTask - gradleBuild.dependsOn gradleVersionsUpdate - } - } - else if (new File(sampleDir, 'pom.xml').isFile()) { - Task mavenVersionsUpdate = createMavenVersionsUpdate(project) - sampleBuild.dependsOn mavenVersionsUpdate - if (build) { - Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) - Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) - verifyIncludesTask.dependsOn(mavenBuild) - sampleBuild.dependsOn verifyIncludesTask - mavenBuild.dependsOn mavenVersionsUpdate - } - } - else { - throw new IllegalStateException("No pom.xml or build.gradle was found in $sampleDir") - } - return sampleBuild - } - - private Task createMavenVersionsUpdate(Project project) { - Task mavenVersionsUpdate = project.tasks.create "${name}MavenVersionUpdates" - mavenVersionsUpdate.doFirst { - replaceVersion(new File(this.workingDir, 'pom.xml'), - '.*', - "${project.version}") - } - return mavenVersionsUpdate - } - - private Task createGradleVersionsUpdate(Project project) { - Task gradleVersionsUpdate = project.tasks.create "${name}GradleVersionUpdates" - gradleVersionsUpdate.doFirst { - replaceVersion(new File(this.workingDir, 'build.gradle'), - "ext\\['spring-restdocs.version'\\] = '.*'", - "ext['spring-restdocs.version'] = '${project.version}'") - replaceVersion(new File(this.workingDir, 'build.gradle'), - "restDocsVersion = \".*\"", - "restDocsVersion = \"${project.version}\"") - } - return gradleVersionsUpdate - } - - private Task createMavenBuild(Project project, File sampleDir, Object... dependencies) { - Task mavenBuild = project.tasks.create("${name}Maven", Exec) - mavenBuild.description = "Builds the ${name} sample with Maven" - mavenBuild.group = "Build" - mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] - mavenBuild.dependsOn dependencies - return mavenBuild - } - - private boolean isWindows() { - return File.separatorChar == '\\' - } - - private Task createGradleBuild(Project project, File sampleDir, Object... dependencies) { - Task gradleBuild = project.tasks.create("${name}Gradle", Exec) - gradleBuild.description = "Builds the ${name} sample with Gradle" - gradleBuild.group = "Build" - gradleBuild.workingDir = this.workingDir - gradleBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/gradlew.bat" : './gradlew', 'clean', 'build', '--no-daemon'] - gradleBuild.dependsOn dependencies - return gradleBuild - } - - private void replaceVersion(File target, String pattern, String replacement) { - def lines = target.readLines() - target.withWriter { writer -> - lines.each { line -> - writer.println(line.replaceAll(pattern, replacement)) - } - } - } - - private Task createVerifyIncludes(Project project, File buildDir) { - Task verifyIncludesTask = project.tasks.create("${name}VerifyIncludes") - verifyIncludesTask.description = "Verifies the includes in the ${name} sample" - verifyIncludesTask.doLast { - Map unprocessedIncludes = [:] - buildDir.eachFileRecurse { file -> - if (file.name.endsWith('.html')) { - file.eachLine { line -> - if (line.contains(new File(this.workingDir).absolutePath)) { - unprocessedIncludes.get(file, []).add(line) - } - } - } - } - if (unprocessedIncludes) { - StringWriter message = new StringWriter() - PrintWriter writer = new PrintWriter(message) - writer.println 'Found unprocessed includes:' - unprocessedIncludes.each { file, lines -> - writer.println " ${file}:" - lines.each { line -> writer.println " ${line}" } - } - throw new GradleException(message.toString()) - } - } - return verifyIncludesTask - } -} diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesExtension.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesExtension.groovy deleted file mode 100644 index f606e2d6..00000000 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesExtension.groovy +++ /dev/null @@ -1,49 +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.build.samples - -import org.gradle.api.Project -import org.gradle.api.Task - -class SamplesExtension { - - Project Project - - Task buildSamplesTask - - Object[] dependencies = [] - - SamplesExtension(Project project, Task buildSamplesTask) { - this.project = project - this.buildSamplesTask = buildSamplesTask - } - - void dependOn(Object... paths) { - this.dependencies += paths - } - - def methodMissing(String name, args) { - SampleBuildConfigurer configurer = new SampleBuildConfigurer(name) - Closure closure = args[0] - closure.delegate = configurer - closure.resolveStrategy = Closure.DELEGATE_FIRST - closure.call() - Task task = configurer.createTask(this.project, this.dependencies) - this.buildSamplesTask.dependsOn task - } - -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesPlugin.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesPlugin.groovy deleted file mode 100644 index f2f65317..00000000 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/samples/SamplesPlugin.groovy +++ /dev/null @@ -1,31 +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.build.samples - -import org.gradle.api.Plugin -import org.gradle.api.Project - -class SamplesPlugin implements Plugin { - - void apply(Project project) { - def buildSamplesTask = project.tasks.create('buildSamples') - buildSamplesTask.description = 'Builds the configured samples' - buildSamplesTask.group = 'Build' - project.extensions.create('samples', SamplesExtension, project, buildSamplesTask) - } - -} \ No newline at end of file diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties deleted file mode 100644 index 062b9c79..00000000 --- a/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class: org.springframework.restdocs.build.samples.SamplesPlugin \ No newline at end of file diff --git a/ci/scripts/build-pr-project.sh b/ci/scripts/build-pr-project.sh index 76e5d84c..9a80167a 100755 --- a/ci/scripts/build-pr-project.sh +++ b/ci/scripts/build-pr-project.sh @@ -4,5 +4,5 @@ set -e source $(dirname $0)/common.sh pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 --continue build buildSamples +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 --continue build popd > /dev/null diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh index d0ae3118..3844d1a3 100755 --- a/ci/scripts/build-project.sh +++ b/ci/scripts/build-project.sh @@ -5,5 +5,5 @@ 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 buildSamples publishAllPublicationsToDeploymentRepository +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository popd > /dev/null diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 15d3a520..acace425 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -1129,7 +1129,7 @@ include::{examples-dir}/com/example/Constraints.java[tags=constraints] <2> Get the descriptions of the `name` property's constraints. This list contains two descriptions: one for the `NotNull` constraint and one for the `Size` constraint. -The {samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS sample shows this functionality in action. +The {samples}/restful-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS sample shows this functionality in action. @@ -1191,7 +1191,7 @@ Validator: * `URL` To override the default descriptions or to provide a new description, you can create a resource bundle with a base name of `org.springframework.restdocs.constraints.ConstraintDescriptions`. -The Spring HATEOAS-based sample contains {samples}/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties[an example of such a resource bundle]. +The Spring HATEOAS-based sample contains {samples}/restful-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties[an example of such a resource bundle]. Each key in the resource bundle is the fully-qualified name of a constraint plus a `.description`. For example, the key for the standard `@NotNull` constraint is `jakarta.validation.constraints.NotNull.description`. @@ -1209,7 +1209,7 @@ To take complete control, you can create `ConstraintDescriptions` with a custom Once you have a constraint's descriptions, you are free to use them however you like in the generated snippets. For example, you may want to include the constraint descriptions as part of a field's description. Alternatively, you could include the constraints as <> in the request fields snippet. -The {samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS-based sample illustrates the latter approach. +The {samples}/restful-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS-based sample illustrates the latter approach. diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 430d71d5..998f7ec0 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -8,63 +8,7 @@ This section describes how to get started with Spring REST Docs. [[getting-started-sample-applications]] === Sample Applications -If you want to jump straight in, a number of sample applications are available: - -[cols="3,2,10"] -.MockMvc -|=== -| Sample | Build system | Description - -| {samples}/rest-notes-spring-data-rest[Spring Data REST] -| Maven -| Demonstrates the creation of a getting started guide and an API guide for a service implemented by using https://projects.spring.io/spring-data-rest/[Spring Data REST]. - -| {samples}/rest-notes-spring-hateoas[Spring HATEOAS] -| Gradle -| Demonstrates the creation of a getting started guide and an API guide for a service implemented by using https://projects.spring.io/spring-hateoas/[Spring HATEOAS]. -|=== - -[cols="3,2,10"] -.WebTestClient -|=== -| Sample | Build system | Description - -| {samples}/web-test-client[WebTestClient] -| Gradle -| Demonstrates the use of Spring REST docs with Spring WebFlux's WebTestClient. -|=== - - -[cols="3,2,10"] -.REST Assured -|=== -| Sample | Build system | Description - -| {samples}/rest-assured[REST Assured] -| Gradle -| Demonstrates the use of Spring REST Docs with https://rest-assured.io/[REST Assured]. - -|=== - - -[cols="3,2,10"] -.Advanced -|=== -| Sample | Build system | Description - -| {samples}/rest-notes-slate[Slate] -| Gradle -| Demonstrates the use of Spring REST Docs with Markdown and - https://github.com/tripit/slate[Slate]. - -| {samples}/testng[TestNG] -| Gradle -| Demonstrates the use of Spring REST Docs with http://testng.org[TestNG]. - -| {samples}/junit5[JUnit 5] -| Gradle -| Demonstrates the use of Spring REST Docs with https://junit.org/junit5/[JUnit 5]. -|=== +If you want to jump straight in, a number of https://github.com/spring-projects/spring-restdocs-samples[sample applications are available]. @@ -82,7 +26,7 @@ Additionally, the `spring-restdocs-restassured` module requires REST Assured 5.1 === Build configuration The first step in using Spring REST Docs is to configure your project's build. -The {samples}/rest-notes-spring-hateoas[Spring HATEOAS] and {samples}/rest-notes-spring-data-rest[Spring Data REST] samples contain a `build.gradle` and `pom.xml`, respectively, that you may wish to use as a reference. +The {samples}/restful-notes-spring-hateoas[Spring HATEOAS] and {samples}/restful-notes-spring-data-rest[Spring Data REST] samples contain a `build.gradle` and `pom.xml`, respectively, that you may wish to use as a reference. The key parts of the configuration are described in the following listings: [source,xml,indent=0,subs="verbatim,attributes",role="primary"] diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 153ef44c..7da64ba4 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -10,7 +10,7 @@ Andy Wilkinson; Jay Bryant :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs :source: {github}/tree/{branch-or-tag} -:samples: {source}/samples +:samples: https://github.com/spring-projects/spring-restdocs-samples/tree/main :templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle :spring-framework-docs: https://docs.spring.io/spring-framework/docs/5.0.x/spring-framework-reference diff --git a/samples/junit5/build.gradle b/samples/junit5/build.gradle deleted file mode 100644 index 9713f32b..00000000 --- a/samples/junit5/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -plugins { - id "eclipse" - id "java" - id "org.asciidoctor.jvm.convert" version "3.3.2" -} - -repositories { - mavenLocal { - content { - includeGroup("org.springframework.restdocs") - } - } - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 17 -targetCompatibility = 17 - -ext { - restdocsVersion = '3.0.0-SNAPSHOT' - snippetsDir = file('build/generated-snippets') -} - -configurations { - asciidoctorExtensions -} - -dependencies { - asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - - implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") - implementation 'org.springframework:spring-webmvc' - - testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' - - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.0' -} - -test { - outputs.dir snippetsDir - useJUnitPlatform() -} - -asciidoctor { - configurations "asciidoctorExtensions" - inputs.dir snippetsDir - dependsOn test -} - -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} diff --git a/samples/junit5/gradle/wrapper/gradle-wrapper.jar b/samples/junit5/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

    K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/samples/junit5/gradle/wrapper/gradle-wrapper.properties b/samples/junit5/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ffed3a25..00000000 --- a/samples/junit5/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/junit5/gradlew b/samples/junit5/gradlew deleted file mode 100755 index 4f906e0c..00000000 --- a/samples/junit5/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/samples/junit5/gradlew.bat b/samples/junit5/gradlew.bat deleted file mode 100644 index 107acd32..00000000 --- a/samples/junit5/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -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. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/junit5/settings.gradle b/samples/junit5/settings.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/samples/junit5/src/docs/asciidoc/index.adoc b/samples/junit5/src/docs/asciidoc/index.adoc deleted file mode 100644 index 68e690a9..00000000 --- a/samples/junit5/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,22 +0,0 @@ -= Spring REST Docs JUnit 5 Sample -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs - -Sample application demonstrating how to use Spring REST Docs with JUnit 5. - -`SampleJUnit5ApplicationTests` makes a call to a very simple service and produces three -documentation snippets. - -One showing how to make a request using cURL: - -include::{snippets}/sample/curl-request.adoc[] - -One showing the HTTP request: - -include::{snippets}/sample/http-request.adoc[] - -And one showing the HTTP response: - -include::{snippets}/sample/http-response.adoc[] diff --git a/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java b/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java deleted file mode 100644 index 3f2d403b..00000000 --- a/samples/junit5/src/main/java/com/example/junit5/SampleJUnit5Application.java +++ /dev/null @@ -1,36 +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 com.example.junit5; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Configuration -public class SampleJUnit5Application { - - @RestController - private static class SampleController { - - @RequestMapping("/") - public String index() { - return "Hello, World"; - } - - } - -} diff --git a/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java b/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java deleted file mode 100644 index 99ddd81b..00000000 --- a/samples/junit5/src/test/java/com/example/junit5/SampleJUnit5ApplicationTests.java +++ /dev/null @@ -1,60 +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 com.example.junit5; - -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -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.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -@WebAppConfiguration -@ContextConfiguration(classes = SampleJUnit5Application.class) -@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) -public class SampleJUnit5ApplicationTests { - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @BeforeEach - public void setUp(RestDocumentationContextProvider restDocumentation) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(context) - .apply(documentationConfiguration(restDocumentation)).build(); - } - - @Test - public void sample() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("sample")); - } - -} diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle deleted file mode 100644 index ab8aba31..00000000 --- a/samples/rest-assured/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id "java" - id "org.asciidoctor.jvm.convert" version "3.3.2" -} - -repositories { - mavenLocal { - content { - includeGroup("org.springframework.restdocs") - } - } - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 17 -targetCompatibility = 17 - -ext { - restdocsVersion = '3.0.0-SNAPSHOT' - snippetsDir = file('build/generated-snippets') -} - -configurations { - asciidoctorExtensions -} - -dependencies { - asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" - - implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") - implementation 'io.projectreactor.netty:reactor-netty-http:1.0.15' - implementation 'org.springframework:spring-context' - implementation 'org.springframework:spring-webflux' - - - testImplementation 'io.rest-assured:rest-assured:5.1.1' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0' - testImplementation "org.springframework.restdocs:spring-restdocs-restassured:$restdocsVersion" - testImplementation 'org.springframework:spring-test' - testImplementation('org.junit.vintage:junit-vintage-engine') { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } - - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.0' -} - -test { - outputs.dir snippetsDir - useJUnitPlatform() -} - -asciidoctor { - configurations "asciidoctorExtensions" - inputs.dir snippetsDir - dependsOn test -} - -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 zcma&O1CT9Y(k9%tZQHhO+qUh#ZQHhO+qmuS+qP|E@9xZO?0h@l{(r>DQ>P;GjjD{w zH}lENr;dU&FbEU?00aa80D$0M0RRB{U*7-#kbjS|qAG&4l5%47zyJ#WrfA#1$1Ctx zf&Z_d{GW=lf^w2#qRJ|CvSJUi(^E3iv~=^Z(zH}F)3Z%V3`@+rNB7gTVU{Bb~90p|f+0(v;nz01EG7yDMX9@S~__vVgv%rS$+?IH+oZ03D5zYrv|^ zC1J)SruYHmCki$jLBlTaE5&dFG9-kq3!^i>^UQL`%gn6)jz54$WDmeYdsBE9;PqZ_ zoGd=P4+|(-u4U1dbAVQrFWoNgNd;0nrghPFbQrJctO>nwDdI`Q^i0XJDUYm|T|RWc zZ3^Qgo_Qk$%Fvjj-G}1NB#ZJqIkh;kX%V{THPqOyiq)d)0+(r9o(qKlSp*hmK#iIY zA^)Vr$-Hz<#SF=0@tL@;dCQsm`V9s1vYNq}K1B)!XSK?=I1)tX+bUV52$YQu*0%fnWEukW>mxkz+%3-S!oguE8u#MGzST8_Dy^#U?fA@S#K$S@9msUiX!gd_ow>08w5)nX{-KxqMOo7d?k2&?Vf z&diGDtZr(0cwPe9z9FAUSD9KC)7(n^lMWuayCfxzy8EZsns%OEblHFSzP=cL6}?J| z0U$H!4S_TVjj<`6dy^2j`V`)mC;cB%* z8{>_%E1^FH!*{>4a7*C1v>~1*@TMcLK{7nEQ!_igZC}ikJ$*<$yHy>7)oy79A~#xE zWavoJOIOC$5b6*q*F_qN1>2#MY)AXVyr$6x4b=$x^*aqF*L?vmj>Mgv+|ITnw_BoW zO?jwHvNy^prH{9$rrik1#fhyU^MpFqF2fYEt(;4`Q&XWOGDH8k6M=%@fics4ajI;st# zCU^r1CK&|jzUhRMv;+W~6N;u<;#DI6cCw-otsc@IsN3MoSD^O`eNflIoR~l4*&-%RBYk@gb^|-JXs&~KuSEmMxB}xSb z@K76cXD=Y|=I&SNC2E+>Zg?R6E%DGCH5J1nU!A|@eX9oS(WPaMm==k2s_ueCqdZw| z&hqHp)47`c{BgwgvY2{xz%OIkY1xDwkw!<0veB#yF4ZKJyabhyyVS`gZepcFIk%e2 zTcrmt2@-8`7i-@5Nz>oQWFuMC_KlroCl(PLSodswHqJ3fn<;gxg9=}~3x_L3P`9Sn zChIf}8vCHvTriz~T2~FamRi?rh?>3bX1j}%bLH+uFX+p&+^aXbOK7clZxdU~6Uxgy z8R=obwO4dL%pmVo*Ktf=lH6hnlz_5k3cG;m8lgaPp~?eD!Yn2kf)tU6PF{kLyn|oI@eQ`F z3IF7~Blqg8-uwUuWZScRKn%c2_}dXB6Dx_&xR*n9M9LXasJhtZdr$vBY!rP{c@=)& z#!?L$2UrkvClwQO>U*fSMs67oSj2mxiJ$t;E|>q%Kh_GzzWWO&3;ufU%2z%ucBU8H z3WIwr$n)cfCXR&>tyB7BcSInK>=ByZA%;cVEJhcg<#6N{aZC4>K41XF>ZgjG`z_u& zGY?;Ad?-sgiOnI`oppF1o1Gurqbi*;#x2>+SSV6|1^G@ooVy@fg?wyf@0Y!UZ4!}nGuLeC^l)6pwkh|oRY`s1Pm$>zZ3u-83T|9 zGaKJIV3_x+u1>cRibsaJpJqhcm%?0-L;2 zitBrdRxNmb0OO2J%Y&Ym(6*`_P3&&5Bw157{o7LFguvxC$4&zTy#U=W*l&(Q2MNO} zfaUwYm{XtILD$3864IA_nn34oVa_g^FRuHL5wdUd)+W-p-iWCKe8m_cMHk+=? zeKX)M?Dt(|{r5t7IenkAXo%&EXIb-i^w+0CX0D=xApC=|Xy(`xy+QG^UyFe z+#J6h_&T5i#sV)hj3D4WN%z;2+jJcZxcI3*CHXGmOF3^)JD5j&wfX)e?-|V0GPuA+ zQFot%aEqGNJJHn$!_}#PaAvQ^{3-Ye7b}rWwrUmX53(|~i0v{}G_sI9uDch_brX&6 zWl5Ndj-AYg(W9CGfQf<6!YmY>Ey)+uYd_JNXH=>|`OH-CDCmcH(0%iD_aLlNHKH z7bcW-^5+QV$jK?R*)wZ>r9t}loM@XN&M-Pw=F#xn(;u3!(3SXXY^@=aoj70;_=QE9 zGghsG3ekq#N||u{4We_25U=y#T*S{4I{++Ku)> zQ!DZW;pVcn>b;&g2;YE#+V`v*Bl&Y-i@X6D*OpNA{G@JAXho&aOk(_j^weW{#3X5Y z%$q_wpb07EYPdmyH(1^09i$ca{O<}7) zRWncXdSPgBE%BM#by!E>tdnc$8RwUJg1*x($6$}ae$e9Knj8gvVZe#bLi!<+&BkFj zg@nOpDneyc+hU9P-;jmOSMN|*H#>^Ez#?;%C3hg_65leSUm;iz)UkW)jX#p)e&S&M z1|a?wDzV5NVnlhRBCd_;F87wp>6c<&nkgvC+!@KGiIqWY4l}=&1w7|r6{oBN8xyzh zG$b#2=RJp_iq6)#t5%yLkKx(0@D=C3w+oiXtSuaQ%I1WIb-eiE$d~!)b@|4XLy!CZ z9p=t=%3ad@Ep+<9003D2KZ5VyP~_n$=;~r&YUg5UZ0KVD&tR1DHy9x)qWtKJp#Kq# zP*8p#W(8JJ_*h_3W}FlvRam?<4Z+-H77^$Lvi+#vmhL9J zJ<1SV45xi;SrO2f=-OB(7#iNA5)x1uNC-yNxUw|!00vcW2PufRm>e~toH;M0Q85MQLWd?3O{i8H+5VkR@l9Dg-ma ze2fZ%>G(u5(k9EHj2L6!;(KZ8%8|*-1V|B#EagbF(rc+5iL_5;Eu)L4Z-V;0HfK4d z*{utLse_rvHZeQ>V5H=f78M3Ntg1BPxFCVD{HbNA6?9*^YIq;B-DJd{Ca2L#)qWP? zvX^NhFmX?CTWw&Ns}lgs;r3i+Bq@y}Ul+U%pzOS0Fcv9~aB(0!>GT0)NO?p=25LjN z2bh>6RhgqD7bQj#k-KOm@JLgMa6>%-ok1WpOe)FS^XOU{c?d5shG(lIn3GiVBxmg`u%-j=)^v&pX1JecJics3&jvPI)mDut52? z3jEA)DM%}BYbxxKrizVYwq?(P&19EXlwD9^-6J+4!}9{ywR9Gk42jjAURAF&EO|~N z)?s>$Da@ikI4|^z0e{r`J8zIs>SpM~Vn^{3fArRu;?+43>lD+^XtUcY1HidJwnR6+ z!;oG2=B6Z_=M%*{z-RaHc(n|1RTKQdNjjV!Pn9lFt^4w|AeN06*j}ZyhqZ^!-=cyGP_ShV1rGxkx8t zB;8`h!S{LD%ot``700d0@Grql(DTt4Awgmi+Yr0@#jbe=2#UkK%rv=OLqF)9D7D1j z!~McAwMYkeaL$~kI~90)5vBhBzWYc3Cj1WI0RS`z000R8-@ET0dA~*r(gSiCJmQMN&4%1D zyVNf0?}sBH8zNbBLn>~(W{d3%@kL_eQ6jEcR{l>C|JK z(R-fA!z|TTRG40|zv}7E@PqCAXP3n`;%|SCQ|ZS%ym$I{`}t3KPL&^l5`3>yah4*6 zifO#{VNz3)?ZL$be;NEaAk9b#{tV?V7 zP|wf5YA*1;s<)9A4~l3BHzG&HH`1xNr#%){4xZ!jq%o=7nN*wMuXlFV{HaiQLJ`5G zBhDi#D(m`Q1pLh@Tq+L;OwuC52RdW7b8}~60WCOK5iYMUad9}7aWBuILb({5=z~YF zt?*Jr5NG+WadM{mDL>GyiByCuR)hd zA=HM?J6l1Xv0Dl+LW@w$OTcEoOda^nFCw*Sy^I@$sSuneMl{4ys)|RY#9&NxW4S)9 zq|%83IpslTLoz~&vTo!Ga@?rj_kw{|k{nv+w&Ku?fyk4Ki4I?);M|5Axm)t+BaE)D zm(`AQ#k^DWrjbuXoJf2{Aj^KT zFb1zMSqxq|vceV+Mf-)$oPflsO$@*A0n0Z!R{&(xh8s}=;t(lIy zv$S8x>m;vQNHuRzoaOo?eiWFe{0;$s`Bc+Osz~}Van${u;g(su`3lJ^TEfo~nERfP z)?aFzpDgnLYiERsKPu|0tq4l2wT)Atr6Qb%m-AUn6HnCue*yWICp7TjW$@sO zm5rm4aTcPQ(rfi7a`xP7cKCFrJD}*&_~xgLyr^-bmsL}y;A5P|al8J3WUoBSjqu%v zxC;mK!g(7r6RRJ852Z~feoC&sD3(6}^5-uLK8o)9{8L_%%rItZK9C){UxB|;G>JbP zsRRtS4-3B*5c+K2kvmgZK8472%l>3cntWUOVHxB|{Ay~aOg5RN;{PJgeVD*H%ac+y!h#wi%o2bF2Ca8IyMyH{>4#{E_8u^@+l-+n=V}Sq?$O z{091@v%Bd*3pk0^2UtiF9Z+(a@wy6 zUdw8J*ze$K#=$48IBi1U%;hmhO>lu!uU;+RS}p&6@rQila7WftH->*A4=5W|Fmtze z)7E}jh@cbmr9iup^i%*(uF%LG&!+Fyl@LFA-}Ca#bxRfDJAiR2dt6644TaYw1Ma79 zt8&DYj31j^5WPNf5P&{)J?WlCe@<3u^78wnd(Ja4^a>{^Tw}W>|Cjt^If|7l^l)^Q zbz|7~CF(k_9~n|h;ysZ+jHzkXf(*O*@5m zLzUmbHp=x!Q|!9NVXyipZ3)^GuIG$k;D)EK!a5=8MFLI_lpf`HPKl=-Ww%z8H_0$j ztJ||IfFG1lE9nmQ0+jPQy zCBdKkjArH@K7jVcMNz);Q(Q^R{d5G?-kk;Uu_IXSyWB)~KGIizZL(^&qF;|1PI7!E zTP`%l)gpX|OFn&)M%txpQ2F!hdA~hX1Cm5)IrdljqzRg!f{mN%G~H1&oqe`5eJCIF zHdD7O;AX-{XEV(a`gBFJ9ews#CVS2y!&>Cm_dm3C8*n3MA*e67(WC?uP@8TXuMroq z{#w$%z@CBIkRM7?}Xib+>hRjy?%G!fiw8! z8(gB+8J~KOU}yO7UGm&1g_MDJ$IXS!`+*b*QW2x)9>K~Y*E&bYMnjl6h!{17_8d!%&9D`a7r&LKZjC<&XOvTRaKJ1 zUY@hl5^R&kZl3lU3njk`3dPzxj$2foOL26r(9zsVF3n_F#v)s5vv3@dgs|lP#eylq62{<-vczqP!RpVBTgI>@O6&sU>W|do17+#OzQ7o5A$ICH z?GqwqnK^n2%LR;$^oZM;)+>$X3s2n}2jZ7CdWIW0lnGK-b#EG01)P@aU`pg}th&J-TrU`tIpb5t((0eu|!u zQz+3ZiOQ^?RxxK4;zs=l8q!-n7X{@jSwK(iqNFiRColuEOg}!7cyZi`iBX4g1pNBj zAPzL?P^Ljhn;1$r8?bc=#n|Ed7wB&oHcw()&*k#SS#h}jO?ZB246EGItsz*;^&tzp zu^YJ0=lwsi`eP_pU8}6JA7MS;9pfD;DsSsLo~ogzMNP70@@;Fm8f0^;>$Z>~}GWRw!W5J3tNX*^2+1f3hz{~rIzJo z6W%J(H!g-eI_J1>0juX$X4Cl6i+3wbc~k146UIX&G22}WE>0ga#WLsn9tY(&29zBvH1$`iWtTe zG2jYl@P!P)eb<5DsR72BdI7-zP&cZNI{7q3e@?N8IKc4DE#UVr->|-ryuJXk^u^>4 z$3wE~=q390;XuOQP~TNoDR?#|NSPJ%sTMInA6*rJ%go|=YjGe!B>z6u$IhgQSwoV* zjy3F2#I>uK{42{&IqP59)Y(1*Z>>#W8rCf4_eVsH)`v!P#^;BgzKDR`ARGEZzkNX+ zJUQu=*-ol=Xqqt5=`=pA@BIn@6a9G8C{c&`i^(i+BxQO9?YZ3iu%$$da&Kb?2kCCo zo7t$UpSFWqmydXf@l3bVJ=%K?SSw)|?srhJ-1ZdFu*5QhL$~-IQS!K1s@XzAtv6*Y zl8@(5BlWYLt1yAWy?rMD&bwze8bC3-GfNH=p zynNFCdxyX?K&G(ZZ)afguQ2|r;XoV^=^(;Cku#qYn4Lus`UeKt6rAlFo_rU`|Rq z&G?~iWMBio<78of-2X(ZYHx~=U0Vz4btyXkctMKdc9UM!vYr~B-(>)(Hc|D zMzkN4!PBg%tZoh+=Gba!0++d193gbMk2&krfDgcbx0jI92cq?FFESVg0D$>F+bil} zY~$)|>1HZsX=5sAZ2WgPB5P=8X#TI+NQ(M~GqyVB53c6IdX=k>Wu@A0Svf5#?uHaF zsYn|koIi3$(%GZ2+G+7Fv^lHTb#5b8sAHSTnL^qWZLM<(1|9|QFw9pnRU{svj}_Al zL)b9>fN{QiA($8peNEJyy`(a{&uh-T4_kdZFIVsKKVM(?05}76EEz?#W za^fiZOAd14IJ4zLX-n7Lq0qlQ^lW8Cvz4UKkV9~P}>sq0?xD3vg+$4vLm~C(+ zM{-3Z#qnZ09bJ>}j?6ry^h+@PfaD7*jZxBEY4)UG&daWb??6)TP+|3#Z&?GL?1i+280CFsE|vIXQbm| zM}Pk!U`U5NsNbyKzkrul-DzwB{X?n3E6?TUHr{M&+R*2%yOiXdW-_2Yd6?38M9Vy^ z*lE%gA{wwoSR~vN0=no}tP2Ul5Gk5M(Xq`$nw#ndFk`tcpd5A=Idue`XZ!FS>Q zG^0w#>P4pPG+*NC9gLP4x2m=cKP}YuS!l^?sHSFftZy{4CoQrb_ z^20(NnG`wAhMI=eq)SsIE~&Gp9Ne0nD4%Xiu|0Fj1UFk?6avDqjdXz{O1nKao*46y zT8~iA%Exu=G#{x=KD;_C&M+Zx4+n`sHT>^>=-1YM;H<72k>$py1?F3#T1*ef9mLZw z5naLQr?n7K;2l+{_uIw*_1nsTn~I|kkCgrn;|G~##hM;9l7Jy$yJfmk+&}W@JeKcF zx@@Woiz8qdi|D%aH3XTx5*wDlbs?dC1_nrFpm^QbG@wM=i2?Zg;$VK!c^Dp8<}BTI zyRhAq@#%2pGV49*Y5_mV4+OICP|%I(dQ7x=6Ob}>EjnB_-_18*xrY?b%-yEDT(wrO z9RY2QT0`_OpGfMObKHV;QLVnrK%mc?$WAdIT`kJQT^n%GuzE7|9@k3ci5fYOh(287 zuIbg!GB3xLg$YN=n)^pHGB0jH+_iIiC=nUcD;G6LuJsjn2VI1cyZx=a?ShCsF==QK z;q~*m&}L<-cb+mDDXzvvrRsybcgQ;Vg21P(uLv5I+eGc7o7tc6`;OA9{soHFOz zT~2?>Ts}gprIX$wRBb4yE>ot<8+*Bv`qbSDv*VtRi|cyWS>)Fjs>fkNOH-+PX&4(~ z&)T8Zam2L6puQl?;5zg9h<}k4#|yH9czHw;1jw-pwBM*O2hUR6yvHATrI%^mvs9q_ z&ccT0>f#eDG<^WG^q@oVqlJrhxH)dcq2cty@l3~|5#UDdExyXUmLQ}f4#;6fI{f^t zDCsgIJ~0`af%YR%Ma5VQq-p21k`vaBu6WE?66+5=XUd%Ay%D$irN>5LhluRWt7 zov-=f>QbMk*G##&DTQyou$s7UqjjW@k6=!I@!k+S{pP8R(2=e@io;N8E`EOB;OGoI zw6Q+{X1_I{OO0HPpBz!X!@`5YQ2)t{+!?M_iH25X(d~-Zx~cXnS9z>u?+If|iNJbx zyFU2d1!ITX64D|lE0Z{dLRqL1Ajj=CCMfC4lD3&mYR_R_VZ>_7_~|<^o*%_&jevU+ zQ4|qzci=0}Jydw|LXLCrOl1_P6Xf@c0$ieK2^7@A9UbF{@V_0p%lqW|L?5k>bVM8|p5v&2g;~r>B8uo<4N+`B zH{J)h;SYiIVx@#jI&p-v3dwL5QNV1oxPr8J%ooezTnLW>i*3Isb49%5i!&ac_dEXv zvXmVUck^QHmyrF8>CGXijC_R-y(Qr{3Zt~EmW)-nC!tiH`wlw5D*W7Pip;T?&j%kX z6DkZX4&}iw>hE(boLyjOoupf6JpvBG8}jIh!!VhnD0>}KSMMo{1#uU6kiFcA04~|7 zVO8eI&x1`g4CZ<2cYUI(n#wz2MtVFHx47yE5eL~8bot~>EHbevSt}LLMQX?odD{Ux zJMnam{d)W4da{l7&y-JrgiU~qY3$~}_F#G7|MxT)e;G{U`In&?`j<5D->}cb{}{T(4DF0BOk-=1195KB-E*o@c?`>y#4=dMtYtSY=&L{!TAjFVcq0y@AH`vH! z$41+u!Ld&}F^COPgL(EE{0X7LY&%D7-(?!kjFF7=qw<;`V{nwWBq<)1QiGJgUc^Vz ztMUlq1bZqKn17|6x6iAHbWc~l1HcmAxr%$Puv!znW)!JiukwIrqQ00|H$Z)OmGG@= zv%A8*4cq}(?qn4rN6o`$Y))(MyXr8R<2S^J+v(wmFmtac!%VOfN?&(8Nr!T@kV`N; z*Q33V3t`^rN&aBiHet)18wy{*wi1=W!B%B-Q6}SCrUl$~Hl{@!95ydml@FK8P=u4s z4e*7gV2s=YxEvskw2Ju!2%{8h01rx-3`NCPc(O zH&J0VH5etNB2KY6k4R@2Wvl^Ck$MoR3=)|SEclT2ccJ!RI9Nuter7u9@;sWf-%um;GfI!=eEIQ2l2p_YWUd{|6EG ze{yO6;lMc>;2tPrsNdi@&1K6(1;|$xe8vLgiouj%QD%gYk`4p{Ktv9|j+!OF-P?@p z;}SV|oIK)iwlBs+`ROXkhd&NK zzo__r!B>tOXpBJMDcv!Mq54P+n4(@dijL^EpO1wdg~q+!DT3lB<>9AANSe!T1XgC=J^)IP0XEZ()_vpu!!3HQyJhwh?r`Ae%Yr~b% zO*NY9t9#qWa@GCPYOF9aron7thfWT`eujS4`t2uG6)~JRTI;f(ZuoRQwjZjp5Pg34 z)rp$)Kr?R+KdJ;IO;pM{$6|2y=k_siqvp%)2||cHTe|b5Ht8&A{wazGNca zX$Ol?H)E_R@SDi~4{d-|8nGFhZPW;Cts1;08TwUvLLv&_2$O6Vt=M)X;g%HUr$&06 zISZb(6)Q3%?;3r~*3~USIg=HcJhFtHhIV(siOwV&QkQe#J%H9&E21!C*d@ln3E@J* zVqRO^<)V^ky-R|%{(9`l-(JXq9J)1r$`uQ8a}$vr9E^nNiI*thK8=&UZ0dsFN_eSl z(q~lnD?EymWLsNa3|1{CRPW60>DSkY9YQ;$4o3W7Ms&@&lv9eH!tk~N&dhqX&>K@} zi1g~GqglxkZ5pEFkllJ)Ta1I^c&Bt6#r(QLQ02yHTaJB~- zCcE=5tmi`UA>@P=1LBfBiqk)HB4t8D?02;9eXj~kVPwv?m{5&!&TFYhu>3=_ zsGmYZ^mo*-j69-42y&Jj0cBLLEulNRZ9vXE)8~mt9C#;tZs;=#M=1*hebkS;7(aGf zcs7zH(I8Eui9UU4L--))yy`&d&$In&VA2?DAEss4LAPCLd>-$i?lpXvn!gu^JJ$(DoUlc6wE98VLZ*z`QGQov5l4Fm_h?V-;mHLYDVOwKz7>e4+%AzeO>P6v}ndPW| zM>m#6Tnp7K?0mbK=>gV}=@k*0Mr_PVAgGMu$j+pWxzq4MAa&jpCDU&-5eH27Iz>m^ zax1?*HhG%pJ((tkR(V(O(L%7v7L%!_X->IjS3H5kuXQT2!ow(;%FDE>16&3r){!ex zhf==oJ!}YU89C9@mfDq!P3S4yx$aGB?rbtVH?sHpg?J5C->!_FHM%Hl3#D4eplxzQ zRA+<@LD%LKSkTk2NyWCg7u=$%F#;SIL44~S_OGR}JqX}X+=bc@swpiClB`Zbz|f!4 z7Ysah7OkR8liXfI`}IIwtEoL}(URrGe;IM8%{>b1SsqXh)~w}P>yiFRaE>}rEnNkT z!HXZUtxUp1NmFm)Dm@-{FI^aRQqpSkz}ZSyKR%Y}YHNzBk)ZIp} zMtS=aMvkgWKm9&oTcU0?S|L~CDqA+sHpOxwnswF-fEG)cXCzUR?ps@tZa$=O)=L+5 zf%m58cq8g_o}3?Bhh+c!w4(7AjxwQ3>WnVi<{{38g7yFboo>q|+7qs<$8CPXUFAN< zG&}BHbbyQ5n|qqSr?U~GY{@GJ{(Jny{bMaOG{|IkUj7tj^9pa9|FB_<+KHLxSxR;@ zHpS$4V)PP+tx}22fWx(Ku9y+}Ap;VZqD0AZW4gCDTPCG=zgJmF{|x;(rvdM|2|9a}cex6xrMkERnkE;}jvU-kmzd%_J50$M`lIPCKf+^*zL=@LW`1SaEc%=m zQ+lT06Gw+wVwvQ9fZ~#qd430v2HndFsBa9WjD0P}K(rZYdAt^5WQIvb%D^Q|pkVE^ zte$&#~zmULFACGfS#g=2OLOnIf2Of-k!(BIHjs77nr!5Q1*I9 z1%?=~#Oss!rV~?-6Gm~BWJiA4mJ5TY&iPm_$)H1_rTltuU1F3I(qTQ^U$S>%$l z)Wx1}R?ij0idp@8w-p!Oz{&*W;v*IA;JFHA9%nUvVDy7Q8woheC#|8QuDZb-L_5@R zOqHwrh|mVL9b=+$nJxM`3eE{O$sCt$UK^2@L$R(r^-_+z?lOo+me-VW=Zw z-Bn>$4ovfWd%SPY`ab-u9{INc*k2h+yH%toDHIyqQ zO68=u`N}RIIs7lsn1D){)~%>ByF<>i@qFb<-axvu(Z+6t7v<^z&gm9McRB~BIaDn$ z#xSGT!rzgad8o>~kyj#h1?7g96tOcCJniQ+*#=b7wPio>|6a1Z?_(TS{)KrPe}(8j z!#&A=k(&Pj^F;r)CI=Z{LVu>uj!_W1q4b`N1}E(i%;BWjbEcnD=mv$FL$l?zS6bW!{$7j1GR5ocn94P2u{ z70tAAcpqtQo<@cXw~@i-@6B23;317|l~S>CB?hR5qJ%J3EFgyBdJd^fHZu7AzHF(BQ!tyAz^L0`X z23S4Fe{2X$W0$zu9gm%rg~A>ijaE#GlYlrF9$ds^QtaszE#4M(OLVP2O-;XdT(XIC zatwzF*)1c+t~c{L=fMG8Z=k5lv>U0;C{caN1NItnuSMp)6G3mbahu>E#sj&oy94KC zpH}8oEw{G@N3pvHhp{^-YaZeH;K+T_1AUv;IKD<=mv^&Ueegrb!yf`4VlRl$M?wsl zZyFol(2|_QM`e_2lYSABpKR{{NlxlDSYQNkS;J66aT#MSiTx~;tUmvs-b*CrR4w=f z8+0;*th6kfZ3|5!Icx3RV11sp=?`0Jy3Fs0N4GZQMN=8HmT6%x9@{Dza)k}UwL6JT zHRDh;%!XwXr6yuuy`4;Xsn0zlR$k%r%9abS1;_v?`HX_hI|+EibVnlyE@3aL5vhQq zlIG?tN^w@0(v9M*&L+{_+RQZw=o|&BRPGB>e5=ys7H`nc8nx)|-g;s7mRc7hg{GJC zAe^vCIJhajmm7C6g! zL&!WAQ~5d_5)00?w_*|*H>3$loHrvFbitw#WvLB!JASO?#5Ig5$Ys10n>e4|3d;tS zELJ0|R4n3Az(Fl3-r^QiV_C;)lQ1_CW{5bKS15U|E9?ZgLec@%kXr84>5jV2a5v=w z?pB1GPdxD$IQL4)G||B_lI+A=08MUFFR4MxfGOu07vfIm+j=z9tp~5i_6jb`tR>qV z$#`=BQ*jpCjm$F0+F)L%xRlnS%#&gro6PiRfu^l!EVan|r3y}AHJQOORGx4~ z&<)3=K-tx518DZyp%|!EqpU!+X3Et7n2AaC5(AtrkW>_57i}$eqs$rupubg0a1+WO zGHZKLN2L0D;ab%{_S1Plm|hx8R?O14*w*f&2&bB050n!R2by zw!@XOQx$SqZ5I<(Qu$V6g>o#A!JVwErWv#(Pjx=KeS0@hxr4?13zj#oWwPS(7Ro|v z>Mp@Kmxo79q|}!5qtX2-O@U&&@6s~!I&)1WQIl?lTnh6UdKT_1R640S4~f=_xoN3- zI+O)$R@RjV$F=>Ti7BlnG1-cFKCC(t|Qjm{SalS~V-tX#+2ekRhwmN zZr`8{QF6y~Z!D|{=1*2D-JUa<(1Z=;!Ei!KiRNH?o{p5o3crFF=_pX9O-YyJchr$~ zRC`+G+8kx~fD2k*ZIiiIGR<8r&M@3H?%JVOfE>)})7ScOd&?OjgAGT@WVNSCZ8N(p zuQG~76GE3%(%h1*vUXg$vH{ua0b`sQ4f0*y=u~lgyb^!#CcPJa2mkSEHGLsnO^kb$ zru5_l#nu=Y{rSMWiYx?nO{8I!gH+?wEj~UM?IrG}E|bRIBUM>UlY<`T1EHpRr36vv zBi&dG8oxS|J$!zoaq{+JpJy+O^W(nt*|#g32bd&K^w-t>!Vu9N!k9eA8r!Xc{utY> zg9aZ(D2E0gL#W0MdjwES-7~Wa8iubPrd?8-$C4BP?*wok&O8+ykOx{P=Izx+G~hM8 z*9?BYz!T8~dzcZr#ux8kS7u7r@A#DogBH8km8Ry4slyie^n|GrTbO|cLhpqgMdsjX zJ_LdmM#I&4LqqsOUIXK8gW;V0B(7^$y#h3h>J0k^WJfAMeYek%Y-Dcb_+0zPJez!GM zAmJ1u;*rK=FNM0Nf}Y!!P9c4)HIkMnq^b;JFd!S3?_Qi2G#LIQ)TF|iHl~WKK6JmK zbv7rPE6VkYr_%_BT}CK8h=?%pk@3cz(UrZ{@h40%XgThP*-Oeo`T0eq9 zA8BnWZKzCy5e&&_GEsU4*;_k}(8l_&al5K-V*BFM=O~;MgRkYsOs%9eOY6s6AtE*<7GQAR2ulC3RAJrG_P1iQK5Z~&B z&f8X<>yJV6)oDGIlS$Y*D^Rj(cszTy5c81a5IwBr`BtnC6_e`ArI8CaTX_%rx7;cn zR-0?J_LFg*?(#n~G8cXut(1nVF0Oka$A$1FGcERU<^ggx;p@CZc?3UB41RY+wLS`LWFNSs~YP zuw1@DNN3lTd|jDL7gjBsd9}wIw}4xT2+8dBQzI00m<@?c2L%>}QLfK5%r!a-iII`p zX@`VEUH)uj^$;7jVUYdADQ2k*!1O3WdfgF?OMtUXNpQ1}QINamBTKDuv19^{$`8A1 zeq%q*O0mi@(%sZU>Xdb0Ru96CFqk9-L3pzLVsMQ`Xpa~N6CR{9Rm2)A|CI21L(%GW zh&)Y$BNHa=FD+=mBw3{qTgw)j0b!Eahs!rZnpu)z!!E$*eXE~##yaXz`KE5(nQM`s zD!$vW9XH)iMxu9R>r$VlLk9oIR%HxpUiW=BK@4U)|1WNQ=mz9a z^!KkO=>GaJ!GBXm{KJj^;kh-MkUlEQ%lza`-G&}C5y1>La1sR6hT=d*NeCnuK%_LV zOXt$}iP6(YJKc9j-Fxq~*ItVUqljQ8?oaysB-EYtFQp9oxZ|5m0^Hq(qV!S+hq#g( z?|i*H2MIr^Kxgz+3vIljQ*Feejy6S4v~jKEPTF~Qhq!(ms5>NGtRgO5vfPPc4Z^AM zTj!`5xEreIN)vaNxa|q6qWdg>+T`Ol0Uz)ckXBXEGvPNEL3R8hB3=C5`@=SYgAju1 z!)UBr{2~=~xa{b8>x2@C7weRAEuatC)3pkRhT#pMPTpSbA|tan%U7NGMvzmF?c!V8 z=pEWxbdXbTAGtWTyI?Fml%lEr-^AE}w#l(<7OIw;ctw}imYax&vR4UYNJZK6P7ZOd zP87XfhnUHxCUHhM@b*NbTi#(-8|wcv%3BGNs#zRCVV(W?1Qj6^PPQa<{yaBwZ`+<`w|;rqUY_C z&AeyKwwf*q#OW-F()lir=T^<^wjK65Lif$puuU5+tk$;e_EJ;Lu+pH>=-8=PDhkBg z8cWt%@$Sc#C6F$Vd+0507;{OOyT7Hs%nKS88q-W!$f~9*WGBpHGgNp}=C*7!RiZ5s zn1L_DbKF@B8kwhDiLKRB@lsXVVLK|ph=w%_`#owlf@s@V(pa`GY$8h%;-#h@TsO|Y8V=n@*!Rog7<7Cid%apR|x zOjhHCyfbIt%+*PCveTEcuiDi%Wx;O;+K=W?OFUV%)%~6;gl?<0%)?snDDqIvkHF{ zyI02)+lI9ov42^hL>ZRrh*HhjF9B$A@=H94iaBESBF=eC_KT$8A@uB^6$~o?3Wm5t1OIaqF^~><2?4e3c&)@wKn9bD? zoeCs;H>b8DL^F&>Xw-xjZEUFFTv>JD^O#1E#)CMBaG4DX9bD(Wtc8Rzq}9soQ8`jf zeSnHOL}<+WVSKp4kkq&?SbETjq6yr@4%SAqOG=9E(3YeLG9dtV+8vmzq+6PFPk{L; z(&d++iu=^F%b+ea$i2UeTC{R*0Isk;vFK!no<;L+(`y`3&H-~VTdKROkdyowo1iqR zbVW(3`+(PQ2>TKY>N!jGmGo7oeoB8O|P_!Ic@ zZ^;3dnuXo;WJ?S+)%P>{Hcg!Jz#2SI(s&dY4QAy_vRlmOh)QHvs_7c&zkJCmJGVvV zX;Mtb>QE+xp`KyciG$Cn*0?AK%-a|=o!+7x&&yzHQOS>8=B*R=niSnta^Pxp1`=md z#;$pS$4WCT?mbiCYU?FcHGZ#)kHVJTTBt^%XE(Q};aaO=Zik0UgLcc0I(tUpt(>|& zcxB_|fxCF7>&~5eJ=Dpn&5Aj{A^cV^^}(7w#p;HG&Q)EaN~~EqrE1qKrMAc&WXIE;>@<&)5;gD2?={Xf@Mvn@OJKw=8Mgn z!JUFMwD+s==JpjhroT&d{$kQAy%+d`a*XxDEVxy3`NHzmITrE`o!;5ClXNPb4t*8P zzAivdr{j_v!=9!^?T3y?gzmqDWX6mkzhIzJ-3S{T5bcCFMr&RPDryMcdwbBuZbsgN zGrp@^i?rcfN7v0NKGzDPGE#4yszxu=I_`MI%Z|10nFjU-UjQXXA?k8Pk|OE<(?ae) zE%vG#eZAlj*E7_3dx#Zz4kMLj>H^;}33UAankJiDy5ZvEhrjr`!9eMD8COp}U*hP+ zF}KIYx@pkccIgyxFm#LNw~G&`;o&5)2`5aogs`1~7cMZQ7zj!%L4E`2yzlQN6REX20&O<9 zKV6fyr)TScJPPzNTC2gL+0x#=u>(({{D7j)c-%tvqls3#Y?Z1m zV5WUE)zdJ{$p>yX;^P!UcXP?UD~YM;IRa#Rs5~l+*$&nO(;Ers`G=0D!twR(0GF@c zHl9E5DQI}Oz74n zfKP>&$q0($T4y$6w(p=ERAFh+>n%iaeRA%!T%<^+pg?M)@ucY<&59$x9M#n+V&>}=nO9wCV{O~lg&v#+jcUj(tQ z`0u1YH)-`U$15a{pBkGyPL0THv1P|4e@pf@3IBZS4dVJPo#H>pWq%Lr0YS-SeWash z8R7=jb28KPMI|_lo#GEO|5B?N_e``H*23{~a!AmUJ+fb4HX-%QI@lSEUxKlGV7z7Q zSKw@-TR>@1RL%w{x}dW#k1NgW+q4yt2Xf1J62Bx*O^WG8OJ|FqI4&@d3_o8Id@*)4 zYrk=>@!wv~mh7YWv*bZhxqSmFh2Xq)o=m;%n$I?GSz49l1$xRpPu_^N(vZ>*>Z<04 z2+rP70oM=NDysd!@fQdM2OcyT?3T^Eb@lIC-UG=Bw{BjQ&P`KCv$AcJ;?`vdZ4){d z&gkoUK{$!$$K`3*O-jyM1~p-7T*qb)Ys>Myt^;#1&a%O@x8A+E>! zY8=eD`ZG)LVagDLBeHg>=atOG?Kr%h4B%E6m@J^C+U|y)XX@f z8oyJDW|9g=<#f<{JRr{y#~euMnv)`7j=%cHWLc}ngjq~7k**6%4u>Px&W%4D94(r* z+akunK}O0DC2A%Xo9jyF;DobX?!1I(7%}@7F>i%&nk*LMO)bMGg2N+1iqtg+r(70q zF5{Msgsm5GS7DT`kBsjMvOrkx&|EU!{{~gL4d2MWrAT=KBQ-^zQCUq{5PD1orxlIL zq;CvlWx#f1NWvh`hg011I%?T_s!e38l*lWVt|~z-PO4~~1g)SrJ|>*tXh=QfXT)%( z+ex+inPvD&O4Ur;JGz>$sUOnWdpSLcm1X%aQDw4{dB!cnj`^muI$CJ2%p&-kULVCE z>$eMR36kN$wCPR+OFDM3-U(VOrp9k3)lI&YVFqd;Kpz~K)@Fa&FRw}L(SoD z9B4a+hQzZT-BnVltst&=kq6Y(f^S4hIGNKYBgMxGJ^;2yrO}P3;r)(-I-CZ)26Y6? z&rzHI_1GCvGkgy-t1E;r^3Le30|%$ebDRu2+gdLG)r=A~Qz`}~&L@aGJ{}vVs_GE* zVUjFnzHiXfKQbpv&bR&}l2bzIjAooB)=-XNcYmrGmBh(&iu@o!^hn0^#}m2yZZUK8 zufVm7Gq0y`Mj;9b>`c?&PZkU0j4>IL=UL&-Lp3j&47B5pAW4JceG{!XCA)kT<%2nqCxj<)uy6XR_uws~>_MEKPOpAQ!H zkn>FKh)<9DwwS*|Y(q?$^N!6(51O0 z^JM~Ax{AI1Oj$fs-S5d4T7Z_i1?{%0SsIuQ&r8#(JA=2iLcTN+?>wOL532%&dMYkT z*T5xepC+V6zxhS@vNbMoi|i)=rpli@R9~P!39tWbSSb904ekv7D#quKbgFEMTb48P zuq(VJ+&L8aWU(_FCD$3^uD!YM%O^K(dvy~Wm2hUuh6bD|#(I39Xt>N1Y{ZqXL`Fg6 zKQ?T2htHN!(Bx;tV2bfTtIj7e)liN-29s1kew>v(D^@)#v;}C4-G=7x#;-dM4yRWm zyY`cS21ulzMK{PoaQ6xChEZ}o_#}X-o}<&0)$1#3we?+QeLt;aVCjeA)hn!}UaKt< zat1fHEx13y-rXNMvpUUmCVzocPmN~-Y4(YJvQ#db)4|%B!rBsgAe+*yor~}FrNH08 z3V!97S}D7d$zbSD{$z;@IYMxM6aHdypIuS*pr_U6;#Y!_?0i|&yU*@16l z*dcMqDQgfNBf}?quiu4e>H)yTVfsp#f+Du0@=Kc41QockXkCkvu>FBd6Q+@FL!(Yx z2`YuX#eMEiLEDhp+9uFqME_E^faV&~9qjBHJkIp~%$x^bN=N)K@kvSVEMdDuzA0sn z88CBG?`RX1@#hQNd`o^V{37)!w|nA)QfiYBE^m=yQKv-fQF+UCMcuEe1d4BH7$?>b zJl-r9@0^Ie=)guO1vOd=i$_4sz>y3x^R7n4ED!5oXL3@5**h(xr%Hv)_gILarO46q+MaDOF%ChaymKoI6JU5Pg;7#2n9-18|S1;AK+ zgsn6;k6-%!QD>D?cFy}8F;r@z8H9xN1jsOBw2vQONVqBVEbkiNUqgw~*!^##ht>w0 zUOykwH=$LwX2j&nLy=@{hr)2O&-wm-NyjW7n~Zs9UlH;P7iP3 zI}S(r0YFVYacnKH(+{*)Tbw)@;6>%=&Th=+Z6NHo_tR|JCI8TJiXv2N7ei7M^Q+RM z?9o`meH$5Yi;@9XaNR#jIK^&{N|DYNNbtdb)XW1Lv2k{E>;?F`#Pq|&_;gm~&~Zc9 zf+6ZE%{x4|{YdtE?a^gKyzr}dA>OxQv+pq|@IXL%WS0CiX!V zm$fCePA%lU{%pTKD7|5NJHeXg=I0jL@$tOF@K*MI$)f?om)D63K*M|r`gb9edD1~Y zc|w7N)Y%do7=0{RC|AziW7#am$)9jciRJ?IWl9PE{G3U+$%FcyKs_0Cgq`=K3@ttV z9g;M!3z~f_?P%y3-ph%vBMeS@p7P&Ea8M@97+%XEj*(1E6vHj==d zjsoviB>j^$_^OI_DEPvFkVo(BGRo%cJeD){6Uckei=~1}>sp299|IRjhXe)%?uP0I zF5+>?0#Ye}T^Y$u_rc4=lPcq4K^D(TZG-w30-YiEM=dcK+4#o*>lJ8&JLi+3UcpZk z!^?95S^C0ja^jwP`|{<+3cBVog$(mRdQmadS+Vh~z zS@|P}=|z3P6uS+&@QsMp0no9Od&27O&14zHXGAOEy zh~OKpymK5C%;LLb467@KgIiVwYbYd6wFxI{0-~MOGfTq$nBTB!{SrWmL9Hs}C&l&l#m?s*{tA?BHS4mVKHAVMqm63H<|c5n0~k)-kbg zXidai&9ZUy0~WFYYKT;oe~rytRk?)r8bptITsWj(@HLI;@=v5|XUnSls7$uaxFRL+ zRVMGuL3w}NbV1`^=Pw*0?>bm8+xfeY(1PikW*PB>>Tq(FR`91N0c2&>lL2sZo5=VD zQY{>7dh_TX98L2)n{2OV=T10~*YzX27i2Q7W86M4$?gZIXZaBq#sA*{PH8){|GUi;oM>e?ua7eF4WFuFYZSG| zze?srg|5Ti8Og{O zeFxuw9!U+zhyk?@w zjsA6(oKD=Ka;A>Ca)oPORxK+kxH#O@zhC!!XS4@=swnuMk>t+JmLmFiE^1aX3f<)D@`%K0FGK^gg1a1j>zi z2KhV>sjU7AX3F$SEqrXSC}fRx64GDoc%!u2Yag68Lw@w9v;xOONf@o)Lc|Uh3<21ctTYu-mFZuHk*+R{GjXHIGq3p)tFtQp%TYqD=j1&y)>@zxoxUJ!G@ zgI0XKmP6MNzw>nRxK$-Gbzs}dyfFzt>#5;f6oR27ql!%+{tr+(`(>%51|k`ML} zY4eE)Lxq|JMas(;JibNQds1bUB&r}ydMQXBY4x(^&fY_&LlQC)3hylc$~8&~|06-D z#T+%66rYbHX%^KuqJED_wuGB+=h`nWA!>1n0)3wZrBG3%`b^Ozv6__dNa@%V14|!D zQ?o$z5u0^8`giv%qE!BzZ!3j;BlDlJDk)h@9{nSQeEk!z9RGW) z${RSF3phEM*ce*>Xdp}585vj$|40=&S{S-GTiE?Op*vY&Lvr9}BO$XWy80IF+6@%n z5*2ueT_g@ofP#u5pxb7n*fv^Xtt7&?SRc{*2Ka-*!BuOpf}neHGCiHy$@Ka1^Dint z;DkmIL$-e)rj4o2WQV%Gy;Xg(_Bh#qeOsTM2f@KEe~4kJ8kNLQ+;(!j^bgJMcNhvklP5Z6I+9Fq@c&D~8Fb-4rmDT!MB5QC{Dsb;BharP*O;SF4& zc$wj-7Oep7#$WZN!1nznc@Vb<_Dn%ga-O#J(l=OGB`dy=Sy&$(5-n3zzu%d7E#^8`T@}V+5B;PP8J14#4cCPw-SQTdGa2gWL0*zKM z#DfSXs_iWOMt)0*+Y>Lkd=LlyoHjublNLefhKBv@JoC>P7N1_#> zv=mLWe96%EY;!ZGSQDbZWb#;tzqAGgx~uk+-$+2_8U`!ypbwXl z^2E-FkM1?lY@yt8=J3%QK+xaZ6ok=-y%=KXCD^0r!5vUneW>95PzCkOPO*t}p$;-> ze5j-BLT_;)cZQzR2CEsm@rU7GZfFtdp*a|g4wDr%8?2QkIGasRfDWT-Dvy*U{?IHT z*}wGnzdlSptl#ZF^sf)KT|BJs&kLG91^A6ls{CzFprZ6-Y!V0Xysh%9p%iMd7HLsS zN+^Un$tDV)T@i!v?3o0Fsx2qI(AX_$dDkBzQ@fRM%n zRXk6hb9Py#JXUs+7)w@eo;g%QQ95Yq!K_d=z{0dGS+pToEI6=Bo8+{k$7&Z zo4>PH(`ce8E-Ps&uv`NQ;U$%t;w~|@E3WVOCi~R4oj5wP?%<*1C%}Jq%a^q~T7u>K zML5AKfQDv6>PuT`{SrKHRAF+^&edg6+5R_#H?Lz3iGoWo#PCEd0DS;)2U({{X#zU^ zw_xv{4x7|t!S)>44J;KfA|DC?;uQ($l+5Vp7oeqf7{GBF9356nx|&B~gs+@N^gSdd zvb*>&W)|u#F{Z_b`f#GVtQ`pYv3#||N{xj1NgB<#=Odt6{eB%#9RLt5v zIi|0u70`#ai}9fJjKv7dE!9ZrOIX!3{$z_K5FBd-Kp-&e4(J$LD-)NMTp^_pB`RT; zftVVlK2g@+1Ahv2$D){@Y#cL#dUj9*&%#6 zd2m9{1NYp>)6=oAvqdCn5#cx{AJ%S8skUgMglu2*IAtd+z1>B&`MuEAS(D(<6X#Lj z?f4CFx$)M&$=7*>9v1ER4b6!SIz-m0e{o0BfkySREchp?WdVPpQCh!q$t>?rL!&Jg zd#heM;&~A}VEm8Dvy&P|J*eAV&w!&Nx6HFV&B8jJFVTmgLaswn!cx$&%JbTsloz!3 zMEz1d`k==`Ueub_JAy_&`!ogbwx27^ZXgFNAbx=g_I~5nO^r)}&myw~+yY*cJl4$I znNJ32M&K=0(2Dj_>@39`3=FX!v3nZHno_@q^!y}%(yw0PqOo=);6Y@&ylVe>nMOZ~ zd>j#QQSBn3oaWd;qy$&5(5H$Ayi)0haAYO6TH>FR?rhqHmNOO+(})NB zLI@B@v0)eq!ug`>G<@htRlp3n!EpU|n+G+AvXFrWSUsLMBfL*ZB`CRsIVHNTR&b?K zxBgsN0BjfB>UVcJ|x%=-zb%OV7lmZc& zxiupadZVF7)6QuhoY;;FK2b*qL0J-Rn-8!X4ZY$-ZSUXV5DFd7`T41c(#lAeLMoeT z4%g655v@7AqT!i@)Edt5JMbN(=Q-6{=L4iG8RA%}w;&pKmtWvI4?G9pVRp|RTw`g0 zD5c12B&A2&P6Ng~8WM2eIW=wxd?r7A*N+&!Be7PX3s|7~z=APxm=A?5 zt>xB4WG|*Td@VX{Rs)PV0|yK`oI3^xn(4c_j&vgxk_Y3o(-`_5o`V zRTghg6%l@(qodXN;dB#+OKJEEvhfcnc#BeO2|E(5df-!fKDZ!%9!^BJ_4)9P+9Dq5 zK1=(v?KmIp34r?z{NEWnLB3Px{XYwy-akun4F7xTRr2^zeYW{gcK9)>aJDdU5;w5@ zak=<+-PLH-|04pelTb%ULpuuuJC7DgyT@D|p{!V!0v3KpDnRjANN12q6SUR3mb9<- z>2r~IApQGhstZ!3*?5V z8#)hJ0TdZg0M-BK#nGFP>$i=qk82DO z7h;Ft!D5E15OgW)&%lej*?^1~2=*Z5$2VX>V{x8SC+{i10BbtUk9@I#Vi&hX)q

    Q!LwySI{Bnv%Sm)yh{^sSVJ8&h_D-BJ_YZe5eCaAWU9b$O2c z$T|{vWVRtOL!xC0DTc(Qbe`ItNtt5hr<)VijD0{U;T#bUEp381_y`%ZIav?kuYG{iyYdEBPW=*xNSc;Rlt6~F4M`5G+VtOjc z*0qGzCb@gME5udTjJA-9O<&TWd~}ysBd(eVT1-H82-doyH9RST)|+Pb{o*;$j9Tjs zhU!IlsPsj8=(x3bAKJTopW3^6AKROHR^7wZ185wJGVhA~hEc|LP;k7NEz-@4p5o}F z`AD6naG3(n=NF9HTH81=F+Q|JOz$7wm9I<+#BSmB@o_cLt2GkW9|?7mM;r!JZp89l zbo!Hp8=n!XH1{GwaDU+k)pGp`C|cXkCU5%vcH)+v@0eK>%7gWxmuMu9YLlChA|_D@ zi#5zovN_!a-0?~pUV-Rj*1P)KwdU-LguR>YM&*Nen+ln8Q$?WFCJg%DY%K}2!!1FE zDv-A%Cbwo^p(lzac&_TZ-l#9kq`mhLcY3h9ZTUVCM(Ad&=EriQY5{jJv<5K&g|*Lk zgV%ILnf1%8V2B0E&;Sp4sYbYOvvMebLwYwzkRQ#F8GpTQq#uv=J`uaSJ34OWITeSGo6+-8Xw znCk*n{kdDEi)Hi&u^)~cs@iyCkFWB2SWZU|Uc%^43ZIZQ-vWNExCCtDWjqHs;;tWf$v{}0{p0Rvxkq``)*>+Akq%|Na zA`@~-Vfe|+(AIlqru+7Ceh4nsVmO9p9jc8}HX^W&ViBDXT+uXbT#R#idPn&L>+#b6 zflC-4C5-X;kUnR~L>PSLh*gvL68}RBsu#2l`s_9KjUWRhiqF`j)`y`2`YU(>3bdBj z?>iyjEhe-~$^I5!nn%B6Wh+I`FvLNvauve~eX<+Ipl&04 zT}};W&1a3%W?dJ2=N#0t?e+aK+%t}5q%jSLvp3jZ%?&F}nOOWr>+{GFIa%wO_2`et z=JzoRR~}iKuuR+azPI8;Gf9)z3kyA4EIOSl!sRR$DlW}0>&?GbgPojmjmnln;cTqCt=ADbE zZ8GAnoM+S1(5$i8^O4t`ue;vO4i}z0wz-QEIVe5_u03;}-!G1NyY8;h^}y;tzY}i5 zqQr#Ur3Fy8sSa$Q0ys+f`!`+>9WbvU_I`Sj;$4{S>O3?#inLHCrtLy~!s#WXV=oVP zeE93*Nc`PBi4q@%Ao$x4lw9vLHM!6mn3-b_cebF|n-2vt-zYVF_&sDE--J-P;2WHo z+@n2areE0o$LjvjlV2X7ZU@j+`{*8zq`JR3gKF#EW|#+{nMyo-a>nFFTg&vhyT=b} zDa8+v0(Dgx0yRL@ZXOYIlVSZ0|MFizy0VPW8;AfA5|pe!#j zX}Py^8fl5SyS4g1WSKKtnyP+_PoOwMMwu`(i@Z)diJp~U54*-miOchy7Z35eL>^M z4p<-aIxH4VUZgS783@H%M7P9hX>t{|RU7$n4T(brCG#h9e9p! z+o`i;EGGq3&pF;~5V~eBD}lC)>if$w%Vf}AFxGqO88|ApfHf&Bvu+xdG)@vuF}Yvk z)o;~k-%+0K0g+L`Wala!$=ZV|z$e%>f0%XoLib%)!R^RoS+{!#X?h-6uu zF&&KxORdZU&EwQFITIRLo(7TA3W}y6X{?Y%y2j0It!ekU#<)$qghZtpcS>L3uh`Uj z7GY;6f$9qKynP#oS3$$a{p^{D+0oJQ71`1?OAn_m8)UGZmj3l*ZI)`V-a>MKGGFG< z&^jg#Ok%(hhm>hSrZ5;Qga4u(?^i>GiW_j9%_7M>j(^|Om$#{k+^*ULnEgzW_1gCICtAD^WpC`A z{9&DXkG#01Xo)U$OC(L5Y$DQ|Q4C6CjUKk1UkPj$nXH##J{c8e#K|&{mA*;b$r0E4 zUNo0jthwA(c&N1l=PEe8Rw_8cEl|-eya9z&H3#n`B$t#+aJ03RFMzrV@gowbe8v(c zIFM60^0&lCFO10NU4w@|61xiZ4CVXeaKjd;d?sv52XM*lS8XiVjgWpRB;&U_C0g+`6B5V&w|O6B*_q zsATxL!M}+$He)1eOWECce#eS@2n^xhlB4<_Nn?yCVEQWDs(r`|@2GqLe<#(|&P0U? z$7V5IgpWf09uIf_RazRwC?qEqRaHyL?iiS05UiGesJy%^>-C{{ypTBI&B0-iUYhk> zIk<5xpsuV@g|z(AZD+C-;A!fTG=df1=<%nxy(a(IS+U{ME4ZbDEBtcD_3V=icT6*_ z)>|J?>&6%nvHhZERBtjK+s4xnut*@>GAmA5m*OTp$!^CHTr}vM4n(X1Q*;{e-Rd2BCF-u@1ZGm z!S8hJ6L=Gl4T_SDa7Xx|-{4mxveJg=ctf`BJ*fy!yF6Dz&?w(Q_6B}WQVtNI!BVBC zKfX<>7vd6C96}XAQmF-Jd?1Q4eTfRB3q7hCh0f!(JkdWT5<{iAE#dKy*Jxq&3a1@~ z8C||Dn2mFNyrUV|<-)C^_y7@8c2Fz+2jrae9deBDu;U}tJ{^xAdxCD248(k;dCJ%o z`y3sADe>U%suxwwv~8A1+R$VB=Q?%U?4joI$um;aH+eCrBqpn- z%79D_7rb;R-;-9RTrwi9dPlg8&@tfWhhZ(Vx&1PQ+6(huX`;M9x~LrW~~#3{j0Bh2kDU$}@!fFQej4VGkJv?M4rU^x!RU zEwhu$!CA_iDjFjrJa`aocySDX16?~;+wgav;}Zut6Mg%C4>}8FL?8)Kgwc(Qlj{@#2Pt0?G`$h7P#M+qoXtlV@d}%c&OzO+QYKK`kyXaK{U(O^2DyIXCZlNQjt0^8~8JzNGrIxhj}}M z&~QZlbx%t;MJ(Vux;2tgNKGlAqphLq%pd}JG9uoVHUo?|hN{pLQ6Em%r*+7t^<);X zm~6=qChlNAVXNN*Sow->*4;}T;l;D1I-5T{Bif@4_}=>l`tK;qqDdt5zvisCKhMAH z#r}`)7VW?LZqfdmXQ%zo5bJ00{Xb9^YKrk0Nf|oIW*K@(=`o2Vndz}ZDyk{!u}PVx zzd--+_WC*U{~DH3{?GI64IB+@On&@9X>EUAo&L+G{L^dozaI4C3G#2wr~hseW@K&g zKWs{uHu-9Je!3;4pE>eBltKUXb^*hG8I&413)$J&{D4N%7PcloU6bn%jPxJyQL?g* z9g+YFFEDiE`8rW^laCNzQmi7CTnPfwyg3VDHRAl>h=In6jeaVOP@!-CP60j3+#vpL zEYmh_oP0{-gTe7Or`L6x)6w?77QVi~jD8lWN@3RHcm80iV%M1A!+Y6iHM)05iC64tb$X2lV_%Txk@0l^hZqi^%Z?#- zE;LE0uFx)R08_S-#(wC=dS&}vj6P4>5ZWjhthP=*Hht&TdLtKDR;rXEX4*z0h74FA zMCINqrh3Vq;s%3MC1YL`{WjIAPkVL#3rj^9Pj9Ss7>7duy!9H0vYF%>1jh)EPqvlr6h%R%CxDsk| z!BACz7E%j?bm=pH6Eaw{+suniuY7C9Ut~1cWfOX9KW9=H><&kQlinPV3h9R>3nJvK z4L9(DRM=x;R&d#a@oFY7mB|m8h4692U5eYfcw|QKwqRsshN(q^v$4$)HgPpAJDJ`I zkqjq(8Cd!K!+wCd=d@w%~e$=gdUgD&wj$LQ1r>-E=O@c ze+Z$x{>6(JA-fNVr)X;*)40Eym1TtUZI1Pwwx1hUi+G1Jlk~vCYeXMNYtr)1?qwyg zsX_e*$h?380O00ou?0R@7-Fc59o$UvyVs4cUbujHUA>sH!}L54>`e` zHUx#Q+Hn&Og#YVOuo*niy*GU3rH;%f``nk#NN5-xrZ34NeH$l`4@t);4(+0|Z#I>Y z)~Kzs#exIAaf--65L0UHT_SvV8O2WYeD>Mq^Y6L!Xu8%vnpofG@w!}R7M28?i1*T&zp3X4^OMCY6(Dg<-! zXmcGQrRgHXGYre7GfTJ)rhl|rs%abKT_Nt24_Q``XH{88NVPW+`x4ZdrMuO0iZ0g` z%p}y};~T5gbb9SeL8BSc`SO#ixC$@QhXxZ=B}L`tP}&k?1oSPS=4%{UOHe0<_XWln zwbl5cn(j-qK`)vGHY5B5C|QZd5)W7c@{bNVXqJ!!n$^ufc?N9C-BF2QK1(kv++h!>$QbAjq)_b$$PcJdV+F7hz0Hu@ zqj+}m0qn{t^tD3DfBb~0B36|Q`bs*xs|$i^G4uNUEBl4g;op-;Wl~iThgga?+dL7s zUP(8lMO?g{GcYpDS{NM!UA8Hco?#}eNEioRBHy4`mq!Pd-9@-97|k$hpEX>xoX+dY zDr$wfm^P&}Wu{!%?)U_(%Mn79$(ywvu*kJ9r4u|MyYLI_67U7%6Gd_vb##Nerf@>& z8W11z$$~xEZt$dPG}+*IZky+os5Ju2eRi;1=rUEeIn>t-AzC_IGM-IXWK3^6QNU+2pe=MBn4I*R@A%-iLDCOHTE-O^wo$sL_h{dcPl=^muAQb`_BRm};=cy{qSkui;`WSsj9%c^+bIDQ z0`_?KX0<-=o!t{u(Ln)v>%VGL z0pC=GB7*AQ?N7N{ut*a%MH-tdtNmNC+Yf$|KS)BW(gQJ*z$d{+{j?(e&hgTy^2|AR9vx1Xre2fagGv0YXWqtNkg*v%40v?BJBt|f9wX5 z{QTlCM}b-0{mV?IG>TW_BdviUKhtosrBqdfq&Frdz>cF~yK{P@(w{Vr7z2qKFwLhc zQuogKO@~YwyS9%+d-zD7mJG~@?EFJLSn!a&mhE5$_4xBl&6QHMzL?CdzEnC~C3$X@ zvY!{_GR06ep5;<#cKCSJ%srxX=+pn?ywDwtJ2{TV;0DKBO2t++B(tIO4)Wh`rD13P z4fE$#%zkd=UzOB74gi=-*CuID&Z3zI^-`4U^S?dHxK8fP*;fE|a(KYMgMUo`THIS1f!*6dOI2 zFjC3O=-AL`6=9pp;`CYPTdVX z8(*?V&%QoipuH0>WKlL8A*zTKckD!paN@~hh zmXzm~qZhMGVdQGd=AG8&20HW0RGV8X{$9LldFZYm zE?}`Q3i?xJRz43S?VFMmqRyvWaS#(~Lempg9nTM$EFDP(Gzx#$r)W&lpFKqcAoJh-AxEw$-bjW>`_+gEi z2w`99#UbFZGiQjS8kj~@PGqpsPX`T{YOj`CaEqTFag;$jY z8_{Wzz>HXx&G*Dx<5skhpETxIdhKH?DtY@b9l8$l?UkM#J-Snmts7bd7xayKTFJ(u zyAT&@6cAYcs{PBfpqZa%sxhJ5nSZBPji?Zlf&}#L?t)vC4X5VLp%~fz2Sx<*oN<7` z?ge=k<=X7r<~F7Tvp9#HB{!mA!QWBOf%EiSJ6KIF8QZNjg&x~-%e*tflL(ji_S^sO ztmib1rp09uon}RcsFi#k)oLs@$?vs(i>5k3YN%$T(5Or(TZ5JW9mA6mIMD08=749$ z!d+l*iu{Il7^Yu}H;lgw=En1sJpCKPSqTCHy4(f&NPelr31^*l%KHq^QE>z>Ks_bH zjbD?({~8Din7IvZeJ>8Ey=e;I?thpzD=zE5UHeO|neioJwG;IyLk?xOz(yO&0DTU~ z^#)xcs|s>Flgmp;SmYJ4g(|HMu3v7#;c*Aa8iF#UZo7CvDq4>8#qLJ|YdZ!AsH%^_7N1IQjCro

    K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ffed3a25..00000000 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew deleted file mode 100755 index 4f906e0c..00000000 --- a/samples/rest-assured/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat deleted file mode 100644 index 107acd32..00000000 --- a/samples/rest-assured/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -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. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-assured/settings.gradle b/samples/rest-assured/settings.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/samples/rest-assured/src/docs/asciidoc/index.adoc b/samples/rest-assured/src/docs/asciidoc/index.adoc deleted file mode 100644 index f89aeb6e..00000000 --- a/samples/rest-assured/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,26 +0,0 @@ -= Spring REST Docs REST Assured Sample -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs - -Sample application demonstrating how to use Spring REST Docs with REST Assured. - -`SampleRestAssuredApplicationTests` makes a call to a very simple service. The service -that is being tested is running on a random port on `localhost`. The tests make use of a -preprocessor to modify the request so that it appears to have been sent to -`https://api.example.com`. If your service includes URIs in its responses, for example -because it uses hypermedia, similar preprocessing can be applied to the response before -it is documented. - -Three snippets are produced. One showing how to make a request using cURL: - -include::{snippets}/sample/curl-request.adoc[] - -One showing the HTTP request: - -include::{snippets}/sample/http-request.adoc[] - -And one showing the HTTP response: - -include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java deleted file mode 100644 index af82e0bb..00000000 --- a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java +++ /dev/null @@ -1,71 +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 com.example.restassured; - -import java.net.InetSocketAddress; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.function.server.RequestPredicates; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerResponse; - -import reactor.netty.DisposableServer; -import reactor.netty.http.server.HttpServer; - -@EnableWebFlux -@Configuration -class SampleRestAssuredApplication { - - @Bean - RouterFunction routerFunction() { - return RouterFunctions.route(RequestPredicates.GET("/"), - (request) -> ServerResponse.status(HttpStatus.OK).bodyValue("Hello, World")); - } - - @Bean - WebServer webServer(RouterFunction routerFunction) { - return new WebServer(routerFunction); - } - - static class WebServer implements DisposableBean { - - private final DisposableServer httpServer; - - WebServer(RouterFunction routerFunction) { - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(routerFunction)); - HttpServer httpServer = HttpServer.create().handle(adapter); - this.httpServer = httpServer.bindNow(); - } - - @Override - public void destroy() throws Exception { - this.httpServer.disposeNow(); - } - - int getPort() { - return ((InetSocketAddress)this.httpServer.address()).getPort(); - } - - } - -} diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java deleted file mode 100644 index 3c9c2ef3..00000000 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ /dev/null @@ -1,77 +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 com.example.restassured; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import com.example.restassured.SampleRestAssuredApplication.WebServer; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; - -@ContextConfiguration(classes = SampleRestAssuredApplication.class) -@RunWith(SpringJUnit4ClassRunner.class) -public class SampleRestAssuredApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - private RequestSpecification documentationSpec; - - @Autowired - private WebServer webServer; - - private int port; - - @Before - public void setUp() { - this.documentationSpec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(restDocumentation)).build(); - this.port = webServer.getPort(); - } - - @Test - public void sample() { - given(this.documentationSpec) - .accept("text/plain") - .filter(document("sample", - preprocessRequest(modifyUris() - .scheme("https") - .host("api.example.com") - .removePort()))) - .when() - .port(this.port) - .get("/") - .then() - .assertThat().statusCode(is(200)); - } - -} diff --git a/samples/rest-notes-slate/README.md b/samples/rest-notes-slate/README.md deleted file mode 100644 index df99663c..00000000 --- a/samples/rest-notes-slate/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# REST Notes Slate - -This sample shows how to document a RESTful API using Spring REST Docs and [Slate][1]. -The sample can be built with Gradle, but requires Ruby and the `bundler` gem to -be installed. - -## Quickstart - -``` -./gradlew build -open build/docs/api-guide.html -``` - -## Details - -The bulk of the documentation is written in Markdown in the file named -[slate/source/api-guide.md.erb][2]. When the documentation is built, snippets generated by -Spring REST Docs in the [ApiDocumentation][3] tests are incorporated into this -documentation by [ERB][4]. The combined Markdown document is then turned into HTML. - -[1]: https://github.com/lord/slate -[2]: slate/source/api-guide.html.md.erb -[3]: src/test/java/com/example/notes/ApiDocumentation.java -[4]: https://ruby-doc.org/stdlib-2.2.3/libdoc/erb/rdoc/ERB.html \ No newline at end of file diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle deleted file mode 100644 index ee274abe..00000000 --- a/samples/rest-notes-slate/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -plugins { - id "java" -} - -repositories { - mavenLocal { - content { - includeGroup("org.springframework.restdocs") - } - } - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 17 -targetCompatibility = 17 - -ext { - restdocsVersion = '3.0.0-SNAPSHOT' - snippetsDir = file('build/generated-snippets') -} - -ext['spring-restdocs.version'] = '3.0.0-SNAPSHOT' - -dependencies { - implementation platform("org.springframework:spring-framework-bom:6.0.0-M5") - implementation platform("org.springframework.data:spring-data-bom:2022.0.0-M5") - implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" - implementation "jakarta.servlet:jakarta.servlet-api:5.0.0" - implementation "org.hibernate.validator:hibernate-validator:7.0.4.Final" - implementation "org.hibernate:hibernate-core-jakarta:5.6.9.Final" - implementation "org.springframework:spring-webmvc" - implementation "org.springframework.data:spring-data-jpa" - implementation "org.springframework.data:spring-data-rest-webmvc" - - runtimeOnly 'com.h2database:h2:2.1.210' - runtimeOnly 'org.atteo:evo-inflector:1.2.1' - - testImplementation 'com.jayway.jsonpath:json-path:2.6.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.assertj:assertj-core:3.21.0' - testImplementation 'org.hamcrest:hamcrest-library:2.2' - testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" - - testRuntimeOnly 'org.glassfish:jakarta.el:4.0.2' -} - -test { - outputs.dir snippetsDir -} - -task(bundleInstall, type: Exec) { - workingDir file('slate') - executable 'bundle' - args 'install' -} - -task(slate, type: Exec) { - dependsOn 'bundleInstall', 'test' - workingDir file('slate') - executable 'bundle' - args 'exec', 'middleman', 'build' -} - -build { - dependsOn 'slate' -} diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59203 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

    K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ffed3a25..00000000 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/rest-notes-slate/gradlew b/samples/rest-notes-slate/gradlew deleted file mode 100755 index 4f906e0c..00000000 --- a/samples/rest-notes-slate/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat deleted file mode 100644 index 107acd32..00000000 --- a/samples/rest-notes-slate/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -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. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-notes-slate/settings.gradle b/samples/rest-notes-slate/settings.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/samples/rest-notes-slate/slate/.dockerignore b/samples/rest-notes-slate/slate/.dockerignore deleted file mode 100644 index f6430175..00000000 --- a/samples/rest-notes-slate/slate/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -.git/ -.github/ -build/ -.editorconfig -.gitattributes -.gitignore -CHANGELOG.md -CODE_OF_CONDUCT.md -deploy.sh -font-selection.json -README.md -Vagrantfile \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/.gitignore b/samples/rest-notes-slate/slate/.gitignore deleted file mode 100644 index 1d5d08dd..00000000 --- a/samples/rest-notes-slate/slate/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -*.gem -*.rbc -.bundle -.config -coverage -InstalledFiles -lib/bundler/man -pkg -rdoc -spec/reports -test/tmp -test/version_tmp -tmp -*.DS_STORE -build/ -.cache -.vagrant -.sass-cache - -# YARD artifacts -.yardoc -_yardoc -doc/ -.idea/ - -# Vagrant artifacts -ubuntu-*-console.log diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md deleted file mode 100644 index 6e7e39df..00000000 --- a/samples/rest-notes-slate/slate/CHANGELOG.md +++ /dev/null @@ -1,320 +0,0 @@ -# Changelog - -## Version 2.13.0 - -*April 22, 2022* - -* __Drop support for ruby 2.5__ -* Bump rouge from 3.26.1 to 3.28.0 -* Formally support ruby 3.1 -* Bump nokogiri from 1.12.5 to 1.13.4 -* Build docker images for multiple architectures (e.g. `aarch64`) -* Remove `VOLUME` declaration from Dockerfile (thanks @aemengo) - -The security vulnerabilities reported against recent versions of nokogiri should not affect slate users with a regular setup. - -## Version 2.12.0 - -*November 04, 2021* - -* Bump nokogiri from 1.12.3 to 1.12.5 -* Bump ffi from 1.15.0 to 1.15.4 -* Bump rouge from 3.26.0 to 3.26.1 -* Bump middleman from 4.4.0 to 4.4.2 -* Remove unnecessary files from docker images - -## Version 2.11.0 - -*August 12, 2021* - -* __[Security]__ Bump addressable transitive dependency from 2.7.0 to 2.8.0 -* Support specifying custom meta tags in YAML front-matter -* Bump nokogiri from 1.11.3 to 1.12.3 (minimum supported version is 1.11.4) -* Bump middleman-autoprefixer from 2.10.1 to 3.0.0 -* Bump jquery from 3.5.1 to 3.6.0 -* Bump middleman from [`d180ca3`](https://github.com/middleman/middleman/commit/d180ca337202873f2601310c74ba2b6b4cf063ec) to 4.4.0 - -## Version 2.10.0 - -*April 13, 2021* - -* Add support for Ruby 3.0 (thanks @shaun-scale) -* Add requirement for Git on installing dependencies -* Bump nokogiri from 1.11.2 to 1.11.3 -* Bump middleman from 4.3.11 to [`d180ca3`](https://github.com/middleman/middleman/commit/d180ca337202873f2601310c74ba2b6b4cf063ec) - -## Version 2.9.2 - -*March 30, 2021* - -* __[Security]__ Bump kramdown from 2.3.0 to 2.3.1 -* Bump nokogiri from 1.11.1 to 1.11.2 - -## Version 2.9.1 - -*February 27, 2021* - -* Fix Slate language tabs not working if localStorage is disabled - -## Version 2.9.0 - -*January 19, 2021* - -* __Drop support for Ruby 2.3 and 2.4__ -* __[Security]__ Bump nokogiri from 1.10.10 to 1.11.1 -* __[Security]__ Bump redcarpet from 3.5.0 to 3.5.1 -* Specify slate is not supported on Ruby 3.x -* Bump rouge from 3.24.0 to 3.26.0 - -## Version 2.8.0 - -*October 27, 2020* - -* Remove last trailing newline when using the copy code button -* Rework docker image and make available at slatedocs/slate -* Improve Dockerfile layout to improve caching (thanks @micvbang) -* Bump rouge from 3.20 to 3.24 -* Bump nokogiri from 1.10.9 to 1.10.10 -* Bump middleman from 4.3.8 to 4.3.11 -* Bump lunr.js from 2.3.8 to 2.3.9 - -## Version 2.7.1 - -*August 13, 2020* - -* __[security]__ Bumped middleman from 4.3.7 to 4.3.8 - -_Note_: Slate uses redcarpet, not kramdown, for rendering markdown to HTML, and so was unaffected by the security vulnerability in middleman. -If you have changed slate to use kramdown, and with GFM, you may need to install the `kramdown-parser-gfm` gem. - -## Version 2.7.0 - -*June 21, 2020* - -* __[security]__ Bumped rack in Gemfile.lock from 2.2.2 to 2.2.3 -* Bumped bundled jQuery from 3.2.1 to 3.5.1 -* Bumped bundled lunr from 0.5.7 to 2.3.8 -* Bumped imagesloaded from 3.1.8 to 4.1.4 -* Bumped rouge from 3.17.0 to 3.20.0 -* Bumped redcarpet from 3.4.0 to 3.5.0 -* Fix color of highlighted code being unreadable when printing page -* Add clipboard icon for "Copy to Clipboard" functionality to code boxes (see note below) -* Fix handling of ToC selectors that contain punctutation (thanks @gruis) -* Fix language bar truncating languages that overflow screen width -* Strip HTML tags from ToC title before displaying it in title bar in JS (backup to stripping done in Ruby code) (thanks @atic) - -To enable the new clipboard icon, you need to add `code_clipboard: true` to the frontmatter of source/index.html.md. -See [this line](https://github.com/slatedocs/slate/blame/main/source/index.html.md#L19) for an example of usage. - -## Version 2.6.1 - -*May 30, 2020* - -* __[security]__ update child dependency activesupport in Gemfile.lock to 5.4.2.3 -* Update Middleman in Gemfile.lock to 4.3.7 -* Replace Travis-CI with GitHub actions for continuous integration -* Replace Spectrum with GitHub discussions - -## Version 2.6.0 - -*May 18, 2020* - -__Note__: 2.5.0 was "pulled" due to a breaking bug discovered after release. It is recommended to skip it, and move straight to 2.6.0. - -* Fix large whitespace gap in middle column for sections with codeblocks -* Fix highlighted code elements having a different background than rest of code block -* Change JSON keys to have a different font color than their values -* Disable asset hashing for woff and woff2 elements due to middleman bug breaking woff2 asset hashing in general -* Move Dockerfile to Debian from Alpine -* Converted repo to a [GitHub template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository) -* Update sassc to 2.3.0 in Gemfile.lock - -## Version 2.5.0 - -*May 8, 2020* - -* __[security]__ update nokogiri to ~> 1.10.8 -* Update links in example docs to https://github.com/slatedocs/slate from https://github.com/lord/slate -* Update LICENSE to include full Apache 2.0 text -* Test slate against Ruby 2.5 and 2.6 on Travis-CI -* Update Vagrantfile to use Ubuntu 18.04 (thanks @bradthurber) -* Parse arguments and flags for deploy.sh on script start, instead of potentially after building source files -* Install nodejs inside Vagrantfile (thanks @fernandoaguilar) -* Add Dockerfile for running slate (thanks @redhatxl) -* update middleman-syntax and rouge to ~>3.2 -* update middleman to 4.3.6 - -## Version 2.4.0 - -*October 19, 2019* - -- Move repository from lord/slate to slatedocs/slate -- Fix documentation to point at new repo link, thanks to [Arun](https://github.com/slash-arun), [Gustavo Gawryszewski](https://github.com/gawry), and [Daniel Korbit](https://github.com/danielkorbit) -- Update `nokogiri` to 1.10.4 -- Update `ffi` in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack) -- Update `rack` to 2.0.7 in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack) -- Update middleman to `4.3` and relax constraints on middleman related gems, thanks to [jakemack](https://github.com/jakemack) -- Add sass gem, thanks to [jakemack](https://github.com/jakemack) -- Activate `asset_cache` in middleman to improve cacheability of static files, thanks to [Sam Gilman](https://github.com/thenengah) -- Update to using bundler 2 for `Gemfile.lock`, thanks to [jakemack](https://github.com/jakemack) - -## Version 2.3.1 - -*July 5, 2018* - -- Update `sprockets` in `Gemfile.lock` to fix security warnings - -## Version 2.3 - -*July 5, 2018* - -- Allows strikethrough in markdown by default. -- Upgrades jQuery to 3.2.1, thanks to [Tomi Takussaari](https://github.com/TomiTakussaari) -- Fixes invalid HTML in `layout.erb`, thanks to [Eric Scouten](https://github.com/scouten) for pointing out -- Hopefully fixes Vagrant memory issues, thanks to [Petter Blomberg](https://github.com/p-blomberg) for the suggestion -- Cleans HTML in headers before setting `document.title`, thanks to [Dan Levy](https://github.com/justsml) -- Allows trailing whitespace in markdown files, thanks to [Samuel Cousin](https://github.com/kuzyn) -- Fixes pushState/replaceState problems with scrolling not changing the document hash, thanks to [Andrey Fedorov](https://github.com/anfedorov) -- Removes some outdated examples, thanks [@al-tr](https://github.com/al-tr), [Jerome Dahdah](https://github.com/jdahdah), and [Ricardo Castro](https://github.com/mccricardo) -- Fixes `nav-padding` bug, thanks [Jerome Dahdah](https://github.com/jdahdah) -- Code style fixes thanks to [Sebastian Zaremba](https://github.com/vassyz) -- Nokogiri version bump thanks to [Grey Baker](https://github.com/greysteil) -- Fix to default `index.md` text thanks to [Nick Busey](https://github.com/NickBusey) - -Thanks to everyone who contributed to this release! - -## Version 2.2 - -*January 19, 2018* - -- Fixes bugs with some non-roman languages not generating unique headers -- Adds editorconfig, thanks to [Jay Thomas](https://github.com/jaythomas) -- Adds optional `NestingUniqueHeadCounter`, thanks to [Vladimir Morozov](https://github.com/greenhost87) -- Small fixes to typos and language, thx [Emir Ribić](https://github.com/ribice), [Gregor Martynus](https://github.com/gr2m), and [Martius](https://github.com/martiuslim)! -- Adds links to Spectrum chat for questions in README and ISSUE_TEMPLATE - -## Version 2.1 - -*October 30, 2017* - -- Right-to-left text stylesheet option, thanks to [Mohammad Hossein Rabiee](https://github.com/mhrabiee) -- Fix for HTML5 history state bug, thanks to [Zach Toolson](https://github.com/ztoolson) -- Small styling changes, typo fixes, small bug fixes from [Marian Friedmann](https://github.com/rnarian), [Ben Wilhelm](https://github.com/benwilhelm), [Fouad Matin](https://github.com/fouad), [Nicolas Bonduel](https://github.com/NicolasBonduel), [Christian Oliff](https://github.com/coliff) - -Thanks to everyone who submitted PRs for this version! - -## Version 2.0 - -*July 17, 2017* - -- All-new statically generated table of contents - - Should be much faster loading and scrolling for large pages - - Smaller Javascript file sizes - - Avoids the problem with the last link in the ToC not ever highlighting if the section was shorter than the page - - Fixes control-click not opening in a new page - - Automatically updates the HTML title as you scroll -- Updated design - - New default colors! - - New spacings and sizes! - - System-default typefaces, just like GitHub -- Added search input delay on large corpuses to reduce lag -- We even bumped the major version cause hey, why not? -- Various small bug fixes - -Thanks to everyone who helped debug or wrote code for this version! It was a serious community effort, and I couldn't have done it alone. - -## Version 1.5 - -*February 23, 2017* - -- Add [multiple tabs per programming language](https://github.com/lord/slate/wiki/Multiple-language-tabs-per-programming-language) feature -- Upgrade Middleman to add Ruby 1.4.0 compatibility -- Switch default code highlighting color scheme to better highlight JSON -- Various small typo and bug fixes - -## Version 1.4 - -*November 24, 2016* - -- Upgrade Middleman and Rouge gems, should hopefully solve a number of bugs -- Update some links in README -- Fix broken Vagrant startup script -- Fix some problems with deploy.sh help message -- Fix bug with language tabs not hiding properly if no error -- Add `!default` to SASS variables -- Fix bug with logo margin -- Bump tested Ruby versions in .travis.yml - -## Version 1.3.3 - -*June 11, 2016* - -Documentation and example changes. - -## Version 1.3.2 - -*February 3, 2016* - -A small bugfix for slightly incorrect background colors on code samples in some cases. - -## Version 1.3.1 - -*January 31, 2016* - -A small bugfix for incorrect whitespace in code blocks. - -## Version 1.3 - -*January 27, 2016* - -We've upgraded Middleman and a number of other dependencies, which should fix quite a few bugs. - -Instead of `rake build` and `rake deploy`, you should now run `bundle exec middleman build --clean` to build your server, and `./deploy.sh` to deploy it to Github Pages. - -## Version 1.2 - -*June 20, 2015* - -**Fixes:** - -- Remove crash on invalid languages -- Update Tocify to scroll to the highlighted header in the Table of Contents -- Fix variable leak and update search algorithms -- Update Python examples to be valid Python -- Update gems -- More misc. bugfixes of Javascript errors -- Add Dockerfile -- Remove unused gems -- Optimize images, fonts, and generated asset files -- Add chinese font support -- Remove RedCarpet header ID patch -- Update language tabs to not disturb existing query strings - -## Version 1.1 - -*July 27, 2014* - -**Fixes:** - -- Finally, a fix for the redcarpet upgrade bug - -## Version 1.0 - -*July 2, 2014* - -[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed) - -**Features:** - -- Responsive designs for phones and tablets -- Started tagging versions - -**Fixes:** - -- Fixed 'unrecognized expression' error -- Fixed #undefined hash bug -- Fixed bug where the current language tab would be unselected -- Fixed bug where tocify wouldn't highlight the current section while searching -- Fixed bug where ids of header tags would have special characters that caused problems -- Updated layout so that pages with disabled search wouldn't load search.js -- Cleaned up Javascript diff --git a/samples/rest-notes-slate/slate/Dockerfile b/samples/rest-notes-slate/slate/Dockerfile deleted file mode 100644 index 077ef3b1..00000000 --- a/samples/rest-notes-slate/slate/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM ruby:2.6-slim - -WORKDIR /srv/slate - -EXPOSE 4567 - -COPY Gemfile . -COPY Gemfile.lock . - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential \ - git \ - nodejs \ - && gem install bundler \ - && bundle install \ - && apt-get remove -y build-essential git \ - && apt-get autoremove -y \ - && rm -rf /var/lib/apt/lists/* - -COPY . /srv/slate - -RUN chmod +x /srv/slate/slate.sh - -ENTRYPOINT ["/srv/slate/slate.sh"] -CMD ["build"] diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile deleted file mode 100644 index 7604fd42..00000000 --- a/samples/rest-notes-slate/slate/Gemfile +++ /dev/null @@ -1,13 +0,0 @@ -ruby '>= 2.6' -source 'https://rubygems.org' - -# Middleman -gem 'middleman', '~> 4.4' -gem 'middleman-syntax', '~> 3.2' -gem 'middleman-autoprefixer', '~> 3.0' -gem 'middleman-sprockets', '~> 4.1' -gem 'rouge', '~> 3.21' -gem 'redcarpet', '~> 3.5.0' -gem 'nokogiri', '~> 1.13.3' -gem 'sass' -gem 'webrick' diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock deleted file mode 100644 index bd60f7f6..00000000 --- a/samples/rest-notes-slate/slate/Gemfile.lock +++ /dev/null @@ -1,145 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (6.1.4.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) - autoprefixer-rails (10.2.5.0) - execjs (< 2.8.0) - backports (3.21.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - concurrent-ruby (1.1.9) - contracts (0.13.0) - dotenv (2.7.6) - erubis (2.7.0) - execjs (2.7.0) - fast_blank (1.0.1) - fastimage (2.2.5) - ffi (1.15.4) - haml (5.2.2) - temple (>= 0.8.0) - tilt - hamster (3.0.0) - concurrent-ruby (~> 1.0) - hashie (3.6.0) - i18n (1.6.0) - concurrent-ruby (~> 1.0) - kramdown (2.3.1) - rexml - listen (3.0.8) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - memoist (0.16.2) - middleman (4.4.2) - coffee-script (~> 2.2) - haml (>= 4.0.5) - kramdown (>= 2.3.0) - middleman-cli (= 4.4.2) - middleman-core (= 4.4.2) - middleman-autoprefixer (3.0.0) - autoprefixer-rails (~> 10.0) - middleman-core (>= 4.0.0) - middleman-cli (4.4.2) - thor (>= 0.17.0, < 2.0) - middleman-core (4.4.2) - activesupport (>= 6.1, < 7.0) - addressable (~> 2.4) - backports (~> 3.6) - bundler (~> 2.0) - contracts (~> 0.13.0) - dotenv - erubis - execjs (~> 2.0) - fast_blank - fastimage (~> 2.0) - hamster (~> 3.0) - hashie (~> 3.4) - i18n (~> 1.6.0) - listen (~> 3.0.0) - memoist (~> 0.14) - padrino-helpers (~> 0.15.0) - parallel - rack (>= 1.4.5, < 3) - sassc (~> 2.0) - servolux - tilt (~> 2.0.9) - toml - uglifier (~> 3.0) - webrick - middleman-sprockets (4.1.1) - middleman-core (~> 4.0) - sprockets (>= 3.0) - middleman-syntax (3.2.0) - middleman-core (>= 3.2) - rouge (~> 3.2) - mini_portile2 (2.8.0) - minitest (5.14.4) - nokogiri (1.13.4) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - padrino-helpers (0.15.1) - i18n (>= 0.6.7, < 2) - padrino-support (= 0.15.1) - tilt (>= 1.4.1, < 3) - padrino-support (0.15.1) - parallel (1.21.0) - parslet (2.0.0) - public_suffix (4.0.6) - racc (1.6.0) - rack (2.2.3) - rb-fsevent (0.11.0) - rb-inotify (0.10.1) - ffi (~> 1.0) - redcarpet (3.5.1) - rexml (3.2.5) - rouge (3.28.0) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sassc (2.4.0) - ffi (~> 1.9) - servolux (0.13.0) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - temple (0.8.2) - thor (1.1.0) - tilt (2.0.10) - toml (0.3.0) - parslet (>= 1.8.0, < 3.0.0) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - uglifier (3.2.0) - execjs (>= 0.3.0, < 3) - webrick (1.7.0) - zeitwerk (2.5.1) - -PLATFORMS - ruby - -DEPENDENCIES - middleman (~> 4.4) - middleman-autoprefixer (~> 3.0) - middleman-sprockets (~> 4.1) - middleman-syntax (~> 3.2) - nokogiri (~> 1.13.3) - redcarpet (~> 3.5.0) - rouge (~> 3.21) - sass - webrick - -RUBY VERSION - ruby 2.7.2p137 - -BUNDLED WITH - 2.2.22 diff --git a/samples/rest-notes-slate/slate/LICENSE b/samples/rest-notes-slate/slate/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/samples/rest-notes-slate/slate/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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 - - http://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. diff --git a/samples/rest-notes-slate/slate/README.md b/samples/rest-notes-slate/slate/README.md deleted file mode 100644 index 1560f214..00000000 --- a/samples/rest-notes-slate/slate/README.md +++ /dev/null @@ -1,82 +0,0 @@ -

    - Slate: API Documentation Generator -
    - Build Status - Docker Version -

    - -

    Slate helps you create beautiful, intelligent, responsive API documentation.

    - -

    Screenshot of Example Documentation created with Slate

    - -

    The example above was created with Slate. Check it out at slatedocs.github.io/slate.

    - -Features ------------- - -* **Clean, intuitive design** — With Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [PayPal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even in print. - -* **Everything on a single page** — Gone are the days when your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. - -* **Slate is just Markdown** — When you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks. - -* **Write code samples in multiple languages** — If your API has bindings in multiple programming languages, you can easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with GitHub Flavored Markdown. - -* **Out-of-the-box syntax highlighting** for [over 100 languages](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers), no configuration required. - -* **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents. - -* **Let your users update your documentation for you** — By default, your Slate-generated documentation is hosted in a public GitHub repository. Not only does this mean you get free hosting for your docs with GitHub Pages, but it also makes it simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to use GitHub, you're also welcome to host your docs elsewhere. - -* **RTL Support** Full right-to-left layout for RTL languages such as Arabic, Persian (Farsi), Hebrew etc. - -Getting started with Slate is super easy! Simply press the green "use this template" button above and follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](https://slatedocs.github.io/slate/). - -Getting Started with Slate ------------------------------- - -To get started with Slate, please check out the [Getting Started](https://github.com/slatedocs/slate/wiki#getting-started) -section in our [wiki](https://github.com/slatedocs/slate/wiki). - -We support running Slate in three different ways: -* [Natively](https://github.com/slatedocs/slate/wiki/Using-Slate-Natively) -* [Using Vagrant](https://github.com/slatedocs/slate/wiki/Using-Slate-in-Vagrant) -* [Using Docker](https://github.com/slatedocs/slate/wiki/Using-Slate-in-Docker) - -Companies Using Slate ---------------------------------- - -* [NASA](https://api.nasa.gov) -* [Sony](http://developers.cimediacloud.com) -* [Best Buy](https://bestbuyapis.github.io/api-documentation/) -* [Travis-CI](https://docs.travis-ci.com/api/) -* [Greenhouse](https://developers.greenhouse.io/harvest.html) -* [WooCommerce](http://woocommerce.github.io/woocommerce-rest-api-docs/) -* [Dwolla](https://docs.dwolla.com/) -* [Clearbit](https://clearbit.com/docs) -* [Coinbase](https://developers.coinbase.com/api) -* [Parrot Drones](http://developer.parrot.com/docs/bebop/) -* [CoinAPI](https://docs.coinapi.io/) - -You can view more in [the list on the wiki](https://github.com/slatedocs/slate/wiki/Slate-in-the-Wild). - -Questions? Need Help? Found a bug? --------------------- - -If you've got questions about setup, deploying, special feature implementation in your fork, or just want to chat with the developer, please feel free to [start a thread in our Discussions tab](https://github.com/slatedocs/slate/discussions)! - -Found a bug with upstream Slate? Go ahead and [submit an issue](https://github.com/slatedocs/slate/issues). And, of course, feel free to submit pull requests with bug fixes or changes to the `dev` branch. - -Contributors --------------------- - -Slate was built by [Robert Lord](https://lord.io) while at [TripIt](https://www.tripit.com/). The project is now maintained by [Matthew Peveler](https://github.com/MasterOdin) and [Mike Ralphson](https://github.com/MikeRalphson). - -Thanks to the following people who have submitted major pull requests: - -- [@chrissrogers](https://github.com/chrissrogers) -- [@bootstraponline](https://github.com/bootstraponline) -- [@realityking](https://github.com/realityking) -- [@cvkef](https://github.com/cvkef) - -Also, thanks to [Sauce Labs](http://saucelabs.com) for sponsoring the development of the responsive styles. diff --git a/samples/rest-notes-slate/slate/Vagrantfile b/samples/rest-notes-slate/slate/Vagrantfile deleted file mode 100644 index 769b8a5b..00000000 --- a/samples/rest-notes-slate/slate/Vagrantfile +++ /dev/null @@ -1,47 +0,0 @@ -Vagrant.configure(2) do |config| - config.vm.box = "ubuntu/focal64" - config.vm.network :forwarded_port, guest: 4567, host: 4567 - config.vm.provider "virtualbox" do |vb| - vb.memory = "2048" - end - - config.vm.provision "bootstrap", - type: "shell", - inline: <<-SHELL - # add nodejs v12 repository - curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - - - sudo apt-get update - sudo apt-get install -yq ruby ruby-dev - sudo apt-get install -yq pkg-config build-essential nodejs git libxml2-dev libxslt-dev - sudo apt-get autoremove -yq - gem install --no-ri --no-rdoc bundler - SHELL - - # add the local user git config to the vm - config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" - - config.vm.provision "install", - type: "shell", - privileged: false, - inline: <<-SHELL - echo "==============================================" - echo "Installing app dependencies" - cd /vagrant - sudo gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)" - bundle config build.nokogiri --use-system-libraries - bundle install - SHELL - - config.vm.provision "run", - type: "shell", - privileged: false, - run: "always", - inline: <<-SHELL - echo "==============================================" - echo "Starting up middleman at http://localhost:4567" - echo "If it does not come up, check the ~/middleman.log file for any error messages" - cd /vagrant - bundle exec middleman server --watcher-force-polling --watcher-latency=1 &> ~/middleman.log & - SHELL -end diff --git a/samples/rest-notes-slate/slate/config.rb b/samples/rest-notes-slate/slate/config.rb deleted file mode 100644 index 6f8b677f..00000000 --- a/samples/rest-notes-slate/slate/config.rb +++ /dev/null @@ -1,63 +0,0 @@ -# Unique header generation -require './lib/unique_head.rb' - -# Markdown -set :markdown_engine, :redcarpet -set :markdown, - fenced_code_blocks: true, - smartypants: true, - disable_indented_code_blocks: true, - prettify: true, - strikethrough: true, - tables: true, - with_toc_data: true, - no_intra_emphasis: true, - renderer: UniqueHeadCounter - -# Assets -set :css_dir, 'stylesheets' -set :js_dir, 'javascripts' -set :images_dir, 'images' -set :fonts_dir, 'fonts' - -# Activate the syntax highlighter -activate :syntax -ready do - require './lib/monokai_sublime_slate.rb' - require './lib/multilang.rb' -end - -activate :sprockets - -activate :autoprefixer do |config| - config.browsers = ['last 2 version', 'Firefox ESR'] - config.cascade = false - config.inline = true -end - -# Github pages require relative links -activate :relative_assets -set :relative_links, true - -# Build Configuration -configure :build do - # We do want to hash woff and woff2 as there's a bug where woff2 will use - # woff asset hash which breaks things. Trying to use a combination of ignore and - # rewrite_ignore does not work as it conflicts weirdly with relative_assets. Disabling - # the .woff2 extension only does not work as .woff will still activate it so have to - # have both. See https://github.com/slatedocs/slate/issues/1171 for more details. - activate :asset_hash, :exts => app.config[:asset_extensions] - %w[.woff .woff2] - # If you're having trouble with Middleman hanging, commenting - # out the following two lines has been known to help - activate :minify_css - activate :minify_javascript - # activate :gzip -end - -# Deploy Configuration -# If you want Middleman to listen on a different port, you can set that below -set :port, 4567 - -helpers do - require './lib/toc_data.rb' -end diff --git a/samples/rest-notes-slate/slate/deploy.sh b/samples/rest-notes-slate/slate/deploy.sh deleted file mode 100755 index 9dbd7db9..00000000 --- a/samples/rest-notes-slate/slate/deploy.sh +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env bash -set -o errexit #abort if any command fails -me=$(basename "$0") - -help_message="\ -Usage: $me [-c FILE] [] -Deploy generated files to a git branch. - -Options: - - -h, --help Show this help information. - -v, --verbose Increase verbosity. Useful for debugging. - -e, --allow-empty Allow deployment of an empty directory. - -m, --message MESSAGE Specify the message used when committing on the - deploy branch. - -n, --no-hash Don't append the source commit's hash to the deploy - commit's message. - --source-only Only build but not push - --push-only Only push but not build -" - - -run_build() { - bundle exec middleman build --clean -} - -parse_args() { - # Set args from a local environment file. - if [ -e ".env" ]; then - source .env - fi - - # Parse arg flags - # If something is exposed as an environment variable, set/overwrite it - # here. Otherwise, set/overwrite the internal variable instead. - while : ; do - if [[ $1 = "-h" || $1 = "--help" ]]; then - echo "$help_message" - exit 0 - elif [[ $1 = "-v" || $1 = "--verbose" ]]; then - verbose=true - shift - elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then - allow_empty=true - shift - elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then - commit_message=$2 - shift 2 - elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then - GIT_DEPLOY_APPEND_HASH=false - shift - elif [[ $1 = "--source-only" ]]; then - source_only=true - shift - elif [[ $1 = "--push-only" ]]; then - push_only=true - shift - else - break - fi - done - - if [ ${source_only} ] && [ ${push_only} ]; then - >&2 echo "You can only specify one of --source-only or --push-only" - exit 1 - fi - - # Set internal option vars from the environment and arg flags. All internal - # vars should be declared here, with sane defaults if applicable. - - # Source directory & target branch. - deploy_directory=build - deploy_branch=gh-pages - - #if no user identity is already set in the current git environment, use this: - default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} - default_email=${GIT_DEPLOY_EMAIL:-} - - #repository to deploy to. must be readable and writable. - repo=origin - - #append commit hash to the end of message by default - append_hash=${GIT_DEPLOY_APPEND_HASH:-true} -} - -main() { - enable_expanded_output - - if ! git diff --exit-code --quiet --cached; then - echo Aborting due to uncommitted changes in the index >&2 - return 1 - fi - - commit_title=`git log -n 1 --format="%s" HEAD` - commit_hash=` git log -n 1 --format="%H" HEAD` - - #default commit message uses last title if a custom one is not supplied - if [[ -z $commit_message ]]; then - commit_message="publish: $commit_title" - fi - - #append hash to commit message unless no hash flag was found - if [ $append_hash = true ]; then - commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" - fi - - previous_branch=`git rev-parse --abbrev-ref HEAD` - - if [ ! -d "$deploy_directory" ]; then - echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 - return 1 - fi - - # must use short form of flag in ls for compatibility with macOS and BSD - if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then - echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 - return 1 - fi - - if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then - # deploy_branch exists in $repo; make sure we have the latest version - - disable_expanded_output - git fetch --force $repo $deploy_branch:$deploy_branch - enable_expanded_output - fi - - # check if deploy_branch exists locally - if git show-ref --verify --quiet "refs/heads/$deploy_branch" - then incremental_deploy - else initial_deploy - fi - - restore_head -} - -initial_deploy() { - git --work-tree "$deploy_directory" checkout --orphan $deploy_branch - git --work-tree "$deploy_directory" add --all - commit+push -} - -incremental_deploy() { - #make deploy_branch the current branch - git symbolic-ref HEAD refs/heads/$deploy_branch - #put the previously committed contents of deploy_branch into the index - git --work-tree "$deploy_directory" reset --mixed --quiet - git --work-tree "$deploy_directory" add --all - - set +o errexit - diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? - set -o errexit - case $diff in - 0) echo No changes to files in $deploy_directory. Skipping commit.;; - 1) commit+push;; - *) - echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to main, use: git symbolic-ref HEAD refs/heads/main && git reset --mixed >&2 - return $diff - ;; - esac -} - -commit+push() { - set_user_id - git --work-tree "$deploy_directory" commit -m "$commit_message" - - disable_expanded_output - #--quiet is important here to avoid outputting the repo URL, which may contain a secret token - git push --quiet $repo $deploy_branch - enable_expanded_output -} - -#echo expanded commands as they are executed (for debugging) -enable_expanded_output() { - if [ $verbose ]; then - set -o xtrace - set +o verbose - fi -} - -#this is used to avoid outputting the repo URL, which may contain a secret token -disable_expanded_output() { - if [ $verbose ]; then - set +o xtrace - set -o verbose - fi -} - -set_user_id() { - if [[ -z `git config user.name` ]]; then - git config user.name "$default_username" - fi - if [[ -z `git config user.email` ]]; then - git config user.email "$default_email" - fi -} - -restore_head() { - if [[ $previous_branch = "HEAD" ]]; then - #we weren't on any branch before, so just set HEAD back to the commit it was on - git update-ref --no-deref HEAD $commit_hash $deploy_branch - else - git symbolic-ref HEAD refs/heads/$previous_branch - fi - - git reset --mixed -} - -filter() { - sed -e "s|$repo|\$repo|g" -} - -sanitize() { - "$@" 2> >(filter 1>&2) | filter -} - -parse_args "$@" - -if [[ ${source_only} ]]; then - run_build -elif [[ ${push_only} ]]; then - main "$@" -else - run_build - main "$@" -fi diff --git a/samples/rest-notes-slate/slate/font-selection.json b/samples/rest-notes-slate/slate/font-selection.json deleted file mode 100755 index 5e78f5d8..00000000 --- a/samples/rest-notes-slate/slate/font-selection.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "IcoMoonType": "selection", - "icons": [ - { - "icon": { - "paths": [ - "M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "exclamation-circle" - ], - "defaultCode": 61546, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 100, - "order": 4, - "prevSize": 28, - "code": 58880, - "name": "exclamation-sign", - "ligatures": "" - }, - "setIdx": 0, - "iconIdx": 0 - }, - { - "icon": { - "paths": [ - "M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "info-circle" - ], - "defaultCode": 61530, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 85, - "order": 3, - "name": "info-sign", - "prevSize": 28, - "code": 58882 - }, - "setIdx": 0, - "iconIdx": 2 - }, - { - "icon": { - "paths": [ - "M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "check-circle" - ], - "defaultCode": 61528, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 83, - "order": 9, - "prevSize": 28, - "code": 58886, - "name": "ok-sign" - }, - "setIdx": 0, - "iconIdx": 6 - }, - { - "icon": { - "paths": [ - "M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z" - ], - "width": 951, - "attrs": [], - "isMulticolor": false, - "tags": [ - "search" - ], - "defaultCode": 61442, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 2, - "order": 1, - "prevSize": 28, - "code": 58887, - "name": "icon-search" - }, - "setIdx": 0, - "iconIdx": 7 - } - ], - "height": 1024, - "metadata": { - "name": "slate", - "license": "SIL OFL 1.1" - }, - "preferences": { - "showGlyphs": true, - "showQuickUse": true, - "showQuickUse2": true, - "showSVGs": true, - "fontPref": { - "prefix": "icon-", - "metadata": { - "fontFamily": "slate", - "majorVersion": 1, - "minorVersion": 0, - "description": "Based on FontAwesome", - "license": "SIL OFL 1.1" - }, - "metrics": { - "emSize": 1024, - "baseline": 6.25, - "whitespace": 50 - }, - "resetPoint": 58880, - "showSelector": false, - "selector": "class", - "classSelector": ".icon", - "showMetrics": false, - "showMetadata": true, - "showVersion": true, - "ie7": false - }, - "imagePref": { - "prefix": "icon-", - "png": true, - "useClassSelector": true, - "color": 4473924, - "bgColor": 16777215 - }, - "historySize": 100, - "showCodes": true, - "gridSize": 16, - "showLiga": false - } -} diff --git a/samples/rest-notes-slate/slate/lib/monokai_sublime_slate.rb b/samples/rest-notes-slate/slate/lib/monokai_sublime_slate.rb deleted file mode 100644 index cd2de331..00000000 --- a/samples/rest-notes-slate/slate/lib/monokai_sublime_slate.rb +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- # -# frozen_string_literal: true - -# this is based on https://github.com/rouge-ruby/rouge/blob/master/lib/rouge/themes/monokai_sublime.rb -# but without the added background, and changed styling for JSON keys to be soft_yellow instead of white - -module Rouge - module Themes - class MonokaiSublimeSlate < CSSTheme - name 'monokai.sublime.slate' - - palette :black => '#000000' - palette :bright_green => '#a6e22e' - palette :bright_pink => '#f92672' - palette :carmine => '#960050' - palette :dark => '#49483e' - palette :dark_grey => '#888888' - palette :dark_red => '#aa0000' - palette :dimgrey => '#75715e' - palette :emperor => '#555555' - palette :grey => '#999999' - palette :light_grey => '#aaaaaa' - palette :light_violet => '#ae81ff' - palette :soft_cyan => '#66d9ef' - palette :soft_yellow => '#e6db74' - palette :very_dark => '#1e0010' - palette :whitish => '#f8f8f2' - palette :orange => '#f6aa11' - palette :white => '#ffffff' - - style Generic::Heading, :fg => :grey - style Literal::String::Regex, :fg => :orange - style Generic::Output, :fg => :dark_grey - style Generic::Prompt, :fg => :emperor - style Generic::Strong, :bold => false - style Generic::Subheading, :fg => :light_grey - style Name::Builtin, :fg => :orange - style Comment::Multiline, - Comment::Preproc, - Comment::Single, - Comment::Special, - Comment, :fg => :dimgrey - style Error, - Generic::Error, - Generic::Traceback, :fg => :carmine - style Generic::Deleted, - Generic::Inserted, - Generic::Emph, :fg => :dark - style Keyword::Constant, - Keyword::Declaration, - Keyword::Reserved, - Name::Constant, - Keyword::Type, :fg => :soft_cyan - style Literal::Number::Float, - Literal::Number::Hex, - Literal::Number::Integer::Long, - Literal::Number::Integer, - Literal::Number::Oct, - Literal::Number, - Literal::String::Char, - Literal::String::Escape, - Literal::String::Symbol, :fg => :light_violet - style Literal::String::Doc, - Literal::String::Double, - Literal::String::Backtick, - Literal::String::Heredoc, - Literal::String::Interpol, - Literal::String::Other, - Literal::String::Single, - Literal::String, :fg => :soft_yellow - style Name::Attribute, - Name::Class, - Name::Decorator, - Name::Exception, - Name::Function, :fg => :bright_green - style Name::Variable::Class, - Name::Namespace, - Name::Entity, - Name::Builtin::Pseudo, - Name::Variable::Global, - Name::Variable::Instance, - Name::Variable, - Text::Whitespace, - Text, - Name, :fg => :white - style Name::Label, :fg => :bright_pink - style Operator::Word, - Name::Tag, - Keyword, - Keyword::Namespace, - Keyword::Pseudo, - Operator, :fg => :bright_pink - end - end - end diff --git a/samples/rest-notes-slate/slate/lib/multilang.rb b/samples/rest-notes-slate/slate/lib/multilang.rb deleted file mode 100644 index 36fbe5b1..00000000 --- a/samples/rest-notes-slate/slate/lib/multilang.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Multilang - def block_code(code, full_lang_name) - if full_lang_name - parts = full_lang_name.split('--') - rouge_lang_name = (parts) ? parts[0] : "" # just parts[0] here causes null ref exception when no language specified - super(code, rouge_lang_name).sub("highlight #{rouge_lang_name}") do |match| - match + " tab-" + full_lang_name - end - else - super(code, full_lang_name) - end - end -end - -require 'middleman-core/renderers/redcarpet' -Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, Multilang diff --git a/samples/rest-notes-slate/slate/lib/nesting_unique_head.rb b/samples/rest-notes-slate/slate/lib/nesting_unique_head.rb deleted file mode 100644 index 01278371..00000000 --- a/samples/rest-notes-slate/slate/lib/nesting_unique_head.rb +++ /dev/null @@ -1,22 +0,0 @@ -# Nested unique header generation -require 'middleman-core/renderers/redcarpet' - -class NestingUniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML - def initialize - super - @@headers_history = {} if !defined?(@@headers_history) - end - - def header(text, header_level) - friendly_text = text.gsub(/<[^>]*>/,"").parameterize - @@headers_history[header_level] = text.parameterize - - if header_level > 1 - for i in (header_level - 1).downto(1) - friendly_text.prepend("#{@@headers_history[i]}-") if @@headers_history.key?(i) - end - end - - return "#{text}" - end -end diff --git a/samples/rest-notes-slate/slate/lib/toc_data.rb b/samples/rest-notes-slate/slate/lib/toc_data.rb deleted file mode 100644 index 4a04efee..00000000 --- a/samples/rest-notes-slate/slate/lib/toc_data.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'nokogiri' - -def toc_data(page_content) - html_doc = Nokogiri::HTML::DocumentFragment.parse(page_content) - - # get a flat list of headers - headers = [] - html_doc.css('h1, h2, h3').each do |header| - headers.push({ - id: header.attribute('id').to_s, - content: header.children, - title: header.children.to_s.gsub(/<[^>]*>/, ''), - level: header.name[1].to_i, - children: [] - }) - end - - [3,2].each do |header_level| - header_to_nest = nil - headers = headers.reject do |header| - if header[:level] == header_level - header_to_nest[:children].push header if header_to_nest - true - else - header_to_nest = header if header[:level] < header_level - false - end - end - end - headers -end diff --git a/samples/rest-notes-slate/slate/lib/unique_head.rb b/samples/rest-notes-slate/slate/lib/unique_head.rb deleted file mode 100644 index d42bab2a..00000000 --- a/samples/rest-notes-slate/slate/lib/unique_head.rb +++ /dev/null @@ -1,24 +0,0 @@ -# Unique header generation -require 'middleman-core/renderers/redcarpet' -require 'digest' -class UniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML - def initialize - super - @head_count = {} - end - def header(text, header_level) - friendly_text = text.gsub(/<[^>]*>/,"").parameterize - if friendly_text.strip.length == 0 - # Looks like parameterize removed the whole thing! It removes many unicode - # characters like Chinese and Russian. To get a unique URL, let's just - # URI escape the whole header - friendly_text = Digest::SHA1.hexdigest(text)[0,10] - end - @head_count[friendly_text] ||= 0 - @head_count[friendly_text] += 1 - if @head_count[friendly_text] > 1 - friendly_text += "-#{@head_count[friendly_text]}" - end - return "#{text}" - end -end diff --git a/samples/rest-notes-slate/slate/slate.sh b/samples/rest-notes-slate/slate/slate.sh deleted file mode 100755 index a3cc498e..00000000 --- a/samples/rest-notes-slate/slate/slate.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env bash -set -o errexit #abort if any command fails - -me=$(basename "$0") - -help_message="\ -Usage: $me [] [] -Run commands related to the slate process. - -Commands: - - serve Run the middleman server process, useful for - development. - build Run the build process. - deploy Will build and deploy files to branch. Use - --no-build to only deploy. - -Global Options: - - -h, --help Show this help information. - -v, --verbose Increase verbosity. Useful for debugging. - -Deploy options: - -e, --allow-empty Allow deployment of an empty directory. - -m, --message MESSAGE Specify the message used when committing on the - deploy branch. - -n, --no-hash Don't append the source commit's hash to the deploy - commit's message. - --no-build Do not build the source files. -" - - -run_serve() { - exec bundle exec middleman serve --watcher-force-polling -} - -run_build() { - bundle exec middleman build --clean -} - -parse_args() { - # Set args from a local environment file. - if [ -e ".env" ]; then - source .env - fi - - command= - - # Parse arg flags - # If something is exposed as an environment variable, set/overwrite it - # here. Otherwise, set/overwrite the internal variable instead. - while : ; do - if [[ $1 = "-h" || $1 = "--help" ]]; then - echo "$help_message" - exit 0 - elif [[ $1 = "-v" || $1 = "--verbose" ]]; then - verbose=true - shift - elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then - allow_empty=true - shift - elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then - commit_message=$2 - shift 2 - elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then - GIT_DEPLOY_APPEND_HASH=false - shift - elif [[ $1 = "--no-build" ]]; then - no_build=true - shift - elif [[ $1 = "serve" || $1 = "build" || $1 = "deploy" ]]; then - if [ ! -z "${command}" ]; then - >&2 echo "You can only specify one command." - exit 1 - fi - command=$1 - shift - elif [ -z $1 ]; then - break - fi - done - - if [ -z "${command}" ]; then - >&2 echo "Command not specified." - exit 1 - fi - - # Set internal option vars from the environment and arg flags. All internal - # vars should be declared here, with sane defaults if applicable. - - # Source directory & target branch. - deploy_directory=build - deploy_branch=gh-pages - - #if no user identity is already set in the current git environment, use this: - default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} - default_email=${GIT_DEPLOY_EMAIL:-} - - #repository to deploy to. must be readable and writable. - repo=origin - - #append commit hash to the end of message by default - append_hash=${GIT_DEPLOY_APPEND_HASH:-true} -} - -main() { - enable_expanded_output - - if ! git diff --exit-code --quiet --cached; then - echo Aborting due to uncommitted changes in the index >&2 - return 1 - fi - - commit_title=`git log -n 1 --format="%s" HEAD` - commit_hash=` git log -n 1 --format="%H" HEAD` - - #default commit message uses last title if a custom one is not supplied - if [[ -z $commit_message ]]; then - commit_message="publish: $commit_title" - fi - - #append hash to commit message unless no hash flag was found - if [ $append_hash = true ]; then - commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" - fi - - previous_branch=`git rev-parse --abbrev-ref HEAD` - - if [ ! -d "$deploy_directory" ]; then - echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 - return 1 - fi - - # must use short form of flag in ls for compatibility with macOS and BSD - if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then - echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 - return 1 - fi - - if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then - # deploy_branch exists in $repo; make sure we have the latest version - - disable_expanded_output - git fetch --force $repo $deploy_branch:$deploy_branch - enable_expanded_output - fi - - # check if deploy_branch exists locally - if git show-ref --verify --quiet "refs/heads/$deploy_branch" - then incremental_deploy - else initial_deploy - fi - - restore_head -} - -initial_deploy() { - git --work-tree "$deploy_directory" checkout --orphan $deploy_branch - git --work-tree "$deploy_directory" add --all - commit+push -} - -incremental_deploy() { - #make deploy_branch the current branch - git symbolic-ref HEAD refs/heads/$deploy_branch - #put the previously committed contents of deploy_branch into the index - git --work-tree "$deploy_directory" reset --mixed --quiet - git --work-tree "$deploy_directory" add --all - - set +o errexit - diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? - set -o errexit - case $diff in - 0) echo No changes to files in $deploy_directory. Skipping commit.;; - 1) commit+push;; - *) - echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to main, use: git symbolic-ref HEAD refs/heads/main && git reset --mixed >&2 - return $diff - ;; - esac -} - -commit+push() { - set_user_id - git --work-tree "$deploy_directory" commit -m "$commit_message" - - disable_expanded_output - #--quiet is important here to avoid outputting the repo URL, which may contain a secret token - git push --quiet $repo $deploy_branch - enable_expanded_output -} - -#echo expanded commands as they are executed (for debugging) -enable_expanded_output() { - if [ $verbose ]; then - set -o xtrace - set +o verbose - fi -} - -#this is used to avoid outputting the repo URL, which may contain a secret token -disable_expanded_output() { - if [ $verbose ]; then - set +o xtrace - set -o verbose - fi -} - -set_user_id() { - if [[ -z `git config user.name` ]]; then - git config user.name "$default_username" - fi - if [[ -z `git config user.email` ]]; then - git config user.email "$default_email" - fi -} - -restore_head() { - if [[ $previous_branch = "HEAD" ]]; then - #we weren't on any branch before, so just set HEAD back to the commit it was on - git update-ref --no-deref HEAD $commit_hash $deploy_branch - else - git symbolic-ref HEAD refs/heads/$previous_branch - fi - - git reset --mixed -} - -filter() { - sed -e "s|$repo|\$repo|g" -} - -sanitize() { - "$@" 2> >(filter 1>&2) | filter -} - -parse_args "$@" - -if [ "${command}" = "serve" ]; then - run_serve -elif [[ "${command}" = "build" ]]; then - run_build -elif [[ ${command} = "deploy" ]]; then - if [[ ${no_build} != true ]]; then - run_build - fi - main "$@" -fi diff --git a/samples/rest-notes-slate/slate/source/api-guide.html.md.erb b/samples/rest-notes-slate/slate/source/api-guide.html.md.erb deleted file mode 100644 index 77dab522..00000000 --- a/samples/rest-notes-slate/slate/source/api-guide.html.md.erb +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: REST Notes API Guide - -language_tabs: - - shell - - http - -search: true ---- - -# Overview - - -## HTTP verbs - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs. - - -Verb | Usage --------- | ----- -`GET` | Used to retrieve a resource -`POST` | Used to create a new resource -`PATCH` | Used to update an existing resource, including partial updates -`DELETE` | Used to delete an existing resource - -## HTTP status codes - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes. - -Status code | Usage ------------------ | ----- -`200 OK` | The request completed successfully -`201 Created` | A new resource has been created successfully. The resource's URI is available from the response's `Location` header -`204 No Content` | An update to an existing resource has been applied successfully -`400 Bad Request` | The request was malformed. The response body will include an error providing further information -`404 Not Found` | The requested resource did not exist - -## Errors - -<%= ERB.new(File.read("../build/generated-snippets/error-example/http-response.md")).result(binding) %> - -Whenever an error response (status code >= 400) is returned, the body will contain a JSON object -that describes the problem. The error object has the following structure: - -<%= ERB.new(File.read("../build/generated-snippets/error-example/response-fields.md")).result(binding) %> - -## Hypermedia - -RESTful Notes uses hypermedia and resources include links to other resources in their -responses. Responses are in [Hypertext Application Language (HAL)](https://github.com/mikekelly/hal_specification) -format. Links can be found beneath the `_links` key. Users of the API should not create -URIs themselves, instead they should use the above-described links to navigate - - - -# Resources - - - -## Index - -<%= ERB.new(File.read("../build/generated-snippets/index-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/index-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/index-example/http-response.md")).result(binding) %> - -The index provides the entry point into the service. A `GET` request is used to access the index. - -### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/index-example/response-fields.md")).result(binding) %> - -### Links - -<%= ERB.new(File.read("../build/generated-snippets/index-example/links.md")).result(binding) %> - - - -## Notes - -The Notes resources is used to create and list notes - -### Listing notes - -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-response.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/curl-request.md")).result(binding) %> - -A `GET` request will list all of the service's notes. - -#### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/response-fields.md")).result(binding) %> - -### Creating a note - -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/curl-request.md")).result(binding) %> - -A `POST` request is used to create a note - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/request-fields.md")).result(binding) %> - - - -## Tags - -The Tags resource is used to create and list tags. - -### Listing tags - -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-response.md")).result(binding) %> - -A `GET` request will list all of the service's tags. - -#### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/response-fields.md")).result(binding) %> - - -### Creating a tag - -<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> - -A `POST` request is used to create a tag - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/request-fields.md")).result(binding) %> - - - -## Note - -The Note resource is used to retrieve, update, and delete individual notes - -### Retrieve a note - -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-response.md")).result(binding) %> - -A `GET` request will retrieve the details of a note - -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/response-fields.md")).result(binding) %> - -#### Links - -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/links.md")).result(binding) %> - -### Update a note - -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-response.md")).result(binding) %> - -A `PATCH` request is used to update a note - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/request-fields.md")).result(binding) %> - -To leave an attribute of a note unchanged, any of the above may be omitted from the -request. - - - -## Tag - -The Tag resource is used to retrieve, update, and delete individual tags - -### Retrieve a tag - -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-response.md")).result(binding) %> - -A `GET` request will retrieve the details of a tag - -#### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/response-fields.md")).result(binding) %> - -#### Links - -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/links.md")).result(binding) %> - -### Update a tag - -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-response.md")).result(binding) %> - -A `PATCH` request is used to update a tag - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/request-fields.md")).result(binding) %> \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.eot b/samples/rest-notes-slate/slate/source/fonts/slate.eot deleted file mode 100644 index 13c4839a1975d4c92d66753d75553f922743c6ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1876 zcmaJ?&2Jl35TEDWeRg&sP8)xqh$Kr)Yzaiu#`dnNaHtS!Xx9i53LF}#nAmmP`Xfq= z;s{Zte*qCEK;i~h4hYFPaA-x4sD#vVrBXQ{MIaD@1Ke8kn|8tnQ^fdI9N^x@? zd<=RUJW{D`U;b-v<0kYSA}zO8E|&DaCqF0BzlPme0}$TUZbAP8`m<~GR{K~75*dg= zcCEQu4DE@ZpmS=+>&5muJwb0nf0^x#V!izR7X1vpggLV7&CM3_1j&!tPMS_)mkgrN zC!rsJe5kniow8zt{RT+zltXle=pd}!=-!|+8X9a|iyqm&z#GOh#?Z4hMmoI$K1va6 zrUYgm&_U=R+`ZrJ0!LQ9E`42ef0@uHvC8U zo}w4%B?O&MCX$JGEG)w^HIqqa()pb0xK4IFpUb457c*fwDPqaQf|z%md}h1{#>ac0 zD>_@{@&c$_-fEYWRBE3yj7U8q4MTz%hA^cOxdwei%19MlXpM(P_)+e=@Rm}B>tXap zwoqO*DogaoKF+^`!zvX>{f10 zq*F6dk@0&Ok4=k2cHR|EKKaOn+_Q3KG-~~J-9n!;&D+d{W9=SQ|IqlDm9?y2uUl{( zi(0o$Q@Cby<g+B7U zE7gMM6{=S}Ps}C~lhg72%h#4X&vB-0d)je4Z)w>(?>M%-xE~jH;l@L}Lcyy(n4O9z z65lS`w&R>XbW_^$2bKN!lz(S%N3N!tcP>R={D&-^3rjyfS-aWi!AkH>sk+00G5&qW zC1%n(1Gmpd;5$IPUF_Lw@K%&WbbxEX?LgKcF9x!K$J`8LiMQ^#KsG5yOZ=|rBS1K&l2uG4tC&hwF_o-hDp_Le zTrgI}?0+pse - - -Generated by IcoMoon - - - - - - - - - - diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.ttf b/samples/rest-notes-slate/slate/source/fonts/slate.ttf deleted file mode 100644 index ace9a46a7e1ed6b6ab3de2f30ef3e27c572f88d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1720 zcmaJ>-D_KA7=PaL@t*V?ZMro%kz(6pV-rwZyCyk@Q7?wb>UJv9`gO6wj7^qiCTUqB zO%X@;7dX69!8^V6LWJzXiw!F%45qgVdLa}=5xvm6E&iVKo-9$jJ@CHI_viaTf`}a2 zC!H2wcyVDVd0amU$>&(FZ8pn0bm7yth{U7dH)`ef4)6r{E^wmO*t_`0^~*QG?-S|8 zt!lYq{5ky*k?|Sy{uTt*p8hrX-@re<)$DYS^+1t{800m!H_O^}@g4X@@W-3w?hZXf zuY!M;^{sNV`qeJ|2)=?Gg`Mqo2XzAEd#oqjAaRXMBJF+c79{T|EPkbe7-PE;5S;Q~ zaGL1Q(r@%{&}khDI-bPX!~GUGz-0@x9Aaiik?BxrHq?#(PnWnI%nYaReOv*$ZSm>?)ctla|1hAG;T1^YPnfOv{N&_J8ekcvoG^Cm?MLpzb znO-8ASEU{s)D{<<&JKz%JjTWAM|EWWzjHMajT;ECdZ?bsKw&|^tZONrkvOuIkI%De zUYTtG_26}0jYK0>4*B1QgBuPLXU?}t^*TiboK|r`R>P0_HD+(cdi{Ze{FKYDLBs0R~?v6B%Rx~Edo2aasT@IJ-vtfG(iE^ z$Awv_E5l{^C4s84a|;3+->t#z!u^V-9Nj!@+Ph(RslFP9tMyA^DCS*vdNzG<@yc2l z`u?ov&H8>AuC0gXeBbj{4$|U#n6XQ^x*FE+&d;P>_lp(J^Zj%8%oMl&cI_ZN6TKO{ zkvFp2-&{yO{TDd~50<`txN&oc<4*8TskuV~pXj~g5i{t$k=GYVU^@bQTx>a5uvcK? zADE$i`dge4A3((KH9;?{zv*7K*f>Jt^humckR5yQeHf=xv0R7Ti)jP&N=%#Ng5wPM z_VCv|5z{smX^sCCV+<0Gsc02b(JH2*RZK;zn2MJ0U5I^E%r-TsOdYDyD_EDQO?BF) z7OFc*Cuk9TtZz5Uo$8RKb(q)a%}C8|gD__z_YMNoV9|<#sst6tUZ*|mXK52w&tq|_ J6Wq-M;U6NW`v(93 diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.woff b/samples/rest-notes-slate/slate/source/fonts/slate.woff deleted file mode 100644 index 1e72e0ee0018119d7c814c2097cc60c0bcd05d84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1796 zcmaJ>&2Jl35TCanZ=GF;-Nqj%Qj#SmwgjS~vAs(b4i$%lc7v*fuR|jhlemtH?Nqf< z5&;$c3y8RZ#0{p83^QtGf$~^ZU#vdPL3b z4fcBr_DN>rKz!G#?&4f~pM4ZRM6a}~ts1aTadwIM%N_dh>UO7#K7YRFXF@YA68l_? z@xFm7>0K?wZ&VUvut!OxMlIIQ5*<0_&Hha~Yl49Y@PK@!7+CqFG*;eClSR)#j$=Xw zNnrjF9T`VX|4zRT99<||DqHk_nzSa(NzO5voBad{L?lOWoE4r?ZbRP(V_X@TZL>{} z(3A8mk}l-3xojrwNJr*pi-lsLQVxSKC{0w##ljO}){#>poy#tYg)pcTCk9|L<3To?f*omEO$b9ODUa}gVj!a zAvUB1l6OCpmTg;7PgnO)phbF-Xik@UVo+OLa3((}zVs*;Zywi?{r}GOL=0{q1ou!q ztD%;HAbGE?Z5HC#RzAMSTXWiN9ioS*i+Usm@#fI}eK@$`F!8DQHtAj`iEnm!UKH}P zNl{d*%%o>TwzLq6ppv_9BR_a$H<|Q)z2RXkyY6k4BJlQ)o4+xU@=Bif%MA~%sib$? zbw%hV*Y96nzi0MvpHdWZeO#D>x^i4rP!XsqKRYk5@ZB2RF5E9QWp(qg81F^VmBvaG ztu(Ggk(kS7r)DyTm#?ozQ4q}d{!9==(dt@sJ_vk&`7k5ChZ~1PD=Sgs?%Z@HoBe#* z_k-a4JvVKwyWWZ=q2@1#9uk~9EfrHBtA=!8%MC` zIGu@c6SyDi7WCym@ z2-w*rE$c_UfviKEL=Vy>={0bo+(i5#P1q3$6VB``hVMrCzU@B6vbqKcPy}m-BtQn- zkylU>byNe6DE>18g5d9bm%bZqa|qF=7&3(|U!AY)%Yg-vOsqKIq$w0D$r*3}ks4=+ z>U6?w2xMd=B$;zQ9v)F1K7KwfQ9|)<9nl&i?6-rchs{I8LC1eK^@#(|^QwB-{VeQz zoHLn@cwG?Yo(X!F(Q8iBOqSNs${h}C*?qJYLuZAnVXqx{ww|}Ux0`ca^V#hz?c7{r z7ldYbmk!M1m3G+IYC6(!VcCzqp&pJkZ#1@kw2W<^EJMqOM;^&dq6ELC*|pK!(cC0< zbZtF%BzLsuTw5O*+eg;dX0a+hcfI=gj8a~qWYJoMamHAo)!Qq|6&~9S8`{d0T?L&% z@$R=P6H0rTp6V+-Dlb>EB67JEMQ>kk%`y}{hvBC(RNEw)*%|U|4>SL+B=3rE@y*rw zR_bLZr&~XN?C;+yx2IilcDnrb?M)}An;DsTe@^rq$9L-UWNqY_oTyWyYisN3CMIh; z*C)b12exYgK^2c4S}J#2YJ+00rUlj%+LqQ7E%lldEc`K|1Bky)iLkwF{Wltvj!{A+PBEzhE3BX) zFBgqmj@&z?N1*=OK?z54T7is8FiA(N66oe`X+cKl>`n&&000000HC;3 zk^ug-#56-JW5JAtMV6Rgj#+|9A(7;@^`+^dWy@Alt7D86w|;R>QMs>d+47HJVfKhH aD;vc&%m&dlj4($-ipx}q&#}A+00017%3vV? diff --git a/samples/rest-notes-slate/slate/source/images/logo.png b/samples/rest-notes-slate/slate/source/images/logo.png deleted file mode 100644 index 68a478fae3611d878af18c575cf8218b818f41db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22317 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
    hg5Z+!{Sa)O9QNxi$tfwtDZos~b$g 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 F`9JqINYVfR diff --git a/samples/rest-notes-slate/slate/source/images/navbar.png b/samples/rest-notes-slate/slate/source/images/navbar.png deleted file mode 100644 index df38e90d87e1a215371b4977e18cde90f8832537..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk@BpAX3RW*PVQ%R6tFatx` -This error section is stored in a separate file in includes/_errors.md. Slate allows you to optionally separate out your docs into many files...just save them to the includes folder and add them to the top of your index.md's frontmatter. Files are included in the order listed. - - -The Kittn API uses the following error codes: - - -Error Code | Meaning ----------- | ------- -400 | Bad Request -- Your request is invalid. -401 | Unauthorized -- Your API key is wrong. -403 | Forbidden -- The kitten requested is hidden for administrators only. -404 | Not Found -- The specified kitten could not be found. -405 | Method Not Allowed -- You tried to access a kitten with an invalid method. -406 | Not Acceptable -- You requested a format that isn't json. -410 | Gone -- The kitten requested has been removed from our servers. -418 | I'm a teapot. -429 | Too Many Requests -- You're requesting too many kittens! Slow down! -500 | Internal Server Error -- We had a problem with our server. Try again later. -503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. diff --git a/samples/rest-notes-slate/slate/source/index.html.md b/samples/rest-notes-slate/slate/source/index.html.md deleted file mode 100644 index db246274..00000000 --- a/samples/rest-notes-slate/slate/source/index.html.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: API Reference - -language_tabs: # must be one of https://git.io/vQNgJ - - shell - - ruby - - python - - javascript - -toc_footers: - - Sign Up for a Developer Key - - Documentation Powered by Slate - -includes: - - errors - -search: true - -code_clipboard: true - -meta: - - name: description - content: Documentation for the Kittn API ---- - -# Introduction - -Welcome to the Kittn API! You can use our API to access Kittn API endpoints, which can get information on various cats, kittens, and breeds in our database. - -We have language bindings in Shell, Ruby, Python, and JavaScript! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right. - -This example API documentation page was created with [Slate](https://github.com/slatedocs/slate). Feel free to edit it and use it as a base for your own API's documentation. - -# Authentication - -> To authorize, use this code: - -```ruby -require 'kittn' - -api = Kittn::APIClient.authorize!('meowmeowmeow') -``` - -```python -import kittn - -api = kittn.authorize('meowmeowmeow') -``` - -```shell -# With shell, you can just pass the correct header with each request -curl "api_endpoint_here" \ - -H "Authorization: meowmeowmeow" -``` - -```javascript -const kittn = require('kittn'); - -let api = kittn.authorize('meowmeowmeow'); -``` - -> Make sure to replace `meowmeowmeow` with your API key. - -Kittn uses API keys to allow access to the API. You can register a new Kittn API key at our [developer portal](http://example.com/developers). - -Kittn expects for the API key to be included in all API requests to the server in a header that looks like the following: - -`Authorization: meowmeowmeow` - - - -# Kittens - -## Get All Kittens - -```ruby -require 'kittn' - -api = Kittn::APIClient.authorize!('meowmeowmeow') -api.kittens.get -``` - -```python -import kittn - -api = kittn.authorize('meowmeowmeow') -api.kittens.get() -``` - -```shell -curl "http://example.com/api/kittens" \ - -H "Authorization: meowmeowmeow" -``` - -```javascript -const kittn = require('kittn'); - -let api = kittn.authorize('meowmeowmeow'); -let kittens = api.kittens.get(); -``` - -> The above command returns JSON structured like this: - -```json -[ - { - "id": 1, - "name": "Fluffums", - "breed": "calico", - "fluffiness": 6, - "cuteness": 7 - }, - { - "id": 2, - "name": "Max", - "breed": "unknown", - "fluffiness": 5, - "cuteness": 10 - } -] -``` - -This endpoint retrieves all kittens. - -### HTTP Request - -`GET http://example.com/api/kittens` - -### Query Parameters - -Parameter | Default | Description ---------- | ------- | ----------- -include_cats | false | If set to true, the result will also include cats. -available | true | If set to false, the result will include kittens that have already been adopted. - - - -## Get a Specific Kitten - -```ruby -require 'kittn' - -api = Kittn::APIClient.authorize!('meowmeowmeow') -api.kittens.get(2) -``` - -```python -import kittn - -api = kittn.authorize('meowmeowmeow') -api.kittens.get(2) -``` - -```shell -curl "http://example.com/api/kittens/2" \ - -H "Authorization: meowmeowmeow" -``` - -```javascript -const kittn = require('kittn'); - -let api = kittn.authorize('meowmeowmeow'); -let max = api.kittens.get(2); -``` - -> The above command returns JSON structured like this: - -```json -{ - "id": 2, - "name": "Max", - "breed": "unknown", - "fluffiness": 5, - "cuteness": 10 -} -``` - -This endpoint retrieves a specific kitten. - - - -### HTTP Request - -`GET http://example.com/kittens/` - -### URL Parameters - -Parameter | Description ---------- | ----------- -ID | The ID of the kitten to retrieve - -## Delete a Specific Kitten - -```ruby -require 'kittn' - -api = Kittn::APIClient.authorize!('meowmeowmeow') -api.kittens.delete(2) -``` - -```python -import kittn - -api = kittn.authorize('meowmeowmeow') -api.kittens.delete(2) -``` - -```shell -curl "http://example.com/api/kittens/2" \ - -X DELETE \ - -H "Authorization: meowmeowmeow" -``` - -```javascript -const kittn = require('kittn'); - -let api = kittn.authorize('meowmeowmeow'); -let max = api.kittens.delete(2); -``` - -> The above command returns JSON structured like this: - -```json -{ - "id": 2, - "deleted" : ":(" -} -``` - -This endpoint deletes a specific kitten. - -### HTTP Request - -`DELETE http://example.com/kittens/` - -### URL Parameters - -Parameter | Description ---------- | ----------- -ID | The ID of the kitten to delete - diff --git a/samples/rest-notes-slate/slate/source/javascripts/all.js b/samples/rest-notes-slate/slate/source/javascripts/all.js deleted file mode 100644 index 5f5d4067..00000000 --- a/samples/rest-notes-slate/slate/source/javascripts/all.js +++ /dev/null @@ -1,2 +0,0 @@ -//= require ./all_nosearch -//= require ./app/_search diff --git a/samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js b/samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js deleted file mode 100644 index 026e5a20..00000000 --- a/samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js +++ /dev/null @@ -1,27 +0,0 @@ -//= require ./lib/_energize -//= require ./app/_copy -//= require ./app/_toc -//= require ./app/_lang - -function adjustLanguageSelectorWidth() { - const elem = $('.dark-box > .lang-selector'); - elem.width(elem.parent().width()); -} - -$(function() { - loadToc($('#toc'), '.toc-link', '.toc-list-h2', 10); - setupLanguages($('body').data('languages')); - $('.content').imagesLoaded( function() { - window.recacheHeights(); - window.refreshToc(); - }); - - $(window).resize(function() { - adjustLanguageSelectorWidth(); - }); - adjustLanguageSelectorWidth(); -}); - -window.onpopstate = function() { - activateLanguage(getLanguageFromQueryString()); -}; diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_copy.js b/samples/rest-notes-slate/slate/source/javascripts/app/_copy.js deleted file mode 100644 index 4dfbbb6c..00000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_copy.js +++ /dev/null @@ -1,15 +0,0 @@ -function copyToClipboard(container) { - const el = document.createElement('textarea'); - el.value = container.textContent.replace(/\n$/, ''); - document.body.appendChild(el); - el.select(); - document.execCommand('copy'); - document.body.removeChild(el); -} - -function setupCodeCopy() { - $('pre.highlight').prepend('
    Copy to Clipboard
    '); - $('.copy-clipboard').on('click', function() { - copyToClipboard(this.parentNode.children[1]); - }); -} diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js b/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js deleted file mode 100644 index cc5ac8b6..00000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js +++ /dev/null @@ -1,171 +0,0 @@ -//= require ../lib/_jquery - -/* -Copyright 2008-2013 Concur Technologies, Inc. - -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 - - http://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. -*/ -;(function () { - 'use strict'; - - var languages = []; - - window.setupLanguages = setupLanguages; - window.activateLanguage = activateLanguage; - window.getLanguageFromQueryString = getLanguageFromQueryString; - - function activateLanguage(language) { - if (!language) return; - if (language === "") return; - - $(".lang-selector a").removeClass('active'); - $(".lang-selector a[data-language-name='" + language + "']").addClass('active'); - for (var i=0; i < languages.length; i++) { - $(".highlight.tab-" + languages[i]).hide(); - $(".lang-specific." + languages[i]).hide(); - } - $(".highlight.tab-" + language).show(); - $(".lang-specific." + language).show(); - - window.recacheHeights(); - - // scroll to the new location of the position - if ($(window.location.hash).get(0)) { - $(window.location.hash).get(0).scrollIntoView(true); - } - } - - // parseURL and stringifyURL are from https://github.com/sindresorhus/query-string - // MIT licensed - // https://github.com/sindresorhus/query-string/blob/7bee64c16f2da1a326579e96977b9227bf6da9e6/license - function parseURL(str) { - if (typeof str !== 'string') { - return {}; - } - - str = str.trim().replace(/^(\?|#|&)/, ''); - - if (!str) { - return {}; - } - - return str.split('&').reduce(function (ret, param) { - var parts = param.replace(/\+/g, ' ').split('='); - var key = parts[0]; - var val = parts[1]; - - key = decodeURIComponent(key); - // missing `=` should be `null`: - // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters - val = val === undefined ? null : decodeURIComponent(val); - - if (!ret.hasOwnProperty(key)) { - ret[key] = val; - } else if (Array.isArray(ret[key])) { - ret[key].push(val); - } else { - ret[key] = [ret[key], val]; - } - - return ret; - }, {}); - }; - - function stringifyURL(obj) { - return obj ? Object.keys(obj).sort().map(function (key) { - var val = obj[key]; - - if (Array.isArray(val)) { - return val.sort().map(function (val2) { - return encodeURIComponent(key) + '=' + encodeURIComponent(val2); - }).join('&'); - } - - return encodeURIComponent(key) + '=' + encodeURIComponent(val); - }).join('&') : ''; - }; - - // gets the language set in the query string - function getLanguageFromQueryString() { - if (location.search.length >= 1) { - var language = parseURL(location.search).language; - if (language) { - return language; - } else if (jQuery.inArray(location.search.substr(1), languages) != -1) { - return location.search.substr(1); - } - } - - return false; - } - - // returns a new query string with the new language in it - function generateNewQueryString(language) { - var url = parseURL(location.search); - if (url.language) { - url.language = language; - return stringifyURL(url); - } - return language; - } - - // if a button is clicked, add the state to the history - function pushURL(language) { - if (!history) { return; } - var hash = window.location.hash; - if (hash) { - hash = hash.replace(/^#+/, ''); - } - history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash); - - // save language as next default - if (localStorage) { - localStorage.setItem("language", language); - } - } - - function setupLanguages(l) { - var defaultLanguage = null; - if (localStorage) { - defaultLanguage = localStorage.getItem("language"); - } - - languages = l; - - var presetLanguage = getLanguageFromQueryString(); - if (presetLanguage) { - // the language is in the URL, so use that language! - activateLanguage(presetLanguage); - - if (localStorage) { - localStorage.setItem("language", presetLanguage); - } - } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { - // the language was the last selected one saved in localstorage, so use that language! - activateLanguage(defaultLanguage); - } else { - // no language selected, so use the default - activateLanguage(languages[0]); - } - } - - // if we click on a language tab, activate that language - $(function() { - $(".lang-selector a").on("click", function() { - var language = $(this).data("language-name"); - pushURL(language); - activateLanguage(language); - return false; - }); - }); -})(); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js deleted file mode 100644 index 0b0ccd97..00000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js +++ /dev/null @@ -1,102 +0,0 @@ -//= require ../lib/_lunr -//= require ../lib/_jquery -//= require ../lib/_jquery.highlight -;(function () { - 'use strict'; - - var content, searchResults; - var highlightOpts = { element: 'span', className: 'search-highlight' }; - var searchDelay = 0; - var timeoutHandle = 0; - var index; - - function populate() { - index = lunr(function(){ - - this.ref('id'); - this.field('title', { boost: 10 }); - this.field('body'); - this.pipeline.add(lunr.trimmer, lunr.stopWordFilter); - var lunrConfig = this; - - $('h1, h2').each(function() { - var title = $(this); - var body = title.nextUntil('h1, h2'); - lunrConfig.add({ - id: title.prop('id'), - title: title.text(), - body: body.text() - }); - }); - - }); - determineSearchDelay(); - } - - $(populate); - $(bind); - - function determineSearchDelay() { - if (index.tokenSet.toArray().length>5000) { - searchDelay = 300; - } - } - - function bind() { - content = $('.content'); - searchResults = $('.search-results'); - - $('#input-search').on('keyup',function(e) { - var wait = function() { - return function(executingFunction, waitTime){ - clearTimeout(timeoutHandle); - timeoutHandle = setTimeout(executingFunction, waitTime); - }; - }(); - wait(function(){ - search(e); - }, searchDelay); - }); - } - - function search(event) { - - var searchInput = $('#input-search')[0]; - - unhighlight(); - searchResults.addClass('visible'); - - // ESC clears the field - if (event.keyCode === 27) searchInput.value = ''; - - if (searchInput.value) { - var results = index.search(searchInput.value).filter(function(r) { - return r.score > 0.0001; - }); - - if (results.length) { - searchResults.empty(); - $.each(results, function (index, result) { - var elem = document.getElementById(result.ref); - searchResults.append("
  • " + $(elem).text() + "
  • "); - }); - highlight.call(searchInput); - } else { - searchResults.html('
  • '); - $('.search-results li').text('No Results Found for "' + searchInput.value + '"'); - } - } else { - unhighlight(); - searchResults.removeClass('visible'); - } - } - - function highlight() { - if (this.value) content.highlight(this.value, highlightOpts); - } - - function unhighlight() { - content.unhighlight(highlightOpts); - } -})(); - diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js deleted file mode 100644 index f70bdc0f..00000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js +++ /dev/null @@ -1,122 +0,0 @@ -//= require ../lib/_jquery -//= require ../lib/_imagesloaded.min -;(function () { - 'use strict'; - - var htmlPattern = /<[^>]*>/g; - var loaded = false; - - var debounce = function(func, waitTime) { - var timeout = false; - return function() { - if (timeout === false) { - setTimeout(function() { - func(); - timeout = false; - }, waitTime); - timeout = true; - } - }; - }; - - var closeToc = function() { - $(".toc-wrapper").removeClass('open'); - $("#nav-button").removeClass('open'); - }; - - function loadToc($toc, tocLinkSelector, tocListSelector, scrollOffset) { - var headerHeights = {}; - var pageHeight = 0; - var windowHeight = 0; - var originalTitle = document.title; - - var recacheHeights = function() { - headerHeights = {}; - pageHeight = $(document).height(); - windowHeight = $(window).height(); - - $toc.find(tocLinkSelector).each(function() { - var targetId = $(this).attr('href'); - if (targetId[0] === "#") { - headerHeights[targetId] = $("#" + $.escapeSelector(targetId.substring(1))).offset().top; - } - }); - }; - - var refreshToc = function() { - var currentTop = $(document).scrollTop() + scrollOffset; - - if (currentTop + windowHeight >= pageHeight) { - // at bottom of page, so just select last header by making currentTop very large - // this fixes the problem where the last header won't ever show as active if its content - // is shorter than the window height - currentTop = pageHeight + 1000; - } - - var best = null; - for (var name in headerHeights) { - if ((headerHeights[name] < currentTop && headerHeights[name] > headerHeights[best]) || best === null) { - best = name; - } - } - - // Catch the initial load case - if (currentTop == scrollOffset && !loaded) { - best = window.location.hash; - loaded = true; - } - - var $best = $toc.find("[href='" + best + "']").first(); - if (!$best.hasClass("active")) { - // .active is applied to the ToC link we're currently on, and its parent