├── .editorconfig ├── .github └── workflows │ ├── gradle-nebula.yml │ └── maven.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mvnw ├── mvnw.cmd ├── pom.xml ├── scripts ├── check-release.sh ├── deploy-latest.sh └── tag-release.sh ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── droidablebee │ │ └── springboot │ │ └── rest │ │ ├── Application.java │ │ ├── config │ │ ├── AuthorizationConfiguration.java │ │ ├── WebConfiguration.java │ │ └── WebSecurityConfig.java │ │ ├── domain │ │ ├── Address.java │ │ └── Person.java │ │ ├── endpoint │ │ ├── BaseEndpoint.java │ │ ├── CustomActuatorEndpoint.java │ │ ├── InfoWebEndpointExtension.java │ │ ├── PersonEndpoint.java │ │ ├── PersonValidator.java │ │ └── error │ │ │ └── Error.java │ │ ├── repository │ │ └── PersonRepository.java │ │ └── service │ │ ├── CacheableService.java │ │ └── PersonService.java └── resources │ ├── application.yml │ ├── logback.xml │ └── messages.properties └── test ├── groovy └── com │ └── droidablebee │ └── springboot │ └── rest │ ├── ApplicationSpec.groovy │ ├── endpoint │ ├── ActuatorEndpointSpec.groovy │ ├── ActuatorEndpointStubbedSpec.groovy │ ├── BaseEndpointSpec.groovy │ ├── PersonEndpointAuthorizationDisabledSpec.groovy │ ├── PersonEndpointSpec.groovy │ ├── PersonEndpointStubbedSpec.groovy │ └── SwaggerEndpointSpec.groovy │ ├── repository │ └── PersonRepositorySpec.groovy │ └── service │ └── CacheableServiceSpec.groovy ├── java └── com │ └── droidablebee │ └── springboot │ └── rest │ ├── endpoint │ ├── ActuatorEndpointStubbedTest.java │ ├── ActuatorEndpointTest.java │ ├── BaseEndpointTest.java │ ├── PersonEndpointStubbedTest.java │ ├── PersonEndpointTest.java │ └── SwaggerEndpointTest.java │ └── repository │ └── PersonRepositoryTest.java └── resources ├── application-default.yml └── logback-test.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 4 10 | 11 | # 2 space indentation 12 | [*.{xml,html,json,y*ml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/workflows/gradle-nebula.yml: -------------------------------------------------------------------------------- 1 | name: Java CI using Gradle and Nebula 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* 8 | pull_request: 9 | branches: 10 | - master 11 | - release/* 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | # The GITHUB_TOKEN secret is set to an access token for the repository each time a job in a workflow begins. 17 | # You should set the permissions for this access token in the workflow file to grant 18 | # read access for the contents scope and write access for the packages scope. 19 | permissions: 20 | contents: write 21 | packages: write 22 | pull-requests: write 23 | checks: write 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | # 0 indicates all history for all branches and tags 29 | fetch-depth: "0" 30 | 31 | - name: Set up JDK 17 32 | uses: actions/setup-java@v4 33 | with: 34 | java-version: 17 35 | distribution: 'temurin' 36 | settings-path: ${{ github.workspace }} # location for the settings.xml file 37 | 38 | - name: Setup Gradle 39 | uses: gradle/actions/setup-gradle@v4 40 | 41 | - name: git status 42 | run: | 43 | git status 44 | git remote -v 45 | 46 | - name: Build for PR 47 | if: github.event_name == 'pull_request' 48 | run: ./gradlew build -Prelease.stage=final 49 | 50 | - name: Add test report to PR checks 51 | uses: dorny/test-reporter@v1 52 | if: (success() || failure()) && github.event_name == 'pull_request' 53 | with: 54 | name: Tests report 55 | path: ${{ github.workspace }}/build/test-results/test/*.xml 56 | reporter: java-junit 57 | 58 | - name: Add code coverage to PR 59 | id: jacoco 60 | uses: madrapps/jacoco-report@v1.5 61 | if: github.event_name == 'pull_request' 62 | with: 63 | paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | min-coverage-overall: 90 66 | min-coverage-changed-files: 99 67 | 68 | - name: Build, publish and tag release 69 | if: github.event_name == 'push' 70 | run: ./gradlew build publish final -Prelease.stage=final 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | # upload full test report (e.g. in case stdout / stderr is needed) 75 | - name: Upload test report 76 | uses: actions/upload-artifact@v4 77 | if: success() || failure() 78 | with: 79 | name: Tests report 80 | path: ${{ github.workspace }}/build/reports/tests 81 | retention-days: 1 82 | 83 | - name: deploy new release 84 | if: github.event_name == 'push' 85 | run: scripts/deploy-latest.sh 86 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java Maven CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release/* 8 | pull_request: 9 | branches: 10 | - master 11 | - release/* 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | # 0 indicates all history for all branches and tags 21 | fetch-depth: "0" 22 | - name: Set up JDK 11 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 11 26 | 27 | - name: git status 28 | run: | 29 | git status 30 | git remote -v 31 | 32 | - name: check release tag 33 | if: github.event_name == 'pull_request' 34 | run: scripts/check-release.sh 35 | 36 | - name: Build with Maven 37 | run: mvn -B package --file pom.xml 38 | 39 | - name: tag release 40 | if: github.event_name == 'push' 41 | run: scripts/tag-release.sh 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | bin 3 | .settings 4 | .classpath 5 | .project 6 | .idea 7 | *.iml 8 | 9 | build 10 | .gradle 11 | 12 | # github workflow 13 | settings.xml 14 | toolchains.xml 15 | 16 | # Mac 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavelfomin/spring-boot-rest-example/6409ae7fa7c124cd829ef1ceb7c20e9a25c99876/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring boot example with REST and spring data JPA 2 | See [micronaut-rest-example](https://github.com/pavelfomin/micronaut-rest-example) for `Micronaut` implementation. 3 | 4 | ### Running tests 5 | * Maven: `./mvnw clean test` 6 | * Gradle: `./gradlew clean test` 7 | 8 | ### Endpoints 9 | 10 | | Method | Url | Decription | 11 | | ------ | --- | ---------- | 12 | | GET |/actuator/info | info / heartbeat - provided by boot | 13 | | GET |/actuator/health| application health - provided by boot | 14 | | GET |/v2/api-docs | swagger json | 15 | | GET |/swagger-ui.html| swagger html | 16 | | GET |/v1/person/{id}| get person by id | 17 | | GET |/v1/persons | get N persons with an offset| 18 | | PUT |/v1/person | add / update person| 19 | 20 | ### Change maven version 21 | `mvn -N io.takari:maven:wrapper -Dmaven=3.8.4` 22 | 23 | ### Upgrade Gradle 24 | * ./gradlew wrapper --gradle-version 8.11 25 | * ./gradlew wrapper 26 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '3.4.3' 3 | id 'io.spring.dependency-management' version '1.1.7' 4 | id 'java' 5 | id 'groovy' 6 | id 'nebula.release' version "17.1.0" 7 | id 'nebula.maven-publish' version "18.4.0" 8 | id 'jacoco' 9 | id 'maven-publish' 10 | } 11 | 12 | java { 13 | sourceCompatibility = '17' 14 | withJavadocJar() 15 | withSourcesJar() 16 | } 17 | 18 | springBoot { 19 | buildInfo() 20 | } 21 | 22 | ext { 23 | springDocVersion = '2.3.0' 24 | spockVersion = '2.4-M4-groovy-4.0' 25 | } 26 | 27 | dependencies { 28 | implementation "org.springframework.boot:spring-boot-starter-web" 29 | implementation "org.springframework.boot:spring-boot-starter-actuator" 30 | implementation "org.springframework.boot:spring-boot-starter-validation" 31 | implementation "org.springframework.boot:spring-boot-starter-data-jpa" 32 | implementation "org.springframework.boot:spring-boot-starter-security" 33 | implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server" 34 | // implementation 'org.springframework.boot:spring-boot-starter-cache' 35 | implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springDocVersion}" 36 | 37 | implementation "com.h2database:h2:2.1.210" 38 | // implementation 'com.github.ben-manes.caffeine:caffeine' 39 | 40 | testImplementation ("org.springframework.boot:spring-boot-starter-test") { 41 | // exclude group: 'org.mockito' 42 | // exclude group: 'org.junit.jupiter' 43 | } 44 | testImplementation "org.springframework.security:spring-security-test" 45 | testImplementation platform("org.spockframework:spock-bom:${spockVersion}") 46 | testImplementation "org.spockframework:spock-spring" 47 | testImplementation "org.apache.groovy:groovy-json" 48 | } 49 | 50 | repositories { 51 | mavenLocal() 52 | mavenCentral() 53 | } 54 | 55 | tasks.named('test') { 56 | useJUnitPlatform() 57 | testLogging { 58 | events "passed", "skipped", "failed" 59 | } 60 | afterSuite { desc, result -> 61 | if (!desc.parent) 62 | println("${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)") 63 | } 64 | finalizedBy jacocoTestReport // report is always generated after tests run 65 | } 66 | 67 | jacocoTestReport { 68 | dependsOn test // tests are required to run before generating the report 69 | reports { 70 | xml.required = true 71 | } 72 | } 73 | 74 | publishing { 75 | repositories { 76 | maven { 77 | name = "GitHubPackages" 78 | url = "https://maven.pkg.github.com/pavelfomin/spring-boot-rest-example" 79 | credentials { 80 | username = System.getenv("GITHUB_ACTOR") 81 | password = System.getenv("GITHUB_TOKEN") 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=com.droidablebee 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavelfomin/spring-boot-rest-example/6409ae7fa7c124cd829ef1ceb7c20e9a25c99876/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.droidablebee 6 | spring-boot-rest-example 7 | 2.6.3.0 8 | Spring boot example with REST and spring data JPA 9 | 10 | jar 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 2.6.3 16 | 17 | 18 | 19 | 11 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-validation 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-data-jpa 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-actuator 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-oauth2-resource-server 48 | 49 | 50 | 51 | 52 | com.fasterxml.jackson.dataformat 53 | jackson-dataformat-xml 54 | 2.9.4 55 | 56 | 57 | 58 | 59 | org.springdoc 60 | springdoc-openapi-ui 61 | 1.6.5 62 | 63 | 64 | 65 | 66 | com.h2database 67 | h2 68 | 2.2.220 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-starter-test 75 | test 76 | 77 | 78 | 79 | org.springframework.security 80 | spring-security-test 81 | test 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-compiler-plugin 90 | 91 | ${java.version} 92 | ${java.version} 93 | 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-maven-plugin 99 | 100 | 101 | 102 | build-info 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /scripts/check-release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | version=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) 3 | echo "Checking that release tag $version does not exist" 4 | git tag -l | grep $version 5 | [ $? == 1 ] || exit 1 6 | -------------------------------------------------------------------------------- /scripts/deploy-latest.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | version=$(git describe --abbrev=0 --tags) 3 | echo "Deploying version: $version" 4 | #todo: deployment steps here -------------------------------------------------------------------------------- /scripts/tag-release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | version=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) 3 | echo "Creating release tag $version" 4 | git tag $version 5 | git push --tags 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | // https://docs.gradle.org/current/userguide/plugins.html#sec:custom_plugin_repositories 2 | //pluginManagement { 3 | // repositories { 4 | // maven { 5 | // url 'https://artifactory.droidablebee.com/artifactory/plugins-release' 6 | // credentials { 7 | // username = artifactoryUsername 8 | // password = artifactoryPassword 9 | // } 10 | // } 11 | // } 12 | //} 13 | 14 | rootProject.name = 'spring-boot-rest-example' 15 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/Application.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | 9 | @SpringBootApplication 10 | @EnableCaching 11 | public class Application { 12 | @SuppressWarnings("unused") 13 | private static final Logger log = LoggerFactory.getLogger(Application.class); 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(Application.class); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/config/AuthorizationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class AuthorizationConfiguration { 8 | 9 | @Value("${app.authorization.enabled:true}") 10 | private boolean enabled; 11 | 12 | public boolean isEnabled() { 13 | return enabled; 14 | } 15 | 16 | public boolean isDisabled() { 17 | return !enabled; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/config/WebConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; 7 | import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | @Configuration 11 | public class WebConfiguration implements WebMvcConfigurer { 12 | 13 | @Override 14 | public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 15 | configurer.defaultContentType(MediaType.APPLICATION_JSON); 16 | } 17 | 18 | /** 19 | * Enable @Valid validation exception handler for @PathVariable, @RequestParam and @RequestHeader. 20 | */ 21 | @Bean 22 | public MethodValidationPostProcessor methodValidationPostProcessor() { 23 | return new MethodValidationPostProcessor(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.Customizer; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 11 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 12 | import org.springframework.security.config.http.SessionCreationPolicy; 13 | import org.springframework.security.web.SecurityFilterChain; 14 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 15 | 16 | @Configuration 17 | @EnableWebSecurity 18 | @EnableMethodSecurity 19 | public class WebSecurityConfig { 20 | 21 | @Value("${app.security.ignore:/swagger/**, /swagger-resources/**, /swagger-ui/**, /swagger-ui.html, /webjars/**, /v3/api-docs/**, /actuator/info}") 22 | private String[] ignorePatterns; 23 | 24 | @Bean 25 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 26 | 27 | http 28 | .csrf(AbstractHttpConfigurer::disable) 29 | .authorizeHttpRequests(customizer -> customizer 30 | //make sure principal is created for the health endpoint to verify the role 31 | .requestMatchers(new AntPathRequestMatcher("/actuator/health")) 32 | .permitAll() 33 | .anyRequest() 34 | .authenticated() 35 | ) 36 | .oauth2ResourceServer((configurer) -> configurer.jwt(Customizer.withDefaults())) 37 | .sessionManagement((s) -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); 38 | 39 | return http.build(); 40 | } 41 | 42 | @Bean 43 | public WebSecurityCustomizer webSecurityCustomizer() { 44 | 45 | return (web) -> web.ignoring().requestMatchers(ignorePatterns); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/domain/Address.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.validation.constraints.NotNull; 9 | 10 | @Entity 11 | public class Address { 12 | 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.AUTO) 15 | @Column(name = "address_id") 16 | private Long id; 17 | 18 | @NotNull 19 | private String line1; 20 | private String line2; 21 | @NotNull 22 | private String city; 23 | @NotNull 24 | private String state; 25 | @NotNull 26 | private String zip; 27 | 28 | public Address() { 29 | } 30 | 31 | public Address(String line1, String city, String state, String zip) { 32 | 33 | this.line1 = line1; 34 | this.city = city; 35 | this.state = state; 36 | this.zip = zip; 37 | } 38 | 39 | public Long getId() { 40 | return id; 41 | } 42 | 43 | public String getLine1() { 44 | return line1; 45 | } 46 | 47 | public void setLine1(String line1) { 48 | this.line1 = line1; 49 | } 50 | 51 | public String getLine2() { 52 | return line2; 53 | } 54 | 55 | public void setLine2(String line2) { 56 | this.line2 = line2; 57 | } 58 | 59 | public String getCity() { 60 | return city; 61 | } 62 | 63 | public void setCity(String city) { 64 | this.city = city; 65 | } 66 | 67 | public String getState() { 68 | return state; 69 | } 70 | 71 | public void setState(String state) { 72 | this.state = state; 73 | } 74 | 75 | public String getZip() { 76 | return zip; 77 | } 78 | 79 | public void setZip(String zip) { 80 | this.zip = zip; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/domain/Person.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.domain; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.OneToMany; 11 | import jakarta.validation.Valid; 12 | import jakarta.validation.constraints.NotNull; 13 | 14 | import java.util.Date; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | @Entity 19 | public class Person { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.AUTO) 23 | @Column(name = "person_id") 24 | private Long id; 25 | 26 | @NotNull 27 | @Column(name = "first_name", nullable = false) 28 | private String firstName; 29 | 30 | @NotNull 31 | @Column(name = "last_name", nullable = false) 32 | private String lastName; 33 | 34 | @Column(name = "middle_name") 35 | private String middleName; 36 | 37 | @Column(name = "dob") 38 | private Date dateOfBirth; 39 | 40 | @Column(name = "gender") 41 | private Gender gender; 42 | 43 | @Valid 44 | @OneToMany(cascade = CascadeType.ALL) 45 | @JoinColumn(name = "person_id") 46 | private Set
addresses; 47 | 48 | protected Person() { 49 | } 50 | 51 | public Person(Long id, String firstName, String lastName) { 52 | this.id = id; 53 | this.firstName = firstName; 54 | this.lastName = lastName; 55 | } 56 | 57 | public Person(String firstName, String lastName) { 58 | this.firstName = firstName; 59 | this.lastName = lastName; 60 | } 61 | 62 | public Long getId() { 63 | return id; 64 | } 65 | 66 | public String getFirstName() { 67 | return firstName; 68 | } 69 | 70 | public void setFirstName(String firstName) { 71 | this.firstName = firstName; 72 | } 73 | 74 | public String getLastName() { 75 | return lastName; 76 | } 77 | 78 | public void setLastName(String lastName) { 79 | this.lastName = lastName; 80 | } 81 | 82 | public String getMiddleName() { 83 | return middleName; 84 | } 85 | 86 | public void setMiddleName(String middleName) { 87 | this.middleName = middleName; 88 | } 89 | 90 | public Date getDateOfBirth() { 91 | return dateOfBirth; 92 | } 93 | 94 | public void setDateOfBirth(Date dateOfBirth) { 95 | this.dateOfBirth = dateOfBirth; 96 | } 97 | 98 | public Gender getGender() { 99 | return gender; 100 | } 101 | 102 | public void setGender(Gender gender) { 103 | this.gender = gender; 104 | } 105 | 106 | public Set
getAddresses() { 107 | return addresses; 108 | } 109 | 110 | public void setAddresses(Set
addresses) { 111 | this.addresses = addresses; 112 | } 113 | 114 | public void addAddress(Address address) { 115 | 116 | if (getAddresses() == null) { 117 | setAddresses(new HashSet<>()); 118 | } 119 | getAddresses().add(address); 120 | } 121 | 122 | public static enum Gender { 123 | M, F; 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/endpoint/BaseEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import com.droidablebee.springboot.rest.endpoint.error.Error; 4 | import jakarta.validation.ConstraintViolation; 5 | import jakarta.validation.ConstraintViolationException; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.MessageSource; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.http.converter.HttpMessageNotReadableException; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.validation.BindException; 13 | import org.springframework.validation.FieldError; 14 | import org.springframework.validation.ObjectError; 15 | import org.springframework.web.bind.MethodArgumentNotValidException; 16 | import org.springframework.web.bind.ServletRequestBindingException; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | public abstract class BaseEndpoint { 23 | 24 | @Autowired 25 | protected MessageSource messageSource; 26 | 27 | @ExceptionHandler 28 | protected ResponseEntity handleBindException(BindException exception) { 29 | return ResponseEntity.badRequest().body(convert(exception.getAllErrors())); 30 | } 31 | 32 | /** 33 | * Exception handler for validation errors caused by method parameters @RequesParam, @PathVariable, @RequestHeader annotated with jakarta.validation constraints. 34 | */ 35 | @ExceptionHandler 36 | protected ResponseEntity handleConstraintViolationException(ConstraintViolationException exception) { 37 | 38 | List errors = new ArrayList<>(); 39 | 40 | for (ConstraintViolation violation : exception.getConstraintViolations()) { 41 | String value = (violation.getInvalidValue() == null ? null : violation.getInvalidValue().toString()); 42 | errors.add(new Error(violation.getPropertyPath().toString(), value, violation.getMessage())); 43 | } 44 | 45 | return ResponseEntity.badRequest().body(errors); 46 | } 47 | 48 | /** 49 | * Exception handler for @RequestBody validation errors. 50 | */ 51 | @ExceptionHandler 52 | protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { 53 | 54 | return ResponseEntity.badRequest().body(convert(exception.getBindingResult().getAllErrors())); 55 | } 56 | 57 | /** 58 | * Exception handler for missing required parameters errors. 59 | */ 60 | @ExceptionHandler 61 | protected ResponseEntity handleServletRequestBindingException(ServletRequestBindingException exception) { 62 | 63 | return ResponseEntity.badRequest().body(new Error(null, null, exception.getMessage())); 64 | } 65 | 66 | /** 67 | * Exception handler for invalid payload (e.g. json invalid format error). 68 | */ 69 | @ExceptionHandler 70 | protected ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) { 71 | 72 | return ResponseEntity.badRequest().body(new Error(null, null, exception.getMessage())); 73 | } 74 | 75 | @ExceptionHandler 76 | protected ResponseEntity handleAccessDeniedException(AccessDeniedException exception) { 77 | return new ResponseEntity<>(new Error(null, null, exception.getMessage()), HttpStatus.FORBIDDEN); 78 | } 79 | 80 | @ExceptionHandler 81 | protected ResponseEntity handleException(Exception exception) { 82 | return new ResponseEntity<>(new Error(null, null, exception.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); 83 | } 84 | 85 | protected List convert(List objectErrors) { 86 | 87 | List errors = new ArrayList<>(); 88 | 89 | for (ObjectError objectError : objectErrors) { 90 | 91 | String message = objectError.getDefaultMessage(); 92 | if (message == null) { 93 | //when using custom spring validator org.springframework.validation.Validator need to resolve messages manually 94 | message = messageSource.getMessage(objectError, null); 95 | } 96 | 97 | Error error; 98 | if (objectError instanceof FieldError) { 99 | FieldError fieldError = (FieldError) objectError; 100 | String value = (fieldError.getRejectedValue() == null ? null : fieldError.getRejectedValue().toString()); 101 | error = new Error(fieldError.getField(), value, message); 102 | } else { 103 | error = new Error(objectError.getObjectName(), objectError.getCode(), objectError.getDefaultMessage()); 104 | } 105 | 106 | errors.add(error); 107 | } 108 | 109 | return errors; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/endpoint/CustomActuatorEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 4 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.util.LinkedMultiValueMap; 8 | 9 | /** 10 | * Custom actuator endpoint. 11 | */ 12 | @Component 13 | @Endpoint(id = CustomActuatorEndpoint.CUSTOM) 14 | public class CustomActuatorEndpoint { 15 | 16 | static final String CUSTOM = "custom"; 17 | 18 | @ReadOperation 19 | public ResponseEntity custom() { 20 | 21 | return ResponseEntity.ok(createCustomMap()); 22 | } 23 | 24 | protected LinkedMultiValueMap createCustomMap() { 25 | 26 | return new LinkedMultiValueMap<>(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/endpoint/InfoWebEndpointExtension.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 5 | import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; 6 | import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; 7 | import org.springframework.boot.actuate.info.InfoEndpoint; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.util.LinkedMultiValueMap; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @Component 15 | @EndpointWebExtension(endpoint = InfoEndpoint.class) 16 | public class InfoWebEndpointExtension { 17 | 18 | @Autowired 19 | private InfoEndpoint delegate; 20 | 21 | @ReadOperation 22 | public WebEndpointResponse> info() { 23 | 24 | Map info = new HashMap<>(); 25 | 26 | //add existing values from unmodifiable map 27 | info.putAll(this.delegate.info()); 28 | 29 | info.putAll(createCustomMap()); 30 | 31 | return new WebEndpointResponse<>(info); 32 | } 33 | 34 | protected LinkedMultiValueMap createCustomMap() { 35 | 36 | return new LinkedMultiValueMap<>(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/endpoint/PersonEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import com.droidablebee.springboot.rest.domain.Person; 4 | import com.droidablebee.springboot.rest.service.PersonService; 5 | import io.swagger.v3.oas.annotations.Operation; 6 | import io.swagger.v3.oas.annotations.Parameter; 7 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 9 | import jakarta.validation.Valid; 10 | import jakarta.validation.constraints.Size; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.data.domain.PageRequest; 14 | import org.springframework.data.domain.Pageable; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.security.access.prepost.PreAuthorize; 19 | import org.springframework.validation.annotation.Validated; 20 | import org.springframework.web.bind.WebDataBinder; 21 | import org.springframework.web.bind.annotation.InitBinder; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.RequestBody; 24 | import org.springframework.web.bind.annotation.RequestHeader; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.RequestMethod; 27 | import org.springframework.web.bind.annotation.RequestParam; 28 | import org.springframework.web.bind.annotation.RestController; 29 | 30 | @RestController 31 | @RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) 32 | @Validated //required for @Valid on method parameters such as @RequesParam, @PathVariable, @RequestHeader 33 | public class PersonEndpoint extends BaseEndpoint { 34 | 35 | static final int DEFAULT_PAGE_SIZE = 10; 36 | static final String HEADER_TOKEN = "token"; 37 | static final String HEADER_USER_ID = "userId"; 38 | 39 | static final String PERSON_READ_PERMISSION = "person-read"; 40 | static final String PERSON_WRITE_PERMISSION = "person-write"; 41 | 42 | @Autowired 43 | private PersonService personService; 44 | 45 | @PreAuthorize("hasAuthority('SCOPE_" + PERSON_READ_PERMISSION + "')") 46 | @RequestMapping(path = "/v1/persons", method = RequestMethod.GET) 47 | @Operation( 48 | summary = "Get all persons", 49 | description = "Returns first N persons specified by the size parameter with page offset specified by page parameter.") 50 | public Page getAll( 51 | @Parameter(description = "The size of the page to be returned") @RequestParam(required = false) Integer size, 52 | @Parameter(description = "Zero-based page index") @RequestParam(required = false) Integer page) { 53 | 54 | if (size == null) { 55 | size = DEFAULT_PAGE_SIZE; 56 | } 57 | if (page == null) { 58 | page = 0; 59 | } 60 | 61 | Pageable pageable = PageRequest.of(page, size); 62 | 63 | return personService.findAll(pageable); 64 | } 65 | 66 | @PreAuthorize("hasAuthority('SCOPE_" + PERSON_READ_PERMISSION + "') or @authorizationConfiguration.isDisabled()") 67 | @RequestMapping(path = "/v1/person/{id}", method = RequestMethod.GET) 68 | @Operation( 69 | summary = "Get person by id", 70 | description = "Returns person for id specified.") 71 | @ApiResponses(value = {@ApiResponse(responseCode = "404", description = "Person not found")}) 72 | public ResponseEntity get(@Parameter(description = "Person id") @PathVariable("id") Long id) { 73 | 74 | Person person = personService.findOne(id); 75 | return (person == null ? ResponseEntity.status(HttpStatus.NOT_FOUND) : ResponseEntity.ok()).body(person); 76 | } 77 | 78 | @PreAuthorize("hasAuthority('SCOPE_" + PERSON_WRITE_PERMISSION + "')") 79 | @RequestMapping(path = "/v1/person", method = RequestMethod.PUT, consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) 80 | @Operation( 81 | summary = "Create new or update existing person", 82 | description = "Creates new or updates existing person. Returns created/updated person with id.") 83 | public ResponseEntity add( 84 | @Valid @RequestBody Person person, 85 | @Valid @Size(max = 40, min = 8, message = "user id size 8-40") @RequestHeader(name = HEADER_USER_ID) String userId, 86 | @Valid @Size(max = 40, min = 2, message = "token size 2-40") @RequestHeader(name = HEADER_TOKEN, required = false) String token) { 87 | 88 | person = personService.save(person); 89 | return ResponseEntity.ok().body(person); 90 | } 91 | 92 | @InitBinder("person") 93 | protected void initBinder(WebDataBinder binder) { 94 | binder.addValidators(new PersonValidator()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/endpoint/PersonValidator.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import com.droidablebee.springboot.rest.domain.Person; 4 | import org.springframework.validation.Errors; 5 | import org.springframework.validation.ValidationUtils; 6 | import org.springframework.validation.Validator; 7 | 8 | public class PersonValidator implements Validator { 9 | 10 | @Override 11 | public boolean supports(Class clazz) { 12 | return Person.class.isAssignableFrom(clazz); 13 | } 14 | 15 | @Override 16 | public void validate(Object target, Errors errors) { 17 | 18 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "middleName", "validation.message.field.required"); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/endpoint/error/Error.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint.error; 2 | 3 | public class Error { 4 | 5 | private String field; 6 | 7 | private String value; 8 | 9 | private String message; 10 | 11 | public Error(String field, String value, String message) { 12 | this.field = field; 13 | this.value = value; 14 | this.message = message; 15 | } 16 | 17 | public String getField() { 18 | return field; 19 | } 20 | 21 | public void setField(String field) { 22 | this.field = field; 23 | } 24 | 25 | public String getValue() { 26 | return value; 27 | } 28 | 29 | public void setValue(String value) { 30 | this.value = value; 31 | } 32 | 33 | public String getMessage() { 34 | return message; 35 | } 36 | 37 | public void setMessage(String message) { 38 | this.message = message; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.repository; 2 | 3 | import com.droidablebee.springboot.rest.domain.Person; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface PersonRepository extends JpaRepository { 7 | 8 | } -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/service/CacheableService.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.cache.annotation.Cacheable; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public class CacheableService { 12 | 13 | @Autowired 14 | private CacheableServiceClient cacheableServiceClient; 15 | 16 | @Cacheable("CacheableClient") 17 | public Page retrieve(Pageable pageable) { 18 | 19 | return cacheableServiceClient.retrieve(pageable); 20 | } 21 | 22 | interface Result { 23 | } 24 | 25 | @Component 26 | public static class CacheableServiceClient { 27 | 28 | public Page retrieve(Pageable pageable) { 29 | return Page.empty(pageable); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/droidablebee/springboot/rest/service/PersonService.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.service; 2 | 3 | import com.droidablebee.springboot.rest.domain.Person; 4 | import com.droidablebee.springboot.rest.repository.PersonRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.Optional; 12 | 13 | @Service 14 | @Transactional 15 | public class PersonService { 16 | 17 | @Autowired 18 | private PersonRepository repository; 19 | 20 | /** 21 | * Returns paginated list of Person instances. 22 | * 23 | * @param pageable pageable 24 | * @return paginated list of Person instances 25 | */ 26 | @Transactional(readOnly = true) 27 | public Page findAll(Pageable pageable) { 28 | 29 | return repository.findAll(pageable); 30 | } 31 | 32 | @Transactional(readOnly = true) 33 | public Person findOne(Long id) { 34 | 35 | Optional person = repository.findById(id); 36 | return person.orElse(null); 37 | } 38 | 39 | public Person save(Person person) { 40 | 41 | return repository.saveAndFlush(person); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | management: 2 | endpoints: 3 | web: 4 | exposure: 5 | include: info,health 6 | endpoint: 7 | health: 8 | show-details: when-authorized 9 | roles: SCOPE_health-details 10 | probes: 11 | enabled: true 12 | spring: 13 | jpa: 14 | properties: 15 | hibernate: 16 | format_sql: true 17 | use_sql_comments: true 18 | jackson: 19 | # date-format: yyyy-MM-dd'T'HH:mm:ss.SSSZ: 20 | serialization: 21 | write_dates_as_timestamps: true 22 | security: 23 | oauth2: 24 | resourceserver: 25 | jwt: 26 | # this is to wire the "org.springframework.security.oauth2.jwt.JwtDecoder" bean correctly 27 | jwk-set-uri: http://localhost:9999/.well-known/jwks.json 28 | datasource: 29 | platform: h2 30 | driverClassName: org.h2.Driver 31 | url: jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL;INIT=CREATE SCHEMA IF NOT EXISTS ddb 32 | 33 | cache: 34 | cache-names: CacheableClient 35 | caffeine: 36 | spec: expireAfterAccess=5m 37 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | # validation messages 2 | validation.message.field.required=field is required and may not be null 3 | validation.message.field.required.middleName=middle name is required -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/ApplicationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.boot.test.context.SpringBootTest 5 | import org.springframework.context.ApplicationContext 6 | import spock.lang.Specification 7 | 8 | @SpringBootTest 9 | class ApplicationSpec extends Specification { 10 | 11 | @Autowired 12 | ApplicationContext context 13 | 14 | def "context initializes successfully"() { 15 | 16 | expect: 17 | context 18 | context.environment 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/ActuatorEndpointSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import net.minidev.json.JSONArray 4 | 5 | import static org.hamcrest.Matchers.is 6 | import static org.hamcrest.Matchers.isA 7 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 9 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 13 | 14 | class ActuatorEndpointSpec extends BaseEndpointSpec { 15 | 16 | def "info"() throws Exception { 17 | 18 | expect: 19 | mockMvc.perform( 20 | get('/actuator/info') 21 | ) 22 | .andDo(print()) 23 | .andExpect(status().isOk()) 24 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 25 | .andExpect(jsonPath('$.build', isA(Object.class))) 26 | .andExpect(jsonPath('$.build.version', isA(String.class))) 27 | .andExpect(jsonPath('$.build.artifact', is('spring-boot-rest-example'))) 28 | .andExpect(jsonPath('$.build.group', is('com.droidablebee'))) 29 | .andExpect(jsonPath('$.build.time', isA(Number.class))) 30 | 31 | } 32 | 33 | def "health - w/out authorization token"() throws Exception { 34 | 35 | expect: 36 | mockMvc.perform( 37 | get('/actuator/health') 38 | ) 39 | .andDo(print()) 40 | .andExpect(status().isOk()) 41 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 42 | .andExpect(jsonPath('$.status', is('UP'))) 43 | .andExpect(jsonPath('$.components').doesNotExist()) 44 | 45 | } 46 | 47 | def "health - with authorization token"() throws Exception { 48 | 49 | expect: 50 | mockMvc.perform( 51 | get('/actuator/health') 52 | .with(jwt()) 53 | ) 54 | .andDo(print()) 55 | .andExpect(status().isOk()) 56 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 57 | .andExpect(jsonPath('$.status', is('UP'))) 58 | .andExpect(jsonPath('$.components').doesNotExist()) 59 | 60 | } 61 | 62 | def "health - w/out authorization token and configured role"() throws Exception { 63 | 64 | expect: 65 | mockMvc.perform( 66 | get('/actuator/health') 67 | .with(jwtWithScope('health-details')) 68 | ) 69 | .andDo(print()) 70 | .andExpect(status().isOk()) 71 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 72 | .andExpect(jsonPath('$.size()', is(3))) // status, components, groups 73 | .andExpect(jsonPath('$.status', is("UP"))) 74 | .andExpect(jsonPath('$.groups').exists()) 75 | .andExpect(jsonPath('$.components').exists()) 76 | .andExpect(jsonPath('$.components.size()', is(6))) // disk, ping, db, livenessState, readinessState, ssl 77 | .andExpect(jsonPath('$.components.diskSpace.status', is('UP'))) 78 | .andExpect(jsonPath('$.components.diskSpace.details').isNotEmpty()) 79 | .andExpect(jsonPath('$.components.db.status', is('UP'))) 80 | .andExpect(jsonPath('$.components.db.details').isNotEmpty()) 81 | .andExpect(jsonPath('$.components.ping.status', is('UP'))) 82 | .andExpect(jsonPath('$.components.livenessState.status', is("UP"))) 83 | .andExpect(jsonPath('$.components.readinessState.status', is("UP"))) 84 | .andExpect(jsonPath('$.components.ssl.status', is("UP"))) 85 | } 86 | 87 | def "env - w/out authorization token"() throws Exception { 88 | 89 | expect: 90 | mockMvc.perform( 91 | get('/actuator/env') 92 | ) 93 | .andDo(print()) 94 | .andExpect(status().isUnauthorized()) 95 | 96 | } 97 | 98 | def "env - with authorization token"() throws Exception { 99 | 100 | expect: 101 | mockMvc.perform( 102 | get('/actuator/env') 103 | .with(jwt()) 104 | ) 105 | .andDo(print()) 106 | .andExpect(status().isOk()) 107 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 108 | .andExpect(jsonPath('$.activeProfiles', isA(JSONArray.class))) 109 | .andExpect(jsonPath('$.propertySources', isA(JSONArray.class))) 110 | 111 | } 112 | 113 | def "custom actuator - w/out authorization token"() throws Exception { 114 | 115 | expect: 116 | mockMvc.perform( 117 | get('/actuator/' + CustomActuatorEndpoint.CUSTOM) 118 | ) 119 | .andDo(print()) 120 | .andExpect(status().isUnauthorized()) 121 | 122 | } 123 | 124 | def "custom actuator - with authorization token"() throws Exception { 125 | 126 | expect: 127 | mockMvc.perform( 128 | get('/actuator/' + CustomActuatorEndpoint.CUSTOM) 129 | .with(jwt()) 130 | ) 131 | .andDo(print()) 132 | .andExpect(status().isOk()) 133 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 134 | .andExpect(content().string('{}')) 135 | 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/ActuatorEndpointStubbedSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import org.spockframework.spring.SpringSpy 4 | import org.springframework.util.LinkedMultiValueMap 5 | import spock.lang.Ignore 6 | 7 | import static com.droidablebee.springboot.rest.endpoint.CustomActuatorEndpoint.CUSTOM 8 | import static org.hamcrest.Matchers.is 9 | import static org.hamcrest.Matchers.isA 10 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt 11 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 12 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 16 | 17 | // todo: the beans are getting replaced but not getting registered as custom actuator components 18 | @Ignore("Need to figure out why Spock does not inject Spy beans properly in this case") 19 | class ActuatorEndpointStubbedSpec extends BaseEndpointSpec { 20 | 21 | @SpringSpy 22 | CustomActuatorEndpoint customActuatorEndpoint 23 | 24 | @SpringSpy 25 | InfoWebEndpointExtension infoWebEndpointExtension 26 | 27 | LinkedMultiValueMap custom = new LinkedMultiValueMap<>() 28 | 29 | def setup() { 30 | custom.add('custom1', 11) 31 | custom.add('custom1', 12) 32 | custom.add('custom2', 21) 33 | } 34 | 35 | def "get custom - authorized"() throws Exception { 36 | 37 | when: 38 | mockMvc.perform( 39 | get('/actuator/' + CUSTOM).with(jwt()) 40 | ) 41 | .andDo(print()) 42 | .andExpect(status().isOk()) 43 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 44 | .andExpect(jsonPath('$.custom1', is(custom.get('custom1')))) 45 | .andExpect(jsonPath('$.custom2', is(custom.get('custom2')))) 46 | 47 | then: 48 | 1 * customActuatorEndpoint.createCustomMap() >> custom 49 | } 50 | 51 | def "get info - extended"() throws Exception { 52 | 53 | when: 54 | mockMvc.perform( 55 | get('/actuator/info') 56 | ) 57 | .andDo(print()) 58 | .andExpect(status().isOk()) 59 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 60 | .andExpect(jsonPath('$.build', isA(Object.class))) 61 | .andExpect(jsonPath('$.build.version', isA(String.class))) 62 | .andExpect(jsonPath('$.build.artifact', is('spring-boot-rest-example'))) 63 | .andExpect(jsonPath('$.build.group', is('com.droidablebee'))) 64 | .andExpect(jsonPath('$.build.time', isA(Number.class))) 65 | .andExpect(jsonPath('$.custom1', is(custom.get('custom1')))) 66 | .andExpect(jsonPath('$.custom2', is(custom.get('custom2')))) 67 | 68 | then: 69 | 1 * infoWebEndpointExtension.createCustomMap() >> custom 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/BaseEndpointSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.spockframework.spring.SpringBean 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.http.MediaType 9 | import org.springframework.security.oauth2.jwt.BadJwtException 10 | import org.springframework.security.oauth2.jwt.Jwt 11 | import org.springframework.security.oauth2.jwt.JwtDecoder 12 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors 13 | import org.springframework.test.web.servlet.MockMvc 14 | import spock.lang.Specification 15 | 16 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt 17 | 18 | /** 19 | * AbstractEndpointTest with common test methods. 20 | */ 21 | @SpringBootTest 22 | @AutoConfigureMockMvc 23 | //this creates MockMvc instance correctly, including wiring of the spring security 24 | abstract class BaseEndpointSpec extends Specification { 25 | static final MediaType JSON_MEDIA_TYPE = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype()) 26 | 27 | @Autowired 28 | ObjectMapper objectMapper 29 | 30 | @Autowired 31 | MockMvc mockMvc 32 | 33 | @SpringBean 34 | JwtDecoder jwtDecoder = Mock() 35 | 36 | Jwt jwt = Mock() 37 | 38 | 39 | def setup() { 40 | 41 | jwtDecoder.decode("invalid") >> { String token -> throw new BadJwtException("Mocked token '$token' is invalid") } 42 | jwtDecoder.decode(_ as String) >> jwt 43 | } 44 | 45 | /** 46 | * Returns json representation of the object. 47 | * @param o instance 48 | * @return json 49 | * @throws IOException 50 | */ 51 | protected String json(Object o) throws IOException { 52 | 53 | return objectMapper.writeValueAsString(o) 54 | } 55 | 56 | protected SecurityMockMvcRequestPostProcessors.JwtRequestPostProcessor jwtWithScope(String scope) { 57 | 58 | return jwt().jwt(jwt -> jwt.claims(claims -> claims.put("scope", scope))) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/PersonEndpointAuthorizationDisabledSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import com.droidablebee.springboot.rest.domain.Person 4 | import com.droidablebee.springboot.rest.service.PersonService 5 | import org.spockframework.spring.SpringBean 6 | import org.springframework.test.context.TestPropertySource 7 | 8 | import static org.hamcrest.Matchers.is 9 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 10 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 14 | 15 | @TestPropertySource(properties = [ 16 | "app.authorization.enabled:false" 17 | ]) 18 | class PersonEndpointAuthorizationDisabledSpec extends BaseEndpointSpec { 19 | 20 | @SpringBean 21 | PersonService personService = Mock() 22 | 23 | Person testPerson 24 | 25 | def setup() { 26 | 27 | testPerson = new Person(1L, 'Jack', 'Bauer') 28 | personService.findOne(1L) >> testPerson 29 | } 30 | 31 | def "get person by id does not require scope if authorization is disabled"() throws Exception { 32 | Long id = testPerson.id 33 | 34 | expect: 35 | mockMvc.perform( 36 | get('/v1/person/{id}', id) 37 | .header('Authorization', 'Bearer valid') 38 | ) 39 | .andDo(print()) 40 | .andExpect(status().isOk()) 41 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 42 | .andExpect(jsonPath('$.id', is(testPerson.getId().intValue()))) 43 | .andExpect(jsonPath('$.firstName', is(testPerson.getFirstName()))) 44 | .andExpect(jsonPath('$.lastName', is(testPerson.getLastName()))) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/PersonEndpointSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import com.droidablebee.springboot.rest.domain.Address 4 | import com.droidablebee.springboot.rest.domain.Person 5 | import com.droidablebee.springboot.rest.service.PersonService 6 | import jakarta.persistence.EntityManager 7 | import net.minidev.json.JSONArray 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.data.domain.Page 10 | import org.springframework.data.domain.PageRequest 11 | import org.springframework.transaction.annotation.Transactional 12 | 13 | import static com.droidablebee.springboot.rest.endpoint.PersonEndpoint.PERSON_READ_PERMISSION 14 | import static com.droidablebee.springboot.rest.endpoint.PersonEndpoint.PERSON_WRITE_PERMISSION 15 | import static org.hamcrest.Matchers.containsString 16 | import static org.hamcrest.Matchers.hasItem 17 | import static org.hamcrest.Matchers.is 18 | import static org.hamcrest.Matchers.isA 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put 22 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 26 | 27 | @Transactional 28 | class PersonEndpointSpec extends BaseEndpointSpec { 29 | 30 | @Autowired 31 | private EntityManager entityManager 32 | 33 | @Autowired 34 | private PersonService personService 35 | 36 | private Person testPerson 37 | private long timestamp 38 | 39 | def setup() { 40 | 41 | timestamp = new Date().getTime() 42 | 43 | // create test persons 44 | personService.save(createPerson('Jack', 'Bauer')) 45 | personService.save(createPerson('Chloe', "O'Brian")) 46 | personService.save(createPerson('Kim', 'Bauer')) 47 | personService.save(createPerson('David', 'Palmer')) 48 | personService.save(createPerson('Michelle', 'Dessler')) 49 | 50 | Page persons = personService.findAll(PageRequest.of(0, PersonEndpoint.DEFAULT_PAGE_SIZE)) 51 | assert persons 52 | assert persons.totalElements == 5 53 | 54 | testPerson = persons.getContent().get(0) 55 | 56 | //refresh entity with any changes that have been done during persistence including Hibernate conversion 57 | //example: java.util.Date field is injected with either with java.sql.Date (if @Temporal(TemporalType.DATE) is used) 58 | //or java.sql.Timestamp 59 | entityManager.refresh(testPerson) 60 | } 61 | 62 | def "get person by id - unauthorized, no token"() throws Exception { 63 | Long id = testPerson.getId() 64 | 65 | expect: 66 | mockMvc.perform( 67 | get('/v1/person/{id}', id) 68 | ) 69 | .andDo(print()) 70 | .andExpect(status().isUnauthorized()) 71 | 72 | } 73 | 74 | def "get person by id - unauthorized, invalid token"() throws Exception { 75 | Long id = testPerson.getId() 76 | 77 | expect: 78 | mockMvc.perform( 79 | get('/v1/person/{id}', id) 80 | .header('Authorization', 'Bearer invalid') 81 | ) 82 | .andDo(print()) 83 | .andExpect(status().isUnauthorized()) 84 | 85 | } 86 | 87 | def "get person by id - forbidden, invalid scope"() throws Exception { 88 | Long id = testPerson.getId() 89 | 90 | expect: 91 | mockMvc.perform( 92 | get('/v1/person/{id}', id) 93 | .header('Authorization', 'Bearer valid') 94 | ) 95 | .andDo(print()) 96 | .andExpect(status().isForbidden()) 97 | 98 | } 99 | 100 | def "get person by id"() throws Exception { 101 | Long id = testPerson.getId() 102 | 103 | when: 104 | mockMvc.perform( 105 | get('/v1/person/{id}', id) 106 | .header('Authorization', 'Bearer valid') 107 | ) 108 | .andDo(print()) 109 | .andExpect(status().isOk()) 110 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 111 | .andExpect(jsonPath('$.id', is(id.intValue()))) 112 | .andExpect(jsonPath('$.firstName', is(testPerson.getFirstName()))) 113 | .andExpect(jsonPath('$.lastName', is(testPerson.getLastName()))) 114 | .andExpect(jsonPath('$.dateOfBirth', isA(Number.class))) 115 | 116 | then: 117 | jwt.hasClaim('scope') >> true 118 | jwt.getClaim('scope') >> [PERSON_READ_PERMISSION] 119 | 120 | } 121 | 122 | def "get all"() throws Exception { 123 | 124 | Page persons = personService.findAll(PageRequest.of(0, PersonEndpoint.DEFAULT_PAGE_SIZE)) 125 | 126 | when: 127 | mockMvc.perform( 128 | get('/v1/persons') 129 | .header('Authorization', 'Bearer valid') 130 | ) 131 | .andDo(print()) 132 | .andExpect(status().isOk()) 133 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 134 | .andExpect(jsonPath('$.content.size()', is((int) persons.getTotalElements()))) 135 | 136 | then: 137 | jwt.hasClaim('scope') >> true 138 | jwt.getClaim('scope') >> [PERSON_READ_PERMISSION] 139 | } 140 | 141 | /** 142 | * Test JSR-303 bean validation. 143 | */ 144 | def "create person validation - error last name"() throws Exception { 145 | 146 | //person with missing last name 147 | Person person = createPerson('first', null) 148 | person.setMiddleName('middle') 149 | String json = json(person) 150 | 151 | expect: 152 | mockMvc.perform( 153 | put('/v1/person') 154 | .header('Authorization', 'Bearer valid') 155 | .accept(JSON_MEDIA_TYPE) 156 | .content(json) 157 | .contentType(JSON_MEDIA_TYPE) 158 | ) 159 | .andDo(print()) 160 | .andExpect(status().isBadRequest()) 161 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 162 | .andExpect(jsonPath('$', isA(JSONArray.class))) 163 | .andExpect(jsonPath('$.length()', is(1))) 164 | .andExpect(jsonPath('$.[?(@.field == "lastName")].message', hasItem('must not be null'))) 165 | } 166 | 167 | /** 168 | * Test custom bean validation. 169 | */ 170 | def "create person validation - error middle name"() throws Exception { 171 | 172 | //person with missing middle name - custom validation 173 | Person person = createPerson('first', 'last') 174 | String json = json(person) 175 | 176 | expect: 177 | mockMvc.perform( 178 | put('/v1/person') 179 | .header('Authorization', 'Bearer valid') 180 | .accept(JSON_MEDIA_TYPE) 181 | .content(json) 182 | .contentType(JSON_MEDIA_TYPE) 183 | ) 184 | .andDo(print()) 185 | .andExpect(status().isBadRequest()) 186 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 187 | .andExpect(jsonPath('$', isA(JSONArray.class))) 188 | .andExpect(jsonPath('$.length()', is(1))) 189 | .andExpect(jsonPath('$.[?(@.field == "middleName")].message', hasItem('middle name is required'))) 190 | } 191 | 192 | /** 193 | * Test JSR-303 bean object graph validation with nested entities. 194 | */ 195 | def "create person validation - address"() throws Exception { 196 | 197 | Person person = createPerson('first', 'last') 198 | person.setMiddleName('middle') 199 | person.addAddress(new Address('line1', 'city', 'state', 'zip')) 200 | person.addAddress(new Address()) //invalid address 201 | 202 | String json = json(person) 203 | 204 | expect: 205 | mockMvc.perform( 206 | put('/v1/person') 207 | .header('Authorization', 'Bearer valid') 208 | .accept(JSON_MEDIA_TYPE) 209 | .content(json) 210 | .contentType(JSON_MEDIA_TYPE) 211 | ) 212 | .andDo(print()) 213 | .andExpect(status().isBadRequest()) 214 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 215 | .andExpect(jsonPath('$.length()', is(4))) 216 | .andExpect(jsonPath('$.[?(@.field == "addresses[].line1")].message', hasItem('must not be null'))) 217 | .andExpect(jsonPath('$.[?(@.field == "addresses[].state")].message', hasItem('must not be null'))) 218 | .andExpect(jsonPath('$.[?(@.field == "addresses[].city")].message', hasItem('must not be null'))) 219 | .andExpect(jsonPath('$.[?(@.field == "addresses[].zip")].message', hasItem('must not be null'))) 220 | } 221 | 222 | def "create person validation - token"() throws Exception { 223 | 224 | Person person = createPerson('first', 'last') 225 | person.setMiddleName('middle') 226 | 227 | String json = json(person) 228 | 229 | when: 230 | mockMvc.perform( 231 | put('/v1/person') 232 | .header('Authorization', 'Bearer valid') 233 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 234 | .header(PersonEndpoint.HEADER_TOKEN, '1') //invalid token 235 | .accept(JSON_MEDIA_TYPE) 236 | .content(json) 237 | .contentType(JSON_MEDIA_TYPE)) 238 | .andDo(print()) 239 | .andExpect(status().isBadRequest()) 240 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 241 | .andExpect(jsonPath('$.length()', is(1))) 242 | .andExpect(jsonPath('$.[?(@.field == "add.token")].message', hasItem('token size 2-40'))) 243 | 244 | then: 245 | jwt.hasClaim('scope') >> true 246 | jwt.getClaim('scope') >> [PERSON_WRITE_PERMISSION] 247 | } 248 | 249 | def "create person validation - user id"() throws Exception { 250 | 251 | Person person = createPerson('first', 'last') 252 | person.setMiddleName('middle') 253 | 254 | String json = json(person) 255 | 256 | expect: 257 | mockMvc.perform( 258 | put('/v1/person') 259 | .header('Authorization', 'Bearer valid') 260 | .accept(JSON_MEDIA_TYPE) 261 | .content(json) 262 | .contentType(JSON_MEDIA_TYPE) 263 | ) 264 | .andDo(print()) 265 | .andExpect(status().isBadRequest()) 266 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 267 | //'Required request header 'userId' for method parameter type String is not present' 268 | .andExpect(jsonPath('$.message', containsString("Required request header '" + PersonEndpoint.HEADER_USER_ID))) 269 | } 270 | 271 | def "create person - unauthorized"() throws Exception { 272 | 273 | Person person = createPerson('first', 'last') 274 | person.setMiddleName('middleName') 275 | 276 | String json = json(person) 277 | 278 | expect: 279 | mockMvc.perform( 280 | put('/v1/person') 281 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 282 | .accept(JSON_MEDIA_TYPE) 283 | .content(json) 284 | .contentType(JSON_MEDIA_TYPE) 285 | ) 286 | .andDo(print()) 287 | .andExpect(status().isUnauthorized()) 288 | } 289 | 290 | def "create person - forbidden, invalid scope"() throws Exception { 291 | 292 | Person person = createPerson('first', 'last') 293 | person.setMiddleName('middleName') 294 | 295 | String json = json(person) 296 | 297 | when: 298 | mockMvc.perform( 299 | put('/v1/person') 300 | .header('Authorization', 'Bearer valid') 301 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 302 | .accept(JSON_MEDIA_TYPE) 303 | .content(json) 304 | .contentType(JSON_MEDIA_TYPE) 305 | ) 306 | .andDo(print()) 307 | .andExpect(status().isForbidden()) 308 | 309 | then: 310 | jwt.hasClaim('scope') >> true 311 | jwt.getClaim('scope') >> [PERSON_READ_PERMISSION] 312 | } 313 | 314 | def "create person"() throws Exception { 315 | 316 | Person person = createPerson('first', 'last') 317 | person.setMiddleName('middleName') 318 | 319 | String json = json(person) 320 | 321 | when: 322 | mockMvc.perform( 323 | put('/v1/person') 324 | .header('Authorization', 'Bearer valid') 325 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 326 | .accept(JSON_MEDIA_TYPE) 327 | .content(json) 328 | .contentType(JSON_MEDIA_TYPE) 329 | ) 330 | .andDo(print()) 331 | .andExpect(status().isOk()) 332 | .andExpect(jsonPath('$.id', isA(Number.class))) 333 | .andExpect(jsonPath('$.firstName', is(person.getFirstName()))) 334 | .andExpect(jsonPath('$.lastName', is(person.getLastName()))) 335 | .andExpect(jsonPath('$.dateOfBirth', isA(Number.class))) 336 | .andExpect(jsonPath('$.dateOfBirth', is(person.getDateOfBirth().getTime()))) 337 | 338 | then: 339 | jwt.hasClaim('scope') >> true 340 | jwt.getClaim('scope') >> [PERSON_READ_PERMISSION, PERSON_WRITE_PERMISSION] 341 | } 342 | 343 | def "create person - with date verification"() throws Exception { 344 | 345 | Person person = createPerson('first', 'last') 346 | person.setMiddleName('middleName') 347 | 348 | String json = json(person) 349 | 350 | when: 351 | mockMvc.perform( 352 | put('/v1/person') 353 | .header('Authorization', 'Bearer valid') 354 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 355 | .accept(JSON_MEDIA_TYPE) 356 | .content(json) 357 | .contentType(JSON_MEDIA_TYPE) 358 | ) 359 | .andExpect(status().isOk()) 360 | .andExpect(jsonPath('$.id', isA(Number.class))) 361 | .andExpect(jsonPath('$.firstName', is(person.getFirstName()))) 362 | .andExpect(jsonPath('$.lastName', is(person.getLastName()))) 363 | .andExpect(jsonPath('$.dateOfBirth', isA(Number.class))) 364 | .andExpect(jsonPath('$.dateOfBirth', is(person.getDateOfBirth().getTime()))) 365 | 366 | then: 367 | jwt.hasClaim('scope') >> true 368 | jwt.getClaim('scope') >> [PERSON_WRITE_PERMISSION] 369 | } 370 | 371 | def "request body validation - invalid json value"() throws Exception { 372 | 373 | testPerson.setGender(Person.Gender.M) 374 | String json = json(testPerson) 375 | //payload with invalid gender 376 | json = json.replaceFirst('(\"gender\":\")(M)(\")', '$1Q$3') 377 | 378 | expect: 379 | mockMvc.perform( 380 | put('/v1/person') 381 | .header('Authorization', 'Bearer valid') 382 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 383 | .accept(JSON_MEDIA_TYPE) 384 | .content(json) 385 | .contentType(JSON_MEDIA_TYPE) 386 | ) 387 | .andDo(print()) 388 | .andExpect(status().isBadRequest()) 389 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 390 | .andExpect(jsonPath('$.message', containsString("Cannot deserialize value of type `com.droidablebee.springboot.rest.domain.Person\$Gender`"))) 391 | } 392 | 393 | def "request body validation - invalid json"() throws Exception { 394 | 395 | String json = json('not valid json') 396 | 397 | expect: 398 | mockMvc.perform( 399 | put('/v1/person') 400 | .header('Authorization', 'Bearer valid') 401 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 402 | .accept(JSON_MEDIA_TYPE) 403 | .content(json) 404 | .contentType(JSON_MEDIA_TYPE) 405 | ) 406 | .andDo(print()) 407 | .andExpect(status().isBadRequest()) 408 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 409 | .andExpect(jsonPath('$.message', containsString('Cannot construct instance of `com.droidablebee.springboot.rest.domain.Person`'))) 410 | } 411 | 412 | def "handle http request method not supported exception"() throws Exception { 413 | 414 | String json = json(testPerson) 415 | 416 | expect: 417 | mockMvc.perform( 418 | delete('/v1/person') //not supported method 419 | .header('Authorization', 'Bearer valid') 420 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 421 | .accept(JSON_MEDIA_TYPE) 422 | .content(json) 423 | .contentType(JSON_MEDIA_TYPE) 424 | ) 425 | .andDo(print()) 426 | .andExpect(status().isMethodNotAllowed()) 427 | .andExpect(content().string('')) 428 | } 429 | 430 | private Person createPerson(String first, String last) { 431 | 432 | Person person = new Person(first, last) 433 | person.setDateOfBirth(new Date(timestamp)) 434 | 435 | return person 436 | } 437 | 438 | } 439 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/PersonEndpointStubbedSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import com.droidablebee.springboot.rest.domain.Person 4 | import com.droidablebee.springboot.rest.service.PersonService 5 | import org.spockframework.spring.SpringBean 6 | 7 | import static com.droidablebee.springboot.rest.endpoint.PersonEndpoint.PERSON_READ_PERMISSION 8 | import static org.hamcrest.Matchers.is 9 | import static org.hamcrest.Matchers.nullValue 10 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 11 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 15 | 16 | class PersonEndpointStubbedSpec extends BaseEndpointSpec { 17 | 18 | @SpringBean 19 | PersonService personService = Mock() 20 | 21 | Person testPerson 22 | 23 | def setup() { 24 | 25 | testPerson = new Person(1L, 'Jack', 'Bauer') 26 | personService.findOne(1L) >> testPerson 27 | jwt.hasClaim('scope') >> true 28 | jwt.getClaim('scope') >> [PERSON_READ_PERMISSION] 29 | } 30 | 31 | def "get person By id"() throws Exception { 32 | 33 | expect: 34 | mockMvc.perform( 35 | get('/v1/person/{id}', 1) 36 | .header('Authorization', 'Bearer valid') 37 | ) 38 | .andDo(print()) 39 | .andExpect(status().isOk()) 40 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 41 | .andExpect(jsonPath('$.id', is(testPerson.getId().intValue()))) 42 | .andExpect(jsonPath('$.firstName', is(testPerson.getFirstName()))) 43 | .andExpect(jsonPath('$.lastName', is(testPerson.getLastName()))) 44 | } 45 | 46 | def "handle generic exception"() throws Exception { 47 | 48 | String message = 'Failed to get person by id' 49 | 50 | when: 51 | mockMvc.perform( 52 | get('/v1/person/{id}', 1) 53 | .header('Authorization', 'Bearer valid') 54 | ) 55 | .andDo(print()) 56 | .andExpect(status().is5xxServerError()) 57 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 58 | .andExpect(jsonPath('$.field', nullValue())) 59 | .andExpect(jsonPath('$.value', nullValue())) 60 | .andExpect(jsonPath('$.message', is(message))) 61 | 62 | then: 63 | 1 * personService.findOne(1L) >> { throw new RuntimeException(message) } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/endpoint/SwaggerEndpointSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | import static org.hamcrest.Matchers.is 6 | import static org.hamcrest.Matchers.isA 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 8 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 12 | 13 | @Slf4j 14 | class SwaggerEndpointSpec extends BaseEndpointSpec { 15 | 16 | def "get api docs"() throws Exception { 17 | 18 | expect: 19 | mockMvc.perform( 20 | get('/v3/api-docs') 21 | ) 22 | .andDo(print()) 23 | .andExpect(status().isOk()) 24 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 25 | .andExpect(jsonPath('$.openapi', is('3.0.1'))) 26 | .andExpect(jsonPath('$.info', isA(Object.class))) 27 | .andExpect(jsonPath('$.servers', isA(Object.class))) 28 | .andExpect(jsonPath('$.paths', isA(Object.class))) 29 | .andExpect(jsonPath('$.components', isA(Object.class))) 30 | } 31 | 32 | def "get api docs - swagger config"() throws Exception { 33 | 34 | expect: 35 | mockMvc.perform( 36 | get('/v3/api-docs/swagger-config') 37 | ) 38 | .andExpect(status().isOk()) 39 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 40 | .andExpect(jsonPath('$.configUrl', isA(String.class))) 41 | .andExpect(jsonPath('$.oauth2RedirectUrl', isA(String.class))) 42 | .andExpect(jsonPath('$.url', isA(String.class))) 43 | .andExpect(jsonPath('$.validatorUrl', isA(String.class))) 44 | } 45 | 46 | def "get swagger html"() throws Exception { 47 | 48 | expect: 49 | mockMvc.perform( 50 | get('/swagger-ui.html') 51 | ) 52 | .andExpect(status().isFound()) 53 | 54 | } 55 | 56 | def "get swagger html index"() throws Exception { 57 | 58 | expect: 59 | mockMvc.perform( 60 | get('/swagger-ui/index.html') 61 | ) 62 | .andExpect(status().isOk()) 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/repository/PersonRepositorySpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.repository 2 | 3 | import com.droidablebee.springboot.rest.domain.Person 4 | import jakarta.persistence.EntityManager 5 | import jakarta.persistence.EntityNotFoundException 6 | import jakarta.persistence.PersistenceContext 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest 10 | import spock.lang.Specification 11 | 12 | @DataJpaTest 13 | /* 14 | By default @DataJpaTest uses embeded h2 databaze and ignores the connection string declared in application.properties. 15 | Annotation @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) disables this behavior. 16 | */ 17 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 18 | class PersonRepositorySpec extends Specification { 19 | 20 | @Autowired 21 | PersonRepository personRepository 22 | 23 | @PersistenceContext 24 | EntityManager entityManager 25 | 26 | def getOne() { 27 | 28 | when: 29 | Person person = personRepository.getOne(Long.MAX_VALUE) 30 | 31 | then: 32 | person 33 | 34 | and: "accessing id won't throw an exception" 35 | person.getId() 36 | 37 | when: "accessing the Entity's reference state should cause jakarta.persistence.EntityNotFoundException" 38 | person.getFirstName() 39 | 40 | then: 41 | thrown(EntityNotFoundException) 42 | } 43 | 44 | def getReferenceUsingEntityManager() { 45 | 46 | when: 47 | Person person = entityManager.getReference(Person.class, Long.MAX_VALUE) 48 | 49 | then: 50 | person 51 | 52 | and: "accessing id won't throw an exception" 53 | person.getId() 54 | 55 | when: "accessing the Entity's reference state should cause jakarta.persistence.EntityNotFoundException" 56 | person.getFirstName() 57 | 58 | then: 59 | thrown(EntityNotFoundException) 60 | } 61 | 62 | def findByIdUsingOptional() { 63 | 64 | when: 65 | Optional optional = personRepository.findById(Long.MAX_VALUE) 66 | 67 | then: 68 | optional != null 69 | !optional 70 | optional.empty 71 | } 72 | 73 | def findByIdUsingEntityManager() { 74 | 75 | when: 76 | Person person = entityManager.find(Person.class, Long.MAX_VALUE) 77 | 78 | then: 79 | !person 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/test/groovy/com/droidablebee/springboot/rest/service/CacheableServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.service 2 | 3 | 4 | import org.spockframework.spring.SpringBean 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.data.domain.Page 8 | import org.springframework.data.domain.PageRequest 9 | import spock.lang.Specification 10 | 11 | @SpringBootTest 12 | class CacheableServiceSpec extends Specification { 13 | 14 | @Autowired 15 | CacheableService cacheableService 16 | 17 | @SpringBean 18 | CacheableService.CacheableServiceClient cacheableServiceClient = Mock() 19 | 20 | void setup() { 21 | 22 | 0 * _ 23 | } 24 | 25 | def "retrieve - cached"() { 26 | 27 | given: 28 | PageRequest pageRequest1 = PageRequest.of(0, 10) 29 | PageRequest pageRequest2 = PageRequest.of(0, 20) 30 | Page page1 = Mock() 31 | Page page2 = Mock() 32 | 33 | when: "first service call is made" 34 | Page result = cacheableService.retrieve(pageRequest1) 35 | 36 | then: "client method is called" 37 | 1 * cacheableServiceClient.retrieve(pageRequest1) >> page1 38 | 39 | result == page1 40 | 41 | when: "subsequent service call is made" 42 | Page cached = cacheableService.retrieve(pageRequest1) 43 | 44 | then: "cached value is returned w/out client method call" 45 | cached == result 46 | 47 | when: "different parameter is used" 48 | result = cacheableService.retrieve(pageRequest2) 49 | 50 | then: "client method is called" 51 | 1 * cacheableServiceClient.retrieve(pageRequest2) >> page2 52 | 53 | result == page2 54 | 55 | when: "subsequent service call is made" 56 | cached = cacheableService.retrieve(pageRequest2) 57 | 58 | then: "cached value is returned w/out client method call" 59 | cached == result 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/endpoint/ActuatorEndpointStubbedTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.boot.test.mock.mockito.SpyBean; 6 | import org.springframework.util.LinkedMultiValueMap; 7 | 8 | import static org.hamcrest.Matchers.is; 9 | import static org.hamcrest.Matchers.isA; 10 | import static org.mockito.Mockito.when; 11 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 17 | 18 | @SpringBootTest 19 | public class ActuatorEndpointStubbedTest extends BaseEndpointTest { 20 | 21 | @SpyBean 22 | private CustomActuatorEndpoint customActuatorEndpoint; 23 | 24 | @SpyBean 25 | private InfoWebEndpointExtension infoWebEndpointExtension; 26 | 27 | @Test 28 | public void getCustomAuthorized() throws Exception { 29 | 30 | LinkedMultiValueMap custom = new LinkedMultiValueMap<>(); 31 | custom.add("custom1", 11); 32 | custom.add("custom1", 12); 33 | custom.add("custom2", 21); 34 | 35 | when(customActuatorEndpoint.createCustomMap()).thenReturn(custom); 36 | 37 | mockMvc.perform(get("/actuator/" + CustomActuatorEndpoint.CUSTOM).with(jwt())) 38 | .andDo(print()) 39 | .andExpect(status().isOk()) 40 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 41 | .andExpect(jsonPath("$.custom1", is(custom.get("custom1")))) 42 | .andExpect(jsonPath("$.custom2", is(custom.get("custom2")))) 43 | ; 44 | } 45 | 46 | @Test 47 | public void getInfoExtended() throws Exception { 48 | 49 | LinkedMultiValueMap custom = new LinkedMultiValueMap<>(); 50 | custom.add("custom1", 11); 51 | custom.add("custom1", 12); 52 | custom.add("custom2", 21); 53 | 54 | when(infoWebEndpointExtension.createCustomMap()).thenReturn(custom); 55 | 56 | mockMvc.perform(get("/actuator/info")) 57 | .andDo(print()) 58 | .andExpect(status().isOk()) 59 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 60 | .andExpect(jsonPath("$.build", isA(Object.class))) 61 | .andExpect(jsonPath("$.build.version", isA(String.class))) 62 | .andExpect(jsonPath("$.build.artifact", is("spring-boot-rest-example"))) 63 | .andExpect(jsonPath("$.build.group", is("com.droidablebee"))) 64 | .andExpect(jsonPath("$.build.time", isA(Number.class))) 65 | .andExpect(jsonPath("$.custom1", is(custom.get("custom1")))) 66 | .andExpect(jsonPath("$.custom2", is(custom.get("custom2")))) 67 | ; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/endpoint/ActuatorEndpointTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import net.minidev.json.JSONArray; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import static org.hamcrest.Matchers.is; 8 | import static org.hamcrest.Matchers.isA; 9 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; 10 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 15 | 16 | @SpringBootTest 17 | public class ActuatorEndpointTest extends BaseEndpointTest { 18 | 19 | @Test 20 | public void getInfo() throws Exception { 21 | 22 | mockMvc.perform(get("/actuator/info")) 23 | .andDo(print()) 24 | .andExpect(status().isOk()) 25 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 26 | .andExpect(jsonPath("$.build", isA(Object.class))) 27 | .andExpect(jsonPath("$.build.version", isA(String.class))) 28 | .andExpect(jsonPath("$.build.artifact", is("spring-boot-rest-example"))) 29 | .andExpect(jsonPath("$.build.group", is("com.droidablebee"))) 30 | .andExpect(jsonPath("$.build.time", isA(Number.class))) 31 | ; 32 | } 33 | 34 | @Test 35 | public void getHealth() throws Exception { 36 | 37 | mockMvc.perform(get("/actuator/health")) 38 | .andDo(print()) 39 | .andExpect(status().isOk()) 40 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 41 | .andExpect(jsonPath("$.status", is("UP"))) 42 | .andExpect(jsonPath("$.components").doesNotExist()) 43 | ; 44 | } 45 | 46 | @Test 47 | public void getHealthAuthorized() throws Exception { 48 | 49 | mockMvc.perform(get("/actuator/health").with(jwt())) 50 | .andDo(print()) 51 | .andExpect(status().isOk()) 52 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 53 | .andExpect(jsonPath("$.status", is("UP"))) 54 | .andExpect(jsonPath("$.components").doesNotExist()) 55 | ; 56 | } 57 | 58 | @Test 59 | public void getHealthAuthorizedWithConfiguredRole() throws Exception { 60 | 61 | mockMvc.perform(get("/actuator/health").with(jwtWithScope("health-details"))) 62 | .andDo(print()) 63 | .andExpect(status().isOk()) 64 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 65 | .andExpect(jsonPath("$.status", is("UP"))) 66 | .andExpect(jsonPath("$.components", isA(Object.class))) 67 | .andExpect(jsonPath("$.components.diskSpace.status", is("UP"))) 68 | .andExpect(jsonPath("$.components.diskSpace.details", isA(Object.class))) 69 | .andExpect(jsonPath("$.components.db.status", is("UP"))) 70 | .andExpect(jsonPath("$.components.db.details", isA(Object.class))) 71 | .andExpect(jsonPath("$.components.ping.status", is("UP"))) 72 | ; 73 | } 74 | 75 | @Test 76 | public void getEnv() throws Exception { 77 | 78 | mockMvc.perform(get("/actuator/env")) 79 | .andDo(print()) 80 | .andExpect(status().isUnauthorized()) 81 | ; 82 | } 83 | 84 | @Test 85 | public void getEnvAuthorized() throws Exception { 86 | 87 | mockMvc.perform(get("/actuator/env").with(jwt())) 88 | .andDo(print()) 89 | .andExpect(status().isOk()) 90 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 91 | .andExpect(jsonPath("$.activeProfiles", isA(JSONArray.class))) 92 | .andExpect(jsonPath("$.propertySources", isA(JSONArray.class))) 93 | ; 94 | } 95 | 96 | @Test 97 | public void getCustom() throws Exception { 98 | 99 | mockMvc.perform(get("/actuator/" + CustomActuatorEndpoint.CUSTOM)) 100 | .andDo(print()) 101 | .andExpect(status().isUnauthorized()) 102 | ; 103 | } 104 | 105 | @Test 106 | public void getCustomAuthorized() throws Exception { 107 | 108 | mockMvc.perform(get("/actuator/" + CustomActuatorEndpoint.CUSTOM).with(jwt())) 109 | .andDo(print()) 110 | .andExpect(status().isOk()) 111 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 112 | .andExpect(content().string("{}")) 113 | ; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/endpoint/BaseEndpointTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.mockito.Mock; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.security.oauth2.jwt.BadJwtException; 13 | import org.springframework.security.oauth2.jwt.Jwt; 14 | import org.springframework.security.oauth2.jwt.JwtDecoder; 15 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import java.io.IOException; 19 | 20 | import static org.mockito.ArgumentMatchers.anyString; 21 | import static org.mockito.Mockito.when; 22 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; 23 | 24 | /** 25 | * AbstractEndpointTest with common test methods. 26 | */ 27 | @AutoConfigureMockMvc //this creates MockMvc instance correctly, including wiring of the spring security 28 | public abstract class BaseEndpointTest { 29 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 30 | 31 | protected static final MediaType JSON_MEDIA_TYPE = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype()); 32 | 33 | @Autowired 34 | ObjectMapper objectMapper; 35 | 36 | @Autowired 37 | protected MockMvc mockMvc; 38 | 39 | @MockBean 40 | protected JwtDecoder jwtDecoder; 41 | 42 | @Mock 43 | protected Jwt jwt; 44 | 45 | /** 46 | * @BeforeEach methods are inherited from superclasses as long as they are not overridden. 47 | * Hence, the different method name. 48 | */ 49 | @BeforeEach 50 | public void setupBase() { 51 | 52 | when(jwtDecoder.decode(anyString())).thenAnswer( 53 | invocation -> { 54 | String token = invocation.getArgument(0); 55 | if ("invalid".equals(token)) { 56 | throw new BadJwtException("Token is invalid"); 57 | } else { 58 | return jwt; 59 | } 60 | } 61 | ); 62 | } 63 | 64 | /** 65 | * Returns json representation of the object. 66 | * 67 | * @param o instance 68 | * @return json 69 | * @throws IOException 70 | */ 71 | protected String json(Object o) throws IOException { 72 | 73 | return objectMapper.writeValueAsString(o); 74 | } 75 | 76 | protected SecurityMockMvcRequestPostProcessors.JwtRequestPostProcessor jwtWithScope(String scope) { 77 | 78 | return jwt().jwt(jwt -> jwt.claims(claims -> claims.put("scope", scope))); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/endpoint/PersonEndpointStubbedTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import com.droidablebee.springboot.rest.domain.Person; 4 | import com.droidablebee.springboot.rest.service.PersonService; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.mock.mockito.MockBean; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import static com.droidablebee.springboot.rest.endpoint.PersonEndpoint.PERSON_READ_PERMISSION; 12 | import static org.hamcrest.Matchers.is; 13 | import static org.hamcrest.Matchers.nullValue; 14 | import static org.mockito.Mockito.when; 15 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 20 | 21 | @SpringBootTest 22 | @Transactional 23 | public class PersonEndpointStubbedTest extends BaseEndpointTest { 24 | 25 | @MockBean 26 | private PersonService personService; 27 | 28 | private Person testPerson; 29 | 30 | @BeforeEach 31 | public void setup() { 32 | 33 | testPerson = new Person(1L, "Jack", "Bauer"); 34 | when(personService.findOne(1L)).thenReturn(testPerson); 35 | 36 | when(jwt.hasClaim("scope")).thenReturn(true); 37 | when(jwt.getClaim("scope")).thenReturn(PERSON_READ_PERMISSION); 38 | } 39 | 40 | @Test 41 | public void getPersonById() throws Exception { 42 | 43 | mockMvc.perform(get("/v1/person/{id}", 1) 44 | .header("Authorization", "Bearer valid") 45 | ) 46 | .andDo(print()) 47 | .andExpect(status().isOk()) 48 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 49 | .andExpect(jsonPath("$.id", is(testPerson.getId().intValue()))) 50 | .andExpect(jsonPath("$.firstName", is(testPerson.getFirstName()))) 51 | .andExpect(jsonPath("$.lastName", is(testPerson.getLastName()))) 52 | ; 53 | } 54 | 55 | @Test 56 | public void handleGenericException() throws Exception { 57 | 58 | String message = "Failed to get person by id"; 59 | when(personService.findOne(1L)).thenThrow(new RuntimeException(message)); 60 | 61 | mockMvc.perform(get("/v1/person/{id}", 1) 62 | .header("Authorization", "Bearer valid") 63 | ) 64 | .andDo(print()) 65 | .andExpect(status().is5xxServerError()) 66 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 67 | .andExpect(jsonPath("$.field", nullValue())) 68 | .andExpect(jsonPath("$.value", nullValue())) 69 | .andExpect(jsonPath("$.message", is(message))) 70 | ; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/endpoint/PersonEndpointTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import com.droidablebee.springboot.rest.domain.Address; 4 | import com.droidablebee.springboot.rest.domain.Person; 5 | import com.droidablebee.springboot.rest.service.PersonService; 6 | import jakarta.persistence.EntityManager; 7 | import net.minidev.json.JSONArray; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.data.domain.PageRequest; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import java.util.Date; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | import static com.droidablebee.springboot.rest.endpoint.PersonEndpoint.PERSON_READ_PERMISSION; 21 | import static com.droidablebee.springboot.rest.endpoint.PersonEndpoint.PERSON_WRITE_PERMISSION; 22 | import static org.hamcrest.Matchers.containsString; 23 | import static org.hamcrest.Matchers.hasItem; 24 | import static org.hamcrest.Matchers.is; 25 | import static org.hamcrest.Matchers.isA; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertNotNull; 28 | import static org.mockito.Mockito.when; 29 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 30 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 31 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 32 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 35 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 36 | 37 | @SpringBootTest 38 | @Transactional 39 | public class PersonEndpointTest extends BaseEndpointTest { 40 | 41 | @Autowired 42 | private EntityManager entityManager; 43 | 44 | @Autowired 45 | private PersonService personService; 46 | 47 | private Person testPerson; 48 | private long timestamp; 49 | 50 | @BeforeEach 51 | public void setup() { 52 | 53 | timestamp = new Date().getTime(); 54 | 55 | // create test persons 56 | personService.save(createPerson("Jack", "Bauer")); 57 | personService.save(createPerson("Chloe", "O'Brian")); 58 | personService.save(createPerson("Kim", "Bauer")); 59 | personService.save(createPerson("David", "Palmer")); 60 | personService.save(createPerson("Michelle", "Dessler")); 61 | 62 | Page persons = personService.findAll(PageRequest.of(0, PersonEndpoint.DEFAULT_PAGE_SIZE)); 63 | assertNotNull(persons); 64 | assertEquals(5L, persons.getTotalElements()); 65 | 66 | testPerson = persons.getContent().get(0); 67 | 68 | //refresh entity with any changes that have been done during persistence including Hibernate conversion 69 | //example: java.util.Date field is injected with either with java.sql.Date (if @Temporal(TemporalType.DATE) is used) 70 | //or java.sql.Timestamp 71 | entityManager.refresh(testPerson); 72 | } 73 | 74 | @Test 75 | public void getPersonByIdUnauthorizedNoToken() throws Exception { 76 | Long id = testPerson.getId(); 77 | 78 | mockMvc.perform(get("/v1/person/{id}", id)) 79 | .andDo(print()) 80 | .andExpect(status().isUnauthorized()) 81 | ; 82 | } 83 | 84 | @Test 85 | public void getPersonByIdUnauthorizedInvalidToken() throws Exception { 86 | Long id = testPerson.getId(); 87 | 88 | mockMvc.perform(get("/v1/person/{id}", id) 89 | .header("Authorization", "Bearer invalid") 90 | ) 91 | .andDo(print()) 92 | .andExpect(status().isUnauthorized()) 93 | ; 94 | } 95 | 96 | @Test 97 | public void getPersonByIdForbiddenInvalidScope() throws Exception { 98 | Long id = testPerson.getId(); 99 | 100 | mockMvc.perform(get("/v1/person/{id}", id) 101 | .header("Authorization", "Bearer valid") 102 | ) 103 | .andDo(print()) 104 | .andExpect(status().isForbidden()) 105 | ; 106 | } 107 | 108 | @Test 109 | public void getPersonById() throws Exception { 110 | Long id = testPerson.getId(); 111 | 112 | when(jwt.hasClaim("scope")).thenReturn(true); 113 | when(jwt.getClaim("scope")).thenReturn(PERSON_READ_PERMISSION); 114 | 115 | mockMvc.perform(get("/v1/person/{id}", id) 116 | .header("Authorization", "Bearer valid") 117 | ) 118 | .andDo(print()) 119 | .andExpect(status().isOk()) 120 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 121 | .andExpect(jsonPath("$.id", is(id.intValue()))) 122 | .andExpect(jsonPath("$.firstName", is(testPerson.getFirstName()))) 123 | .andExpect(jsonPath("$.lastName", is(testPerson.getLastName()))) 124 | .andExpect(jsonPath("$.dateOfBirth", isA(Number.class))) 125 | ; 126 | } 127 | 128 | @Test 129 | public void getAll() throws Exception { 130 | 131 | Page persons = personService.findAll(PageRequest.of(0, PersonEndpoint.DEFAULT_PAGE_SIZE)); 132 | 133 | when(jwt.hasClaim("scope")).thenReturn(true); 134 | when(jwt.getClaim("scope")).thenReturn(PERSON_READ_PERMISSION); 135 | 136 | mockMvc.perform(get("/v1/persons") 137 | .header("Authorization", "Bearer valid") 138 | ) 139 | .andDo(print()) 140 | .andExpect(status().isOk()) 141 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 142 | .andExpect(jsonPath("$.content.size()", is((int) persons.getTotalElements()))) 143 | ; 144 | } 145 | 146 | /** 147 | * Test JSR-303 bean validation. 148 | */ 149 | @Test 150 | public void createPersonValidationErrorLastName() throws Exception { 151 | 152 | //person with missing last name 153 | Person person = createPerson("first", null); 154 | person.setMiddleName("middle"); 155 | String content = json(person); 156 | mockMvc.perform( 157 | put("/v1/person") 158 | .header("Authorization", "Bearer valid") 159 | .accept(JSON_MEDIA_TYPE) 160 | .content(content) 161 | .contentType(JSON_MEDIA_TYPE) 162 | ) 163 | .andDo(print()) 164 | .andExpect(status().isBadRequest()) 165 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 166 | .andExpect(jsonPath("$", isA(JSONArray.class))) 167 | .andExpect(jsonPath("$.length()", is(1))) 168 | .andExpect(jsonPath("$.[?(@.field == 'lastName')].message", hasItem("must not be null"))) 169 | ; 170 | } 171 | 172 | /** 173 | * Test custom bean validation. 174 | */ 175 | @Test 176 | public void createPersonValidationErrorMiddleName() throws Exception { 177 | 178 | //person with missing middle name - custom validation 179 | Person person = createPerson("first", "last"); 180 | String content = json(person); 181 | mockMvc.perform( 182 | put("/v1/person") 183 | .header("Authorization", "Bearer valid") 184 | .accept(JSON_MEDIA_TYPE) 185 | .content(content) 186 | .contentType(JSON_MEDIA_TYPE)) 187 | .andDo(print()) 188 | .andExpect(status().isBadRequest()) 189 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 190 | .andExpect(jsonPath("$", isA(JSONArray.class))) 191 | .andExpect(jsonPath("$.length()", is(1))) 192 | .andExpect(jsonPath("$.[?(@.field == 'middleName')].message", hasItem("middle name is required"))) 193 | ; 194 | } 195 | 196 | /** 197 | * Test JSR-303 bean object graph validation with nested entities. 198 | */ 199 | @Test 200 | public void createPersonValidationAddress() throws Exception { 201 | 202 | Person person = createPerson("first", "last"); 203 | person.setMiddleName("middle"); 204 | person.addAddress(new Address("line1", "city", "state", "zip")); 205 | person.addAddress(new Address()); //invalid address 206 | 207 | String content = json(person); 208 | mockMvc.perform( 209 | put("/v1/person") 210 | .header("Authorization", "Bearer valid") 211 | .accept(JSON_MEDIA_TYPE) 212 | .content(content) 213 | .contentType(JSON_MEDIA_TYPE)) 214 | .andDo(print()) 215 | .andExpect(status().isBadRequest()) 216 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 217 | .andExpect(jsonPath("$.length()", is(4))) 218 | .andExpect(jsonPath("$.[?(@.field == 'addresses[].line1')].message", hasItem("must not be null"))) 219 | .andExpect(jsonPath("$.[?(@.field == 'addresses[].state')].message", hasItem("must not be null"))) 220 | .andExpect(jsonPath("$.[?(@.field == 'addresses[].city')].message", hasItem("must not be null"))) 221 | .andExpect(jsonPath("$.[?(@.field == 'addresses[].zip')].message", hasItem("must not be null"))) 222 | ; 223 | } 224 | 225 | @Test 226 | public void createPersonValidationToken() throws Exception { 227 | 228 | when(jwt.hasClaim("scope")).thenReturn(true); 229 | when(jwt.getClaim("scope")).thenReturn(PERSON_WRITE_PERMISSION); 230 | 231 | Person person = createPerson("first", "last"); 232 | person.setMiddleName("middle"); 233 | 234 | String content = json(person); 235 | mockMvc.perform( 236 | put("/v1/person") 237 | .header("Authorization", "Bearer valid") 238 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 239 | .header(PersonEndpoint.HEADER_TOKEN, "1") //invalid token 240 | .accept(JSON_MEDIA_TYPE) 241 | .content(content) 242 | .contentType(JSON_MEDIA_TYPE)) 243 | .andDo(print()) 244 | .andExpect(status().isBadRequest()) 245 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 246 | .andExpect(jsonPath("$.length()", is(1))) 247 | .andExpect(jsonPath("$.[?(@.field == 'add.token')].message", hasItem("token size 2-40"))) 248 | ; 249 | } 250 | 251 | @Test 252 | public void createPersonValidationUserId() throws Exception { 253 | 254 | Person person = createPerson("first", "last"); 255 | person.setMiddleName("middle"); 256 | 257 | String content = json(person); 258 | mockMvc.perform( 259 | put("/v1/person") 260 | .header("Authorization", "Bearer valid") 261 | .accept(JSON_MEDIA_TYPE) 262 | .content(content) 263 | .contentType(JSON_MEDIA_TYPE)) 264 | .andDo(print()) 265 | .andExpect(status().isBadRequest()) 266 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 267 | //"Required request header 'userId' for method parameter type String is not present" 268 | .andExpect(jsonPath("$.message", containsString("Required request header '" + PersonEndpoint.HEADER_USER_ID))) 269 | ; 270 | } 271 | 272 | @Test 273 | public void createPersonUnauthorized() throws Exception { 274 | 275 | Person person = createPerson("first", "last"); 276 | person.setMiddleName("middleName"); 277 | 278 | String content = json(person); 279 | 280 | mockMvc.perform( 281 | put("/v1/person") 282 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 283 | .accept(JSON_MEDIA_TYPE) 284 | .content(content) 285 | .contentType(JSON_MEDIA_TYPE)) 286 | .andDo(print()) 287 | .andExpect(status().isUnauthorized()) 288 | ; 289 | } 290 | 291 | @Test 292 | public void createPersonForbiddenInvalidScope() throws Exception { 293 | 294 | when(jwt.hasClaim("scope")).thenReturn(true); 295 | when(jwt.getClaim("scope")).thenReturn(PERSON_READ_PERMISSION); 296 | 297 | Person person = createPerson("first", "last"); 298 | person.setMiddleName("middleName"); 299 | 300 | String content = json(person); 301 | 302 | mockMvc.perform( 303 | put("/v1/person") 304 | .header("Authorization", "Bearer valid") 305 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 306 | .accept(JSON_MEDIA_TYPE) 307 | .content(content) 308 | .contentType(JSON_MEDIA_TYPE)) 309 | .andDo(print()) 310 | .andExpect(status().isForbidden()) 311 | ; 312 | } 313 | 314 | @Test 315 | public void createPerson() throws Exception { 316 | 317 | when(jwt.hasClaim("scope")).thenReturn(true); 318 | when(jwt.getClaim("scope")).thenReturn(List.of(PERSON_READ_PERMISSION, PERSON_WRITE_PERMISSION)); 319 | 320 | Person person = createPerson("first", "last"); 321 | person.setMiddleName("middleName"); 322 | 323 | String content = json(person); 324 | 325 | mockMvc.perform( 326 | put("/v1/person") 327 | .header("Authorization", "Bearer valid") 328 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 329 | .accept(JSON_MEDIA_TYPE) 330 | .content(content) 331 | .contentType(JSON_MEDIA_TYPE)) 332 | .andDo(print()) 333 | .andExpect(status().isOk()) 334 | .andExpect(jsonPath("$.id", isA(Number.class))) 335 | .andExpect(jsonPath("$.firstName", is(person.getFirstName()))) 336 | .andExpect(jsonPath("$.lastName", is(person.getLastName()))) 337 | .andExpect(jsonPath("$.dateOfBirth", isA(Number.class))) 338 | .andExpect(jsonPath("$.dateOfBirth", is(person.getDateOfBirth().getTime()))) 339 | ; 340 | } 341 | 342 | @Test 343 | public void createPersonWithDateVerification() throws Exception { 344 | 345 | when(jwt.hasClaim("scope")).thenReturn(true); 346 | when(jwt.getClaim("scope")).thenReturn(PERSON_WRITE_PERMISSION); 347 | 348 | Person person = createPerson("first", "last"); 349 | person.setMiddleName("middleName"); 350 | 351 | String content = json(person); 352 | 353 | mockMvc.perform( 354 | put("/v1/person") 355 | .header("Authorization", "Bearer valid") 356 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 357 | .accept(JSON_MEDIA_TYPE) 358 | .content(content) 359 | .contentType(JSON_MEDIA_TYPE)) 360 | .andExpect(status().isOk()) 361 | .andExpect(jsonPath("$.id", isA(Number.class))) 362 | .andExpect(jsonPath("$.firstName", is(person.getFirstName()))) 363 | .andExpect(jsonPath("$.lastName", is(person.getLastName()))) 364 | .andExpect(jsonPath("$.dateOfBirth", isA(Number.class))) 365 | .andExpect(jsonPath("$.dateOfBirth", is(person.getDateOfBirth().getTime()))) 366 | ; 367 | 368 | } 369 | 370 | @Test 371 | public void requestBodyValidationInvalidJsonValue() throws Exception { 372 | 373 | testPerson.setGender(Person.Gender.M); 374 | String content = json(testPerson); 375 | //payload with invalid gender 376 | content = content.replaceFirst("(\"gender\":\")(M)(\")", "$1Q$3"); 377 | 378 | mockMvc.perform( 379 | put("/v1/person") 380 | .header("Authorization", "Bearer valid") 381 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 382 | .accept(JSON_MEDIA_TYPE) 383 | .content(content) 384 | .contentType(JSON_MEDIA_TYPE)) 385 | .andDo(print()) 386 | .andExpect(status().isBadRequest()) 387 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 388 | .andExpect(jsonPath("$.message", containsString("Cannot deserialize value of type `com.droidablebee.springboot.rest.domain.Person$Gender`"))) 389 | ; 390 | } 391 | 392 | @Test 393 | public void requestBodyValidationInvalidJson() throws Exception { 394 | 395 | String content = json("not valid json"); 396 | mockMvc.perform( 397 | put("/v1/person") 398 | .header("Authorization", "Bearer valid") 399 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 400 | .accept(JSON_MEDIA_TYPE) 401 | .content(content) 402 | .contentType(JSON_MEDIA_TYPE)) 403 | .andDo(print()) 404 | .andExpect(status().isBadRequest()) 405 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 406 | .andExpect(jsonPath("$.message", containsString("Cannot construct instance of `com.droidablebee.springboot.rest.domain.Person`"))) 407 | ; 408 | } 409 | 410 | @Test 411 | public void handleHttpRequestMethodNotSupportedException() throws Exception { 412 | 413 | String content = json(testPerson); 414 | 415 | mockMvc.perform( 416 | delete("/v1/person") //not supported method 417 | .header("Authorization", "Bearer valid") 418 | .header(PersonEndpoint.HEADER_USER_ID, UUID.randomUUID()) 419 | .accept(JSON_MEDIA_TYPE) 420 | .content(content) 421 | .contentType(JSON_MEDIA_TYPE)) 422 | .andDo(print()) 423 | .andExpect(status().isMethodNotAllowed()) 424 | .andExpect(content().string("")) 425 | ; 426 | } 427 | 428 | private Person createPerson(String first, String last) { 429 | Person person = new Person(first, last); 430 | person.setDateOfBirth(new Date(timestamp)); 431 | return person; 432 | } 433 | 434 | } 435 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/endpoint/SwaggerEndpointTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.endpoint; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.test.web.servlet.MvcResult; 6 | 7 | import static org.hamcrest.Matchers.is; 8 | import static org.hamcrest.Matchers.isA; 9 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 13 | 14 | @SpringBootTest 15 | public class SwaggerEndpointTest extends BaseEndpointTest { 16 | 17 | @Test 18 | public void getApiDocs() throws Exception { 19 | 20 | MvcResult result = mockMvc.perform(get("/v3/api-docs")) 21 | .andExpect(status().isOk()) 22 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 23 | .andExpect(jsonPath("$.openapi", is("3.0.1"))) 24 | .andExpect(jsonPath("$.info", isA(Object.class))) 25 | .andExpect(jsonPath("$.servers", isA(Object.class))) 26 | .andExpect(jsonPath("$.paths", isA(Object.class))) 27 | .andExpect(jsonPath("$.components", isA(Object.class))) 28 | .andReturn(); 29 | 30 | logger.debug("content=" + result.getResponse().getContentAsString()); 31 | } 32 | 33 | @Test 34 | public void getApiDocSwaggerConfig() throws Exception { 35 | 36 | mockMvc.perform(get("/v3/api-docs/swagger-config")) 37 | .andExpect(status().isOk()) 38 | .andExpect(content().contentType(JSON_MEDIA_TYPE)) 39 | .andExpect(jsonPath("$.configUrl", isA(String.class))) 40 | .andExpect(jsonPath("$.oauth2RedirectUrl", isA(String.class))) 41 | .andExpect(jsonPath("$.url", isA(String.class))) 42 | .andExpect(jsonPath("$.validatorUrl", isA(String.class))) 43 | ; 44 | } 45 | 46 | @Test 47 | public void getSwaggerHtml() throws Exception { 48 | 49 | mockMvc.perform(get("/swagger-ui.html")) 50 | .andExpect(status().isFound()) 51 | ; 52 | } 53 | 54 | @Test 55 | public void getSwaggerHtmlIndex() throws Exception { 56 | 57 | mockMvc.perform(get("/swagger-ui/index.html")) 58 | .andExpect(status().isOk()) 59 | ; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/droidablebee/springboot/rest/repository/PersonRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.droidablebee.springboot.rest.repository; 2 | 3 | import com.droidablebee.springboot.rest.domain.Person; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.persistence.PersistenceContext; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | 11 | import java.util.Optional; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | import static org.junit.jupiter.api.Assertions.assertNotNull; 15 | import static org.junit.jupiter.api.Assertions.assertNull; 16 | import static org.junit.jupiter.api.Assertions.assertThrows; 17 | 18 | @DataJpaTest 19 | /* 20 | By default @DataJpaTest uses embeded h2 databaze and ignores the connection string declared in application.properties. 21 | Annotation @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) disables this behavior. 22 | */ 23 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 24 | public class PersonRepositoryTest { 25 | 26 | @Autowired 27 | PersonRepository personRepository; 28 | 29 | @PersistenceContext 30 | EntityManager entityManager; 31 | 32 | @Test 33 | public void getOne() { 34 | 35 | Person person = personRepository.getOne(Long.MAX_VALUE); 36 | assertNotNull(person); 37 | //access to the Entity's reference state should cause jakarta.persistence.EntityNotFoundException 38 | assertNotNull(person.getId()); // accessing id won't throw an exception 39 | assertThrows(jakarta.persistence.EntityNotFoundException.class, () -> person.getFirstName()); 40 | } 41 | 42 | @Test 43 | public void getReferenceUsingEntityManager() { 44 | 45 | Person person = entityManager.getReference(Person.class, Long.MAX_VALUE); 46 | assertNotNull(person); 47 | //access to the Entity's reference state should cause jakarta.persistence.EntityNotFoundException 48 | assertNotNull(person.getId()); // accessing id won't throw an exception 49 | assertThrows(jakarta.persistence.EntityNotFoundException.class, () -> person.getFirstName()); 50 | } 51 | 52 | @Test 53 | public void findByIdUsingOptional() { 54 | 55 | Optional optional = personRepository.findById(Long.MAX_VALUE); 56 | assertNotNull(optional); 57 | assertFalse(optional.isPresent()); 58 | assertThrows(java.util.NoSuchElementException.class, () -> optional.get()); 59 | } 60 | 61 | @Test 62 | public void findByIdUsingEntityManager() { 63 | 64 | Person person = entityManager.find(Person.class, Long.MAX_VALUE); 65 | assertNull(person); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/test/resources/application-default.yml: -------------------------------------------------------------------------------- 1 | # override any values in application.yml using default profile for testing 2 | management: 3 | endpoints: 4 | web: 5 | exposure: 6 | include: info,health,env,custom 7 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------