├── .codebeatignore ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── learning │ │ └── by │ │ └── example │ │ └── reactive │ │ └── microservices │ │ ├── application │ │ ├── ApplicationConfig.java │ │ └── ReactiveMsApplication.java │ │ ├── exceptions │ │ ├── GeoLocationNotFoundException.java │ │ ├── GetGeoLocationException.java │ │ ├── GetSunriseSunsetException.java │ │ ├── InvalidParametersException.java │ │ └── PathNotFoundException.java │ │ ├── handlers │ │ ├── ApiHandler.java │ │ ├── ErrorHandler.java │ │ └── ThrowableTranslator.java │ │ ├── model │ │ ├── ErrorResponse.java │ │ ├── GeoLocationResponse.java │ │ ├── GeoTimesResponse.java │ │ ├── GeographicCoordinates.java │ │ ├── LocationRequest.java │ │ ├── LocationResponse.java │ │ └── SunriseSunset.java │ │ ├── routers │ │ ├── ApiRouter.java │ │ ├── MainRouter.java │ │ └── StaticRouter.java │ │ └── services │ │ ├── GeoLocationService.java │ │ ├── GeoLocationServiceImpl.java │ │ ├── SunriseSunsetService.java │ │ └── SunriseSunsetServiceImpl.java └── resources │ ├── application.yaml │ ├── banner.txt │ └── public │ ├── api.yaml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.html │ ├── oauth2-redirect.html │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ └── swagger-ui.js.map └── test ├── java └── org │ └── learning │ └── by │ └── example │ └── reactive │ └── microservices │ ├── application │ ├── ReactiveMsApplicationTest.java │ └── ReactiveMsApplicationUnitTest.java │ ├── handlers │ ├── ApiHandlerTest.java │ ├── ErrorHandlerTest.java │ └── ThrowableTranslatorTest.java │ ├── model │ └── WrongRequest.java │ ├── routers │ ├── ApiRouterTest.java │ ├── MainRouterTest.java │ └── StaticRouterTest.java │ ├── services │ ├── GeoLocationServiceImplTest.java │ └── SunriseSunsetServiceImplTest.java │ └── test │ ├── BasicIntegrationTest.java │ ├── HandlersHelper.java │ ├── RestServiceHelper.java │ └── tags │ ├── IntegrationTest.java │ ├── SystemTest.java │ └── UnitTest.java └── resources ├── application-test.yaml └── json ├── GeoLocationResponse_EMPTY.json ├── GeoLocationResponse_NOT_FOUND.json ├── GeoLocationResponse_OK.json ├── GeoLocationResponse_WRONG_STATUS.json ├── GeoTimesResponse_EMPTY.json ├── GeoTimesResponse_KO.json └── GeoTimesResponse_OK.json /.codebeatignore: -------------------------------------------------------------------------------- 1 | src/main/resources/** 2 | src/test/resources/** 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningByExample/reactive-ms-example/c40d5855ce2c5e0b6dfb06aa4a421ae9560d6548/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | jdk: 4 | - oraclejdk8 5 | before_install: 6 | - chmod +x mvnw 7 | after_success: 8 | - bash <(curl -s https://codecov.io/bash) 9 | branches: 10 | only: 11 | - master 12 | - develop 13 | cache: 14 | directories: 15 | - .autoconf 16 | - $HOME/.m2 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Reactive Micro Services Example 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](/LICENSE) 3 | [![Build Status](https://travis-ci.org/LearningByExample/reactive-ms-example.svg?branch=master)](https://travis-ci.org/LearningByExample/reactive-ms-example) 4 | [![codecov](https://codecov.io/gh/LearningByExample/reactive-ms-example/branch/master/graph/badge.svg)](https://codecov.io/gh/LearningByExample/reactive-ms-example) 5 | [![codebeat badge](https://codebeat.co/badges/9f473a67-ab5a-4205-82fe-976e9bbb01e6)](https://codebeat.co/projects/github-com-learningbyexample-reactive-ms-example-master) 6 | 7 | ## info 8 | This is an example of doing reactive MicroServices using spring 5 functional web framework and spring boot 2. 9 | 10 | There is a [Kotlin fork](https://github.com/LearningByExample/KotlinReactiveMS) of this service. 11 | 12 | This service provide and API that will get the geo location and the sunrise and sunset times from an address. 13 | 14 | ```Gherkin 15 | Scenario: Get Location 16 | Given I've an address 17 | When I call the location service 18 | Then I should get a geo location 19 | And I should get the sunrise and sunset times 20 | ``` 21 | To implement this example we consume a couple of REST APIs. 22 | 23 | This example cover several topics: 24 | 25 | - Functional programing. 26 | - Reactive types. 27 | - Router Functions. 28 | - Static Web-Content. 29 | - Creation on Reactive Java Services/Components. 30 | - Error handling in routes and services. 31 | - Reactive Web Client to consume external REST Services. 32 | - Organizing your project in manageable packaging. 33 | 34 | Includes and in depth look to testing using JUnit5: 35 | - Unit, Integration and System tests. 36 | - Mocking, including reactive functions and JSON responses. 37 | - BDD style assertions. 38 | - Test tags with maven profiles. 39 | 40 | ## usage 41 | 42 | To run this service: 43 | 44 | ```shell 45 | $ mvnw spring-boot:run 46 | ``` 47 | 48 | ## Sample requests 49 | 50 | Get from address 51 | ```shell 52 | $ curl -X GET "http://localhost:8080/api/location/Trafalgar%20Square%2C%20London%2C%20England" -H "accept: application/json" 53 | ``` 54 | 55 | Post from JSON 56 | ```shell 57 | $ curl -X POST "http://localhost:8080/api/location" -H "accept: application/json" -H "content-type: application/json" -d "{ \"address\": \"Trafalgar Square, London, England\"}" 58 | ``` 59 | 60 | Both will produce something like: 61 | ```json 62 | { 63 | "geographicCoordinates": { 64 | "latitude": 51.508039, 65 | "longitude": -0.128069 66 | }, 67 | "sunriseSunset": { 68 | "sunrise": "2017-05-21T03:59:08+00:00", 69 | "sunset": "2017-05-21T19:55:11+00:00" 70 | } 71 | } 72 | ``` 73 | _All date and times are ISO 8601 UTC without summer time adjustment_ 74 | ## API 75 | [![View in the embedded Swagger UI](https://avatars0.githubusercontent.com/u/7658037?v=3&s=20) View in the embedded Swagger UI](http://localhost:8080/index.html) 76 | 77 | [![Run in Postman](https://lh4.googleusercontent.com/Dfqo9J42K7-xRvHW3GVpTU7YCa_zpy3kEDSIlKjpd2RAvVlNfZe5pn8Swaa4TgCWNTuOJOAfwWY=s20) Run in Postman](https://app.getpostman.com/run-collection/498aea143dc572212f17) 78 | 79 | ## Project Structure 80 | 81 | - [main/java](/src/main/java/org/learning/by/example/reactive/microservices) 82 | - [/application](/src/main/java/org/learning/by/example/reactive/microservices/application) : Main Spring boot application and context configuration. 83 | - [/routers](/src/main/java/org/learning/by/example/reactive/microservices/routers) : Reactive routing functions. 84 | - [/handlers](/src/main/java/org/learning/by/example/reactive/microservices/handlers) : Handlers used by the routers. 85 | - [/services](/src/main/java/org/learning/by/example/reactive/microservices/services) : Services for the business logic needed by handlers. 86 | - [/exceptions](/src/main/java/org/learning/by/example/reactive/microservices/exceptions) : Businesses exceptions. 87 | - [/model](/src/main/java/org/learning/by/example/reactive/microservices/model) : POJOs. 88 | - [test/java](/src/test/java/org/learning/by/example/reactive/microservices) 89 | - [/application](/src/test/java/org/learning/by/example/reactive/microservices/application) : Application system and unit tests. 90 | - [/routers](/src/test/java/org/learning/by/example/reactive/microservices/routers) : Integration tests for routes. 91 | - [/handlers](/src/test/java/org/learning/by/example/reactive/microservices/handlers) : Unit tests for handlers. 92 | - [/services](/src/test/java/org/learning/by/example/reactive/microservices/services) : Unit tests for services. 93 | - [/model](/src/test/java/org/learning/by/example/reactive/microservices/model) : POJOs used by the test. 94 | - [/test](/src/test/java/org/learning/by/example/reactive/microservices/test) : Helpers and base classes for testing. 95 | 96 | ## References 97 | 98 | - https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework 99 | - https://spring.io/blog/2017/02/23/spring-framework-5-0-m5-update 100 | - http://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven 101 | - https://github.com/junit-team/junit5-samples 102 | - https://developers.google.com/maps/documentation/geocoding/intro 103 | - https://sunrise-sunset.org/api 104 | - https://en.wikipedia.org/wiki/ISO_8601 105 | -------------------------------------------------------------------------------- /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 | # Maven2 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 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /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 Maven2 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 key stroke 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 enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 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 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.learning.by.example.reactive.microservices 7 | reactive-ms-example 8 | 1.1.2 9 | jar 10 | 11 | reactive-ms-example 12 | An educational project to learn reactive programming with Spring 5 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 5.1.0 26 | 1.1.0 27 | 2.19.1 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-webflux 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-test 39 | test 40 | 41 | 42 | 43 | org.jsoup 44 | jsoup 45 | 1.10.2 46 | test 47 | 48 | 49 | 50 | org.mockito 51 | mockito-core 52 | test 53 | 54 | 55 | 56 | org.junit.jupiter 57 | junit-jupiter-api 58 | ${junit.jupiter.version} 59 | test 60 | 61 | 62 | 63 | org.junit.jupiter 64 | junit-jupiter-engine 65 | ${junit.jupiter.version} 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | UnitAndIntegrationTests 74 | 75 | true 76 | 77 | 78 | 79 | 80 | maven-surefire-plugin 81 | ${maven.surefire.plugin.version} 82 | 83 | 84 | UnitTest,IntegrationTest 85 | 86 | 87 | 88 | 89 | org.junit.platform 90 | junit-platform-surefire-provider 91 | ${junit.platform.version} 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | IntegrationTests 100 | 101 | 102 | 103 | maven-surefire-plugin 104 | ${maven.surefire.plugin.version} 105 | 106 | 107 | IntegrationTest 108 | 109 | 110 | 111 | 112 | org.junit.platform 113 | junit-platform-surefire-provider 114 | ${junit.platform.version} 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | UnitTests 123 | 124 | 125 | 126 | maven-surefire-plugin 127 | ${maven.surefire.plugin.version} 128 | 129 | 130 | UnitTest 131 | 132 | 133 | 134 | 135 | org.junit.platform 136 | junit-platform-surefire-provider 137 | ${junit.platform.version} 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | SystemTests 146 | 147 | 148 | 149 | maven-surefire-plugin 150 | ${maven.surefire.plugin.version} 151 | 152 | 153 | SystemTest 154 | 155 | 156 | 157 | 158 | org.junit.platform 159 | junit-platform-surefire-provider 160 | ${junit.platform.version} 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | org.springframework.boot 174 | spring-boot-maven-plugin 175 | 176 | 177 | org.jacoco 178 | jacoco-maven-plugin 179 | 0.8.1 180 | 181 | 182 | 183 | prepare-agent 184 | 185 | 186 | 187 | report 188 | test 189 | 190 | report 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | spring-snapshots 201 | Spring Snapshots 202 | https://repo.spring.io/snapshot 203 | 204 | true 205 | 206 | 207 | 208 | spring-milestones 209 | Spring Milestones 210 | https://repo.spring.io/milestone 211 | 212 | false 213 | 214 | 215 | 216 | 217 | 218 | 219 | spring-snapshots 220 | Spring Snapshots 221 | https://repo.spring.io/snapshot 222 | 223 | true 224 | 225 | 226 | 227 | spring-milestones 228 | Spring Milestones 229 | https://repo.spring.io/milestone 230 | 231 | false 232 | 233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/application/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.application; 2 | 3 | import org.learning.by.example.reactive.microservices.handlers.ApiHandler; 4 | import org.learning.by.example.reactive.microservices.handlers.ErrorHandler; 5 | import org.learning.by.example.reactive.microservices.routers.MainRouter; 6 | import org.learning.by.example.reactive.microservices.services.*; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.web.reactive.config.EnableWebFlux; 11 | import org.springframework.web.reactive.function.server.RouterFunction; 12 | 13 | @Configuration 14 | @EnableWebFlux 15 | class ApplicationConfig { 16 | 17 | @Bean 18 | ApiHandler apiHandler(final GeoLocationService geoLocationService, final SunriseSunsetService sunriseSunsetService, 19 | final ErrorHandler errorHandler) { 20 | return new ApiHandler(geoLocationService, sunriseSunsetService, errorHandler); 21 | } 22 | 23 | @Bean 24 | GeoLocationService locationService(@Value("${GeoLocationServiceImpl.endPoint}") final String endPoint) { 25 | return new GeoLocationServiceImpl(endPoint); 26 | } 27 | 28 | @Bean 29 | SunriseSunsetService sunriseSunsetService(@Value("${SunriseSunsetServiceImpl.endPoint}") final String endPoint) { 30 | return new SunriseSunsetServiceImpl(endPoint); 31 | } 32 | 33 | @Bean 34 | ErrorHandler errorHandler() { 35 | return new ErrorHandler(); 36 | } 37 | 38 | @Bean 39 | RouterFunction mainRouterFunction(final ApiHandler apiHandler, final ErrorHandler errorHandler) { 40 | return MainRouter.doRoute(apiHandler, errorHandler); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/application/ReactiveMsApplication.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.application; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ReactiveMsApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(ReactiveMsApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/exceptions/GeoLocationNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.exceptions; 2 | 3 | public class GeoLocationNotFoundException extends Exception{ 4 | 5 | public GeoLocationNotFoundException(final String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/exceptions/GetGeoLocationException.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.exceptions; 2 | 3 | public class GetGeoLocationException extends Exception { 4 | public GetGeoLocationException(final String message, final Throwable throwable) { 5 | super(message, throwable); 6 | } 7 | 8 | public GetGeoLocationException(final String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/exceptions/GetSunriseSunsetException.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.exceptions; 2 | 3 | public class GetSunriseSunsetException extends Exception { 4 | 5 | public GetSunriseSunsetException(final String message, final Throwable throwable) { 6 | super(message, throwable); 7 | } 8 | 9 | public GetSunriseSunsetException(final String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/exceptions/InvalidParametersException.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.exceptions; 2 | 3 | public class InvalidParametersException extends Exception { 4 | 5 | public InvalidParametersException(final String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/exceptions/PathNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.exceptions; 2 | 3 | public class PathNotFoundException extends Exception { 4 | 5 | public PathNotFoundException(final String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/handlers/ApiHandler.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.handlers; 2 | 3 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 4 | import org.learning.by.example.reactive.microservices.model.LocationRequest; 5 | import org.learning.by.example.reactive.microservices.model.LocationResponse; 6 | import org.learning.by.example.reactive.microservices.model.SunriseSunset; 7 | import org.learning.by.example.reactive.microservices.services.GeoLocationService; 8 | import org.learning.by.example.reactive.microservices.services.SunriseSunsetService; 9 | import org.springframework.web.reactive.function.server.ServerRequest; 10 | import org.springframework.web.reactive.function.server.ServerResponse; 11 | import reactor.core.publisher.Mono; 12 | 13 | public class ApiHandler { 14 | 15 | private static final String ADDRESS = "address"; 16 | private static final String EMPTY_STRING = ""; 17 | 18 | private final ErrorHandler errorHandler; 19 | 20 | private final GeoLocationService geoLocationService; 21 | private final SunriseSunsetService sunriseSunsetService; 22 | 23 | public ApiHandler(final GeoLocationService geoLocationService, final SunriseSunsetService sunriseSunsetService, 24 | final ErrorHandler errorHandler) { 25 | this.errorHandler = errorHandler; 26 | this.geoLocationService = geoLocationService; 27 | this.sunriseSunsetService = sunriseSunsetService; 28 | } 29 | 30 | public Mono postLocation(final ServerRequest request) { 31 | return request.bodyToMono(LocationRequest.class) 32 | .map(LocationRequest::getAddress) 33 | .onErrorResume(throwable -> Mono.just(EMPTY_STRING)) 34 | .transform(this::buildResponse) 35 | .onErrorResume(errorHandler::throwableError); 36 | } 37 | 38 | public Mono getLocation(final ServerRequest request) { 39 | return Mono.just(request.pathVariable(ADDRESS)) 40 | .transform(this::buildResponse) 41 | .onErrorResume(errorHandler::throwableError); 42 | } 43 | 44 | Mono buildResponse(final Mono address) { 45 | return address 46 | .transform(geoLocationService::fromAddress) 47 | .zipWhen(this::sunriseSunset, LocationResponse::new) 48 | .transform(this::serverResponse); 49 | } 50 | 51 | private Mono sunriseSunset(GeographicCoordinates geographicCoordinates) { 52 | return Mono.just(geographicCoordinates).transform(sunriseSunsetService::fromGeographicCoordinates); 53 | } 54 | 55 | Mono serverResponse(Mono locationResponseMono) { 56 | return locationResponseMono.flatMap(locationResponse -> 57 | ServerResponse.ok().body(Mono.just(locationResponse), LocationResponse.class)); 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/handlers/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.handlers; 2 | 3 | import org.learning.by.example.reactive.microservices.exceptions.PathNotFoundException; 4 | import org.learning.by.example.reactive.microservices.model.ErrorResponse; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.web.reactive.function.server.ServerRequest; 8 | import org.springframework.web.reactive.function.server.ServerResponse; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class ErrorHandler { 12 | 13 | private static final String NOT_FOUND = "not found"; 14 | private static final String ERROR_RAISED = "error raised"; 15 | private static final Logger logger = LoggerFactory.getLogger(ErrorHandler.class); 16 | 17 | public Mono notFound(final ServerRequest request) { 18 | return Mono.just(new PathNotFoundException(NOT_FOUND)).transform(this::getResponse); 19 | } 20 | 21 | Mono throwableError(final Throwable error) { 22 | logger.error(ERROR_RAISED, error); 23 | return Mono.just(error).transform(this::getResponse); 24 | } 25 | 26 | Mono getResponse(final Mono monoError) { 27 | return monoError.transform(ThrowableTranslator::translate) 28 | .flatMap(translation -> ServerResponse 29 | .status(translation.getHttpStatus()) 30 | .body(Mono.just(new ErrorResponse(translation.getMessage())), ErrorResponse.class)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/handlers/ThrowableTranslator.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.handlers; 2 | 3 | import org.learning.by.example.reactive.microservices.exceptions.GetGeoLocationException; 4 | import org.learning.by.example.reactive.microservices.exceptions.InvalidParametersException; 5 | import org.learning.by.example.reactive.microservices.exceptions.GeoLocationNotFoundException; 6 | import org.learning.by.example.reactive.microservices.exceptions.PathNotFoundException; 7 | import org.springframework.http.HttpStatus; 8 | import reactor.core.publisher.Mono; 9 | 10 | class ThrowableTranslator { 11 | 12 | private final HttpStatus httpStatus; 13 | private final String message; 14 | 15 | private ThrowableTranslator(final Throwable throwable) { 16 | this.httpStatus = getStatus(throwable); 17 | this.message = throwable.getMessage(); 18 | } 19 | 20 | private HttpStatus getStatus(final Throwable error) { 21 | if (error instanceof InvalidParametersException) { 22 | return HttpStatus.BAD_REQUEST; 23 | } else if (error instanceof PathNotFoundException) { 24 | return HttpStatus.NOT_FOUND; 25 | } else if (error instanceof GeoLocationNotFoundException) { 26 | return HttpStatus.NOT_FOUND; 27 | } else if (error instanceof GetGeoLocationException) { 28 | if (error.getCause() instanceof InvalidParametersException) 29 | return HttpStatus.BAD_REQUEST; 30 | else 31 | return HttpStatus.INTERNAL_SERVER_ERROR; 32 | } else { 33 | return HttpStatus.INTERNAL_SERVER_ERROR; 34 | } 35 | } 36 | 37 | HttpStatus getHttpStatus() { 38 | return httpStatus; 39 | } 40 | 41 | String getMessage() { 42 | return message; 43 | } 44 | 45 | static Mono translate(final Mono throwable) { 46 | return throwable.flatMap(error -> Mono.just(new ThrowableTranslator(error))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | public class ErrorResponse { 8 | 9 | private final String error; 10 | 11 | @JsonCreator 12 | public ErrorResponse(@JsonProperty("error") final String error) { 13 | this.error = error; 14 | } 15 | 16 | public String getError() { 17 | return error; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/GeoLocationResponse.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public final class GeoLocationResponse { 7 | 8 | private final Result results[]; 9 | private final String status; 10 | 11 | @JsonCreator 12 | public GeoLocationResponse(@JsonProperty("results") Result[] results, @JsonProperty("status") String status) { 13 | this.results = results; 14 | this.status = status; 15 | } 16 | 17 | public String getStatus() { 18 | return status; 19 | } 20 | 21 | public Result[] getResults() { 22 | return results; 23 | } 24 | 25 | public static final class Result { 26 | final Address_component address_components[]; 27 | final String formatted_address; 28 | 29 | public Geometry getGeometry() { 30 | return geometry; 31 | } 32 | 33 | final Geometry geometry; 34 | final String place_id; 35 | final String[] types; 36 | 37 | @JsonCreator 38 | public Result(@JsonProperty("address_components") Address_component[] address_components, @JsonProperty("formatted_address") String formatted_address, @JsonProperty("geometry") Geometry geometry, @JsonProperty("place_id") String place_id, @JsonProperty("types") String[] types) { 39 | this.address_components = address_components; 40 | this.formatted_address = formatted_address; 41 | this.geometry = geometry; 42 | this.place_id = place_id; 43 | this.types = types; 44 | } 45 | 46 | public static final class Address_component { 47 | final String long_name; 48 | final String short_name; 49 | final String[] types; 50 | 51 | @JsonCreator 52 | public Address_component(@JsonProperty("long_name") String long_name, @JsonProperty("short_name") String short_name, @JsonProperty("types") String[] types) { 53 | this.long_name = long_name; 54 | this.short_name = short_name; 55 | this.types = types; 56 | } 57 | } 58 | 59 | public static final class Geometry { 60 | final Bounds bounds; 61 | 62 | public Location getLocation() { 63 | return location; 64 | } 65 | 66 | final Location location; 67 | final String location_type; 68 | final Viewport viewport; 69 | 70 | @JsonCreator 71 | public Geometry(@JsonProperty("bounds") Bounds bounds, @JsonProperty("location") Location location, @JsonProperty("location_type") String location_type, @JsonProperty("viewport") Viewport viewport) { 72 | this.bounds = bounds; 73 | this.location = location; 74 | this.location_type = location_type; 75 | this.viewport = viewport; 76 | } 77 | 78 | public static final class Bounds { 79 | final Northeast northeast; 80 | final Southwest southwest; 81 | 82 | @JsonCreator 83 | public Bounds(@JsonProperty("northeast") Northeast northeast, @JsonProperty("southwest") Southwest southwest) { 84 | this.northeast = northeast; 85 | this.southwest = southwest; 86 | } 87 | 88 | public static final class Northeast { 89 | final double lat; 90 | final double lng; 91 | 92 | @JsonCreator 93 | public Northeast(@JsonProperty("lat") double lat, @JsonProperty("lng") double lng) { 94 | this.lat = lat; 95 | this.lng = lng; 96 | } 97 | } 98 | 99 | public static final class Southwest { 100 | final double lat; 101 | final double lng; 102 | 103 | @JsonCreator 104 | public Southwest(@JsonProperty("lat") double lat, @JsonProperty("lng") double lng) { 105 | this.lat = lat; 106 | this.lng = lng; 107 | } 108 | } 109 | } 110 | 111 | public static final class Location { 112 | public double getLat() { 113 | return lat; 114 | } 115 | 116 | public double getLng() { 117 | return lng; 118 | } 119 | 120 | final double lat; 121 | final double lng; 122 | 123 | @JsonCreator 124 | public Location(@JsonProperty("lat") double lat, @JsonProperty("lng") double lng) { 125 | this.lat = lat; 126 | this.lng = lng; 127 | } 128 | } 129 | 130 | public static final class Viewport { 131 | final Northeast northeast; 132 | final Southwest southwest; 133 | 134 | @JsonCreator 135 | public Viewport(@JsonProperty("northeast") Northeast northeast, @JsonProperty("southwest") Southwest southwest) { 136 | this.northeast = northeast; 137 | this.southwest = southwest; 138 | } 139 | 140 | public static final class Northeast { 141 | final double lat; 142 | final double lng; 143 | 144 | @JsonCreator 145 | public Northeast(@JsonProperty("lat") double lat, @JsonProperty("lng") double lng) { 146 | this.lat = lat; 147 | this.lng = lng; 148 | } 149 | } 150 | 151 | public static final class Southwest { 152 | final double lat; 153 | final double lng; 154 | 155 | @JsonCreator 156 | public Southwest(@JsonProperty("lat") double lat, @JsonProperty("lng") double lng) { 157 | this.lat = lat; 158 | this.lng = lng; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/GeoTimesResponse.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public final class GeoTimesResponse { 7 | 8 | private final Results results; 9 | private final String status; 10 | 11 | @JsonCreator 12 | public GeoTimesResponse(@JsonProperty("results") Results results, @JsonProperty("status") String status) { 13 | this.results = results; 14 | this.status = status; 15 | } 16 | 17 | public Results getResults() { 18 | return results; 19 | } 20 | 21 | public String getStatus() { 22 | return status; 23 | } 24 | 25 | public static final class Results { 26 | public String getSunrise() { 27 | return sunrise; 28 | } 29 | 30 | public String getSunset() { 31 | return sunset; 32 | } 33 | 34 | final String sunrise; 35 | final String sunset; 36 | final String solar_noon; 37 | final long day_length; 38 | final String civil_twilight_begin; 39 | final String civil_twilight_end; 40 | final String nautical_twilight_begin; 41 | final String nautical_twilight_end; 42 | final String astronomical_twilight_begin; 43 | final String astronomical_twilight_end; 44 | 45 | @JsonCreator 46 | public Results(@JsonProperty("sunrise") String sunrise, @JsonProperty("sunset") String sunset, @JsonProperty("solar_noon") String solar_noon, @JsonProperty("day_length") long day_length, @JsonProperty("civil_twilight_begin") String civil_twilight_begin, @JsonProperty("civil_twilight_end") String civil_twilight_end, @JsonProperty("nautical_twilight_begin") String nautical_twilight_begin, @JsonProperty("nautical_twilight_end") String nautical_twilight_end, @JsonProperty("astronomical_twilight_begin") String astronomical_twilight_begin, @JsonProperty("astronomical_twilight_end") String astronomical_twilight_end) { 47 | this.sunrise = sunrise; 48 | this.sunset = sunset; 49 | this.solar_noon = solar_noon; 50 | this.day_length = day_length; 51 | this.civil_twilight_begin = civil_twilight_begin; 52 | this.civil_twilight_end = civil_twilight_end; 53 | this.nautical_twilight_begin = nautical_twilight_begin; 54 | this.nautical_twilight_end = nautical_twilight_end; 55 | this.astronomical_twilight_begin = astronomical_twilight_begin; 56 | this.astronomical_twilight_end = astronomical_twilight_end; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/GeographicCoordinates.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | 7 | public final class GeographicCoordinates { 8 | 9 | public double getLatitude() { 10 | return latitude; 11 | } 12 | 13 | public double getLongitude() { 14 | return longitude; 15 | } 16 | 17 | private final double latitude; 18 | private final double longitude; 19 | 20 | @JsonCreator 21 | public GeographicCoordinates(@JsonProperty("latitude") double latitude, @JsonProperty("longitude") double longitude) { 22 | this.latitude = latitude; 23 | this.longitude = longitude; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/LocationRequest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | public class LocationRequest { 8 | 9 | private final String address; 10 | 11 | @JsonCreator 12 | public LocationRequest(@JsonProperty("address") final String address) { 13 | this.address = address; 14 | } 15 | 16 | public String getAddress() { 17 | return address; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/LocationResponse.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | public class LocationResponse { 8 | 9 | private final GeographicCoordinates geographicCoordinates; 10 | private final SunriseSunset sunriseSunset; 11 | 12 | @JsonCreator 13 | public LocationResponse(@JsonProperty("geographicCoordinates") final GeographicCoordinates geographicCoordinates, 14 | @JsonProperty("sunriseSunset") final SunriseSunset sunriseSunset) { 15 | this.geographicCoordinates = geographicCoordinates; 16 | this.sunriseSunset = sunriseSunset; 17 | } 18 | 19 | public GeographicCoordinates getGeographicCoordinates() { 20 | return geographicCoordinates; 21 | } 22 | 23 | public SunriseSunset getSunriseSunset() { 24 | return sunriseSunset; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/model/SunriseSunset.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public class SunriseSunset { 7 | 8 | public String getSunrise() { 9 | return sunrise; 10 | } 11 | 12 | public String getSunset() { 13 | return sunset; 14 | } 15 | 16 | private final String sunrise; 17 | private final String sunset; 18 | 19 | @JsonCreator 20 | public SunriseSunset(@JsonProperty("sunrise") final String sunrise, @JsonProperty("sunset") final String sunset) { 21 | this.sunrise = sunrise; 22 | this.sunset = sunset; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/routers/ApiRouter.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.routers; 2 | 3 | import org.learning.by.example.reactive.microservices.handlers.ErrorHandler; 4 | import org.learning.by.example.reactive.microservices.handlers.ApiHandler; 5 | import org.springframework.web.reactive.function.server.RequestPredicates; 6 | import org.springframework.web.reactive.function.server.RouterFunction; 7 | 8 | import static org.springframework.http.MediaType.APPLICATION_JSON; 9 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; 10 | import static org.springframework.web.reactive.function.server.RouterFunctions.nest; 11 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 12 | 13 | class ApiRouter { 14 | 15 | private static final String API_PATH = "/api"; 16 | private static final String LOCATION_PATH = "/location"; 17 | private static final String ADDRESS_ARG = "/{address}"; 18 | private static final String LOCATION_WITH_ADDRESS_PATH = LOCATION_PATH + ADDRESS_ARG; 19 | 20 | static RouterFunction doRoute(final ApiHandler apiHandler, final ErrorHandler errorHandler) { 21 | return 22 | nest(path(API_PATH), 23 | nest(accept(APPLICATION_JSON), 24 | route(GET(LOCATION_WITH_ADDRESS_PATH), apiHandler::getLocation) 25 | .andRoute(POST(LOCATION_PATH), apiHandler::postLocation) 26 | ).andOther(route(RequestPredicates.all(), errorHandler::notFound)) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/routers/MainRouter.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.routers; 2 | 3 | import org.learning.by.example.reactive.microservices.handlers.ErrorHandler; 4 | import org.learning.by.example.reactive.microservices.handlers.ApiHandler; 5 | import org.springframework.web.reactive.function.server.RouterFunction; 6 | 7 | public class MainRouter { 8 | 9 | public static RouterFunction doRoute(final ApiHandler handler, final ErrorHandler errorHandler) { 10 | return ApiRouter 11 | .doRoute(handler, errorHandler) 12 | .andOther(StaticRouter.doRoute()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/routers/StaticRouter.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.routers; 2 | 3 | import org.springframework.core.io.ClassPathResource; 4 | import org.springframework.web.reactive.function.server.RouterFunction; 5 | 6 | import static org.springframework.web.reactive.function.server.RouterFunctions.resources; 7 | 8 | class StaticRouter { 9 | 10 | private static final String ROUTE = "/**"; 11 | private static final String PUBLIC = "public/"; 12 | 13 | static RouterFunction doRoute() { 14 | return resources(ROUTE, new ClassPathResource(PUBLIC)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/services/GeoLocationService.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.services; 2 | 3 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 4 | import reactor.core.publisher.Mono; 5 | 6 | public interface GeoLocationService { 7 | 8 | Mono fromAddress(Mono addressMono); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/services/GeoLocationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.services; 2 | 3 | import org.learning.by.example.reactive.microservices.exceptions.GetGeoLocationException; 4 | import org.learning.by.example.reactive.microservices.exceptions.GeoLocationNotFoundException; 5 | import org.learning.by.example.reactive.microservices.exceptions.InvalidParametersException; 6 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 7 | import org.learning.by.example.reactive.microservices.model.GeoLocationResponse; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.reactive.function.client.WebClient; 10 | import reactor.core.publisher.Mono; 11 | 12 | public class GeoLocationServiceImpl implements GeoLocationService { 13 | 14 | private static final String OK_STATUS = "OK"; 15 | private static final String ZERO_RESULTS = "ZERO_RESULTS"; 16 | private static final String ERROR_GETTING_LOCATION = "error getting location"; 17 | private static final String ERROR_LOCATION_WAS_NULL = "error location was null"; 18 | private static final String ADDRESS_NOT_FOUND = "address not found"; 19 | private static final String ADDRESS_PARAMETER = "?address="; 20 | private static final String MISSING_ADDRESS = "missing address"; 21 | WebClient webClient; 22 | private final String endPoint; 23 | 24 | public GeoLocationServiceImpl(final String endPoint) { 25 | this.endPoint = endPoint; 26 | this.webClient = WebClient.create(); 27 | } 28 | 29 | @Override 30 | public Mono fromAddress(final Mono addressMono) { 31 | return addressMono 32 | .transform(this::buildUrl) 33 | .transform(this::get) 34 | .onErrorResume(throwable -> Mono.error(new GetGeoLocationException(ERROR_GETTING_LOCATION, throwable))) 35 | .transform(this::geometryLocation); 36 | } 37 | 38 | Mono buildUrl(final Mono addressMono) { 39 | return addressMono.flatMap(address -> { 40 | if (address.equals("")) { 41 | return Mono.error(new InvalidParametersException(MISSING_ADDRESS)); 42 | } 43 | return Mono.just(endPoint.concat(ADDRESS_PARAMETER).concat(address)); 44 | }); 45 | } 46 | 47 | Mono get(final Mono urlMono) { 48 | return urlMono.flatMap(url -> webClient 49 | .get() 50 | .uri(url) 51 | .accept(MediaType.APPLICATION_JSON) 52 | .exchange() 53 | .flatMap(clientResponse -> clientResponse.bodyToMono(GeoLocationResponse.class))); 54 | } 55 | 56 | Mono geometryLocation(final Mono geoLocationResponseMono) { 57 | return geoLocationResponseMono.flatMap(geoLocationResponse -> { 58 | if (geoLocationResponse.getStatus() != null) { 59 | switch (geoLocationResponse.getStatus()) { 60 | case OK_STATUS: 61 | return Mono.just( 62 | new GeographicCoordinates(geoLocationResponse.getResults()[0].getGeometry().getLocation().getLat(), 63 | geoLocationResponse.getResults()[0].getGeometry().getLocation().getLng())); 64 | case ZERO_RESULTS: 65 | return Mono.error(new GeoLocationNotFoundException(ADDRESS_NOT_FOUND)); 66 | default: 67 | return Mono.error(new GetGeoLocationException(ERROR_GETTING_LOCATION)); 68 | } 69 | } else { 70 | return Mono.error(new GetGeoLocationException(ERROR_LOCATION_WAS_NULL)); 71 | } 72 | } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/services/SunriseSunsetService.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.services; 2 | 3 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 4 | import org.learning.by.example.reactive.microservices.model.SunriseSunset; 5 | import reactor.core.publisher.Mono; 6 | 7 | public interface SunriseSunsetService { 8 | 9 | Mono fromGeographicCoordinates(Mono geographicCoordinatesMono); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/learning/by/example/reactive/microservices/services/SunriseSunsetServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.services; 2 | 3 | import org.learning.by.example.reactive.microservices.exceptions.GetSunriseSunsetException; 4 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 5 | import org.learning.by.example.reactive.microservices.model.SunriseSunset; 6 | import org.learning.by.example.reactive.microservices.model.GeoTimesResponse; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.web.reactive.function.client.WebClient; 9 | import reactor.core.publisher.Mono; 10 | 11 | public class SunriseSunsetServiceImpl implements SunriseSunsetService { 12 | 13 | private static final String BEGIN_PARAMETERS = "?"; 14 | private static final String NEXT_PARAMETER = "&"; 15 | private static final String EQUALS = "="; 16 | private static final String LATITUDE_PARAMETER = "lat" + EQUALS; 17 | private static final String LONGITUDE_PARAMETER = "lng" + EQUALS; 18 | private static final String DATE_PARAMETER = "date" + EQUALS; 19 | private static final String TODAY_DATE = "today"; 20 | private static final String FORMATTED_PARAMETER = "formatted" + EQUALS; 21 | private static final String NOT_FORMATTED = "0"; 22 | private static final String ERROR_GETTING_DATA = "error getting sunrise and sunset"; 23 | private static final String SUNRISE_RESULT_NOT_OK = "sunrise and sunrise result was not OK"; 24 | private static final String STATUS_OK = "OK"; 25 | 26 | WebClient webClient; 27 | private final String endPoint; 28 | 29 | public SunriseSunsetServiceImpl(final String endPoint) { 30 | this.endPoint = endPoint; 31 | this.webClient = WebClient.create(); 32 | } 33 | 34 | @Override 35 | public Mono fromGeographicCoordinates(Mono location) { 36 | return location 37 | .transform(this::buildUrl) 38 | .transform(this::get) 39 | .onErrorResume(throwable -> Mono.error(new GetSunriseSunsetException(ERROR_GETTING_DATA, throwable))) 40 | .transform(this::createResult); 41 | } 42 | 43 | Mono buildUrl(final Mono geographicCoordinatesMono) { 44 | return geographicCoordinatesMono.flatMap(geographicCoordinates -> Mono.just(endPoint 45 | .concat(BEGIN_PARAMETERS) 46 | .concat(LATITUDE_PARAMETER).concat(Double.toString(geographicCoordinates.getLatitude())) 47 | .concat(NEXT_PARAMETER) 48 | .concat(LONGITUDE_PARAMETER).concat(Double.toString(geographicCoordinates.getLongitude())) 49 | .concat(NEXT_PARAMETER) 50 | .concat(DATE_PARAMETER).concat(TODAY_DATE) 51 | .concat(NEXT_PARAMETER) 52 | .concat(FORMATTED_PARAMETER).concat(NOT_FORMATTED) 53 | )); 54 | } 55 | 56 | Mono get(final Mono monoUrl) { 57 | return monoUrl.flatMap(url -> webClient 58 | .get() 59 | .uri(url) 60 | .accept(MediaType.APPLICATION_JSON) 61 | .exchange() 62 | .flatMap(clientResponse -> clientResponse.bodyToMono(GeoTimesResponse.class))); 63 | } 64 | 65 | Mono createResult(final Mono geoTimesResponseMono) { 66 | return geoTimesResponseMono.flatMap(geoTimesResponse -> { 67 | if ((geoTimesResponse.getStatus() != null) && (geoTimesResponse.getStatus().equals(STATUS_OK))) { 68 | return Mono.just(new SunriseSunset(geoTimesResponse.getResults().getSunrise(), 69 | geoTimesResponse.getResults().getSunset())); 70 | } else { 71 | return Mono.error(new GetSunriseSunsetException(SUNRISE_RESULT_NOT_OK)); 72 | } 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | logging.level.root: INFO 2 | GeoLocationServiceImpl: 3 | endPoint: "https://maps.googleapis.com/maps/api/geocode/json" 4 | SunriseSunsetServiceImpl: 5 | endPoint: "https://api.sunrise-sunset.org/json" 6 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ _____ _ 2 | | | (_) | | | ___| | | 3 | | | ___ __ _ _ __ _ __ _ _ __ __ _| |__ _ _| |____ ____ _ _ __ ___ _ __ | | ___ 4 | | | / _ \/ _` | '__| '_ \| | '_ \ / _` | '_ \| | | | __\ \/ / _` | '_ ` _ \| '_ \| |/ _ \ 5 | | |___| __/ (_| | | | | | | | | | | (_| | |_) | |_| | |___> < (_| | | | | | | |_) | | __/ 6 | \_____/\___|\__,_|_| |_| |_|_|_| |_|\__, |_.__/ \__, \____/_/\_\__,_|_| |_| |_| .__/|_|\___| 7 | __/ | __/ | | | 8 | |___/ |___/ |_| 9 | 10 | An educational project to learn reactive programming with Spring 5 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/public/api.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | description: "This is the API for the Reactive Micro Service example" 4 | version: "1.0.0" 5 | title: "Reactive Micro Services API" 6 | contact: 7 | url: "https://github.com/LearningByExample/reactive-ms-example" 8 | host: "localhost:8080" 9 | basePath: "/api" 10 | tags: 11 | - name: "location" 12 | description: "geo location services" 13 | schemes: 14 | - "http" 15 | paths: 16 | /location/{address}: 17 | get: 18 | tags: 19 | - "location" 20 | summary: "get latitude, longitude, sunrise and sunset from an address parameter" 21 | produces: 22 | - "application/json" 23 | parameters: 24 | - name: "address" 25 | in: "path" 26 | required: true 27 | type: "string" 28 | responses: 29 | 200: 30 | description: "successful operation" 31 | schema: 32 | $ref: "#/definitions/LocationResponse" 33 | /location: 34 | post: 35 | tags: 36 | - "location" 37 | summary: "get latitude, longitude, sunrise and sunset from an LocationRequest object" 38 | consumes: 39 | - "application/json" 40 | produces: 41 | - "application/json" 42 | parameters: 43 | - in: "body" 44 | name: "body" 45 | description: "LocationRequest object that has an address" 46 | required: true 47 | schema: 48 | $ref: "#/definitions/LocationRequest" 49 | responses: 50 | 200: 51 | description: "successful operation" 52 | schema: 53 | $ref: "#/definitions/LocationResponse" 54 | definitions: 55 | LocationRequest: 56 | type: "object" 57 | properties: 58 | address: 59 | type: "string" 60 | LocationResponse: 61 | type: "object" 62 | properties: 63 | geographicCoordinates: 64 | type: "object" 65 | properties: 66 | latitude: 67 | type: "number" 68 | format: "double" 69 | longitude: 70 | type: "number" 71 | format: "double" 72 | sunriseSunset: 73 | type: "object" 74 | properties: 75 | sunrise: 76 | type: "string" 77 | format: "date-time" 78 | description: "ISO 8601 UTC without summer time adjustment" 79 | sunset: 80 | type: "string" 81 | format: "date-time" 82 | description: "ISO 8601 UTC without summer time adjustment" 83 | -------------------------------------------------------------------------------- /src/main/resources/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningByExample/reactive-ms-example/c40d5855ce2c5e0b6dfb06aa4a421ae9560d6548/src/main/resources/public/favicon-16x16.png -------------------------------------------------------------------------------- /src/main/resources/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningByExample/reactive-ms-example/c40d5855ce2c5e0b6dfb06aa4a421ae9560d6548/src/main/resources/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/resources/public/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 54 | -------------------------------------------------------------------------------- /src/main/resources/public/swagger-ui-bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;AAu/FA;AA6+FA;;;;;;;;;;;;;;;;;;;;;;;;;;AAmTA;;;;;;AAoIA;AAi7FA;AAmtCA;AAi0IA;AA2pJA;AA+uFA;AA2rGA;AAgiFA;AA0rFA;AAk9CA;AA2hDA;AA4rCA;AAi6EA;;;;;AA2gCA;AA02JA;;;;;;;;;;;;;;AAuyEA;AA4mIA;AAquJA;AAwsHA;AA2mGA;AAiiEA;AAq4DA;AA+2DA;AAqlBA;;;;;;AAilFA;AAs1FA;;;;;AAy3CA;AA2qFA;AAw2CA;AAwkCA;AAs/CA;AA4kFA;AAy1FA;;;;;;;;;AAm5CA;AA2zIA;AAk4DA;AAolDA","sourceRoot":""} -------------------------------------------------------------------------------- /src/main/resources/public/swagger-ui-standalone-preset.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIStandalonePreset=t():e.SwaggerUIStandalonePreset=t()}(this,function(){return function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={exports:{},id:r,loaded:!1};return e[r].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var o={};return t.m=e,t.c=o,t.p="/dist",t(0)}([function(e,t,o){e.exports=o(1)},function(e,t,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}var n=o(2),i=r(n);o(33);var a=o(37),s=r(a),l=[s.default,function(){return{components:{StandaloneLayout:i.default}}}];e.exports=l},function(e,t,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var s=function(){function e(e,t){for(var o=0;o1){for(var m=Array(b),x=0;x1){for(var h=Array(w),y=0;ylabel{font-size:12px;font-weight:700;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:-20px 15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{font-size:10px;font-weight:700;position:absolute;top:50%;left:50%;content:\"loading\";-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-transform:uppercase;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .loading-container .loading:before{position:absolute;top:50%;left:50%;display:block;width:60px;height:60px;margin:-30px;content:\"\";-webkit-animation:rotation 1s infinite linear,opacity .5s;animation:rotation 1s infinite linear,opacity .5s;opacity:1;border:2px solid rgba(85,85,85,.1);border-top-color:rgba(0,0,0,.6);border-radius:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden}@-webkit-keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes blinker{50%{opacity:0}}@keyframes blinker{50%{opacity:0}}.swagger-ui .btn{font-size:14px;font-weight:700;padding:5px 23px;transition:all .3s;border:2px solid #888;border-radius:4px;background:transparent;box-shadow:0 1px 2px rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{border-color:#ff6060;font-family:Titillium Web,sans-serif;color:#ff6060}.swagger-ui .btn.authorize{line-height:1;display:inline;color:#49cc90;border-color:#49cc90}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;color:#fff;border-color:#4990e2}@-webkit-keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}@keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}.swagger-ui .btn-group{display:-webkit-box;display:-ms-flexbox;display:flex;padding:30px}.swagger-ui .btn-group .btn{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{padding:0 10px;border:none;background:none}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .expand-methods,.swagger-ui .expand-operation{border:none;background:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{width:20px;height:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#444}.swagger-ui .expand-methods svg{transition:all .3s;fill:#777}.swagger-ui button{cursor:pointer;outline:none}.swagger-ui select{font-size:14px;font-weight:700;padding:5px 40px 5px 10px;border:2px solid #41444e;border-radius:4px;background:#f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat;background-size:20px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);font-family:Titillium Web,sans-serif;color:#3b4151;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui select[multiple]{margin:5px 0;padding:5px;background:#f7f7f7}.swagger-ui .opblock-body select{min-width:230px}.swagger-ui label{font-size:12px;font-weight:700;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui input[type=email],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{min-width:100px;margin:5px 0;padding:8px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fff}.swagger-ui input[type=email].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid{-webkit-animation:shake .4s 1;animation:shake .4s 1;border-color:#f93e3e;background:#feebeb}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}.swagger-ui textarea{font-size:12px;width:100%;min-height:280px;padding:10px;border:none;border-radius:4px;outline:none;background:hsla(0,0%,100%,.8);font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{font-size:12px;min-height:100px;margin:0;padding:10px;resize:none;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .checkbox{padding:5px 0 10px;transition:opacity .5s;color:#333}.swagger-ui .checkbox label{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .checkbox p{font-weight:400!important;font-style:italic;margin:0!important;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{position:relative;top:3px;display:inline-block;width:16px;height:16px;margin:0 8px 0 0;padding:5px;cursor:pointer;border-radius:1px;background:#e8e8e8;box-shadow:0 0 0 2px #e8e8e8;-webkit-box-flex:0;-ms-flex:none;flex:none}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{-webkit-transform:scale(.9);transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E\") 50% no-repeat}.swagger-ui .dialog-ux{position:fixed;z-index:9999;top:0;right:0;bottom:0;left:0}.swagger-ui .dialog-ux .backdrop-ux{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.8)}.swagger-ui .dialog-ux .modal-ux{position:absolute;z-index:9999;top:50%;left:50%;width:100%;min-width:300px;max-width:650px;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:1px solid #ebebeb;border-radius:4px;background:#fff;box-shadow:0 10px 30px 0 rgba(0,0,0,.2)}.swagger-ui .dialog-ux .modal-ux-content{overflow-y:auto;max-height:540px;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{font-size:12px;margin:0 0 5px;color:#41444e;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-content h4{font-size:18px;font-weight:600;margin:15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-header{display:-webkit-box;display:-ms-flexbox;display:flex;padding:12px 0;border-bottom:1px solid #ebebeb;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .dialog-ux .modal-ux-header .close-modal{padding:0 10px;border:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui .dialog-ux .modal-ux-header h3{font-size:20px;font-weight:600;margin:0;padding:0 20px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .model{font-size:12px;font-weight:300;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .model-toggle{font-size:10px;position:relative;top:6px;display:inline-block;margin:auto .3em;cursor:pointer;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.swagger-ui .model-toggle:after{display:block;width:20px;height:20px;content:\"\";background:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E\") 50% no-repeat;background-size:100%}.swagger-ui .model-jump-to-path{position:relative;cursor:pointer}.swagger-ui .model-jump-to-path .view-line-link{position:absolute;top:-.4em;cursor:pointer}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{position:absolute;top:-1.8em;visibility:hidden;padding:.1em .5em;white-space:nowrap;color:#ebebeb;border-radius:4px;background:rgba(0,0,0,.7)}.swagger-ui section.models{margin:30px 0;border:1px solid rgba(59,65,81,.3);border-radius:4px}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{margin:0 0 5px;border-bottom:1px solid rgba(59,65,81,.3)}.swagger-ui section.models.is-open h4 svg{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.swagger-ui section.models h4{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:10px 20px 10px 10px;cursor:pointer;transition:all .2s;font-family:Titillium Web,sans-serif;color:#777;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui section.models h4 svg{transition:all .4s}.swagger-ui section.models h4 span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{font-size:16px;margin:0 0 10px;font-family:Titillium Web,sans-serif;color:#777}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{margin:0 20px 15px;transition:all .5s;border-radius:4px;background:rgba(0,0,0,.05)}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{padding:10px;border-radius:4px;background:rgba(0,0,0,.1)}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-title{font-size:16px;font-family:Titillium Web,sans-serif;color:#555}.swagger-ui span>span.model,.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#999}.swagger-ui table{width:100%;padding:0 10px;border-collapse:collapse}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{width:100px;padding:0}.swagger-ui table.headers td{font-size:12px;font-weight:300;vertical-align:middle;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{width:20%;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{font-size:12px;font-weight:700;padding:12px 0;text-align:left;border-bottom:1px solid rgba(59,65,81,.2);font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description input[type=text]{width:100%;max-width:340px}.swagger-ui .parameter__name{font-size:16px;font-weight:400;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required:after{font-size:10px;position:relative;top:-6px;padding:5px;content:\"required\";color:rgba(255,0,0,.6)}.swagger-ui .parameter__in{font-size:12px;font-style:italic;font-family:Source Code Pro,monospace;font-weight:600;color:#888}.swagger-ui .table-container{padding:20px}.swagger-ui .topbar{padding:8px 30px;background-color:#89bf04}.swagger-ui .topbar .topbar-wrapper{-ms-flex-align:center}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;align-items:center}.swagger-ui .topbar a{font-size:1.5em;font-weight:700;max-width:300px;text-decoration:none;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-align:center;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:3;-ms-flex:3;flex:3}.swagger-ui .topbar .download-url-wrapper input[type=text]{width:100%;min-width:350px;margin:0;border:2px solid #547f00;border-radius:4px 0 0 4px;outline:none}.swagger-ui .topbar .download-url-wrapper .download-url-button{font-size:16px;font-weight:700;padding:4px 40px;border:none;border-radius:0 4px 4px 0;background:#547f00;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .info{margin:50px 0}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info p{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info code{padding:3px 5px;border-radius:4px;background:rgba(0,0,0,.05);font-family:Source Code Pro,monospace;font-weight:600;color:#9012fe}.swagger-ui .info a{font-size:14px;transition:all .4s;font-family:Open Sans,sans-serif;color:#4990e2}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{font-size:12px;font-weight:300!important;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .info .title{font-size:36px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info .title small{font-size:10px;position:relative;top:-5px;display:inline-block;margin:0 0 0 5px;padding:2px 4px;vertical-align:super;border-radius:57px;background:#7d8492}.swagger-ui .info .title small pre{margin:0;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .auth-btn-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 0;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.swagger-ui .auth-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{padding-right:20px}.swagger-ui .auth-container{margin:0 0 10px;padding:10px 20px;border-bottom:1px solid #ebebeb}.swagger-ui .auth-container:last-of-type{margin:0;padding:10px 20px;border:0}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{font-size:12px;padding:10px;border-radius:4px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .scopes h2{font-size:14px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{margin:20px;padding:10px 20px;-webkit-animation:scaleUp .5s;animation:scaleUp .5s;border:2px solid #f93e3e;border-radius:4px;background:rgba(249,62,62,.1)}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{font-size:14px;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .errors-wrapper .errors small{color:#666}.swagger-ui .errors-wrapper hgroup{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .errors-wrapper hgroup h4{font-size:20px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}@-webkit-keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}",""]); 7 | },function(e,t){e.exports=function(){var e=[];return e.toString=function(){for(var e=[],t=0;t=0&&h.splice(t,1)}function s(e){var t=document.createElement("style");return t.type="text/css",i(e,t),t}function l(e){var t=document.createElement("link");return t.rel="stylesheet",i(e,t),t}function p(e,t){var o,r,n;if(t.singleton){var i=w++;o=x||(x=s(t)),r=u.bind(null,o,i,!1),n=u.bind(null,o,i,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(o=l(t),r=f.bind(null,o),n=function(){a(o),o.href&&URL.revokeObjectURL(o.href)}):(o=s(t),r=c.bind(null,o),n=function(){a(o)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else n()}}function u(e,t,o,r){var n=o?"":r.css;if(e.styleSheet)e.styleSheet.cssText=y(t,n);else{var i=document.createTextNode(n),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(i,a[t]):e.appendChild(i)}}function c(e,t){var o=t.css,r=t.media;t.sourceMap;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=o;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(o))}}function f(e,t){var o=t.css,r=(t.media,t.sourceMap);r&&(o+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(r))))+" */");var n=new Blob([o],{type:"text/css"}),i=e.href;e.href=URL.createObjectURL(n),i&&URL.revokeObjectURL(i)}var d={},g=function(e){var t;return function(){return"undefined"==typeof t&&(t=e.apply(this,arguments)),t}},b=g(function(){return/msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase())}),m=g(function(){return document.head||document.getElementsByTagName("head")[0]}),x=null,w=0,h=[];e.exports=function(e,t){t=t||{},"undefined"==typeof t.singleton&&(t.singleton=b()),"undefined"==typeof t.insertAt&&(t.insertAt="bottom");var o=n(e);return r(o,t),function(e){for(var i=[],a=0;alabel{font-size:12px;font-weight:700;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:-20px 15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{font-size:10px;font-weight:700;position:absolute;top:50%;left:50%;content:"loading";-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-transform:uppercase;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .loading-container .loading:before{position:absolute;top:50%;left:50%;display:block;width:60px;height:60px;margin:-30px;content:"";-webkit-animation:rotation 1s infinite linear,opacity .5s;animation:rotation 1s infinite linear,opacity .5s;opacity:1;border:2px solid rgba(85,85,85,.1);border-top-color:rgba(0,0,0,.6);border-radius:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden}@-webkit-keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes blinker{50%{opacity:0}}@keyframes blinker{50%{opacity:0}}.swagger-ui .btn{font-size:14px;font-weight:700;padding:5px 23px;transition:all .3s;border:2px solid #888;border-radius:4px;background:transparent;box-shadow:0 1px 2px rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{border-color:#ff6060;font-family:Titillium Web,sans-serif;color:#ff6060}.swagger-ui .btn.authorize{line-height:1;display:inline;color:#49cc90;border-color:#49cc90}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;color:#fff;border-color:#4990e2}@-webkit-keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}@keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}.swagger-ui .btn-group{display:-webkit-box;display:-ms-flexbox;display:flex;padding:30px}.swagger-ui .btn-group .btn{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{padding:0 10px;border:none;background:none}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .expand-methods,.swagger-ui .expand-operation{border:none;background:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{width:20px;height:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#444}.swagger-ui .expand-methods svg{transition:all .3s;fill:#777}.swagger-ui button{cursor:pointer;outline:none}.swagger-ui select{font-size:14px;font-weight:700;padding:5px 40px 5px 10px;border:2px solid #41444e;border-radius:4px;background:#f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat;background-size:20px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);font-family:Titillium Web,sans-serif;color:#3b4151;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui select[multiple]{margin:5px 0;padding:5px;background:#f7f7f7}.swagger-ui .opblock-body select{min-width:230px}.swagger-ui label{font-size:12px;font-weight:700;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui input[type=email],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{min-width:100px;margin:5px 0;padding:8px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fff}.swagger-ui input[type=email].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid{-webkit-animation:shake .4s 1;animation:shake .4s 1;border-color:#f93e3e;background:#feebeb}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}.swagger-ui textarea{font-size:12px;width:100%;min-height:280px;padding:10px;border:none;border-radius:4px;outline:none;background:hsla(0,0%,100%,.8);font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{font-size:12px;min-height:100px;margin:0;padding:10px;resize:none;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .checkbox{padding:5px 0 10px;transition:opacity .5s;color:#333}.swagger-ui .checkbox label{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .checkbox p{font-weight:400!important;font-style:italic;margin:0!important;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{position:relative;top:3px;display:inline-block;width:16px;height:16px;margin:0 8px 0 0;padding:5px;cursor:pointer;border-radius:1px;background:#e8e8e8;box-shadow:0 0 0 2px #e8e8e8;-webkit-box-flex:0;-ms-flex:none;flex:none}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{-webkit-transform:scale(.9);transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url("data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E") 50% no-repeat}.swagger-ui .dialog-ux{position:fixed;z-index:9999;top:0;right:0;bottom:0;left:0}.swagger-ui .dialog-ux .backdrop-ux{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.8)}.swagger-ui .dialog-ux .modal-ux{position:absolute;z-index:9999;top:50%;left:50%;width:100%;min-width:300px;max-width:650px;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:1px solid #ebebeb;border-radius:4px;background:#fff;box-shadow:0 10px 30px 0 rgba(0,0,0,.2)}.swagger-ui .dialog-ux .modal-ux-content{overflow-y:auto;max-height:540px;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{font-size:12px;margin:0 0 5px;color:#41444e;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-content h4{font-size:18px;font-weight:600;margin:15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-header{display:-webkit-box;display:-ms-flexbox;display:flex;padding:12px 0;border-bottom:1px solid #ebebeb;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .dialog-ux .modal-ux-header .close-modal{padding:0 10px;border:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui .dialog-ux .modal-ux-header h3{font-size:20px;font-weight:600;margin:0;padding:0 20px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .model{font-size:12px;font-weight:300;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .model-toggle{font-size:10px;position:relative;top:6px;display:inline-block;margin:auto .3em;cursor:pointer;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.swagger-ui .model-toggle:after{display:block;width:20px;height:20px;content:"";background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E") 50% no-repeat;background-size:100%}.swagger-ui .model-jump-to-path{position:relative;cursor:pointer}.swagger-ui .model-jump-to-path .view-line-link{position:absolute;top:-.4em;cursor:pointer}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{position:absolute;top:-1.8em;visibility:hidden;padding:.1em .5em;white-space:nowrap;color:#ebebeb;border-radius:4px;background:rgba(0,0,0,.7)}.swagger-ui section.models{margin:30px 0;border:1px solid rgba(59,65,81,.3);border-radius:4px}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{margin:0 0 5px;border-bottom:1px solid rgba(59,65,81,.3)}.swagger-ui section.models.is-open h4 svg{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.swagger-ui section.models h4{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:10px 20px 10px 10px;cursor:pointer;transition:all .2s;font-family:Titillium Web,sans-serif;color:#777;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui section.models h4 svg{transition:all .4s}.swagger-ui section.models h4 span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{font-size:16px;margin:0 0 10px;font-family:Titillium Web,sans-serif;color:#777}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{margin:0 20px 15px;transition:all .5s;border-radius:4px;background:rgba(0,0,0,.05)}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{padding:10px;border-radius:4px;background:rgba(0,0,0,.1)}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-title{font-size:16px;font-family:Titillium Web,sans-serif;color:#555}.swagger-ui span>span.model,.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#999}.swagger-ui table{width:100%;padding:0 10px;border-collapse:collapse}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{width:100px;padding:0}.swagger-ui table.headers td{font-size:12px;font-weight:300;vertical-align:middle;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{width:20%;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{font-size:12px;font-weight:700;padding:12px 0;text-align:left;border-bottom:1px solid rgba(59,65,81,.2);font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description input[type=text]{width:100%;max-width:340px}.swagger-ui .parameter__name{font-size:16px;font-weight:400;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required:after{font-size:10px;position:relative;top:-6px;padding:5px;content:"required";color:rgba(255,0,0,.6)}.swagger-ui .parameter__in{font-size:12px;font-style:italic;font-family:Source Code Pro,monospace;font-weight:600;color:#888}.swagger-ui .table-container{padding:20px}.swagger-ui .topbar{padding:8px 30px;background-color:#89bf04}.swagger-ui .topbar .topbar-wrapper{-ms-flex-align:center}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;align-items:center}.swagger-ui .topbar a{font-size:1.5em;font-weight:700;max-width:300px;text-decoration:none;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-align:center;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:3;-ms-flex:3;flex:3}.swagger-ui .topbar .download-url-wrapper input[type=text]{width:100%;min-width:350px;margin:0;border:2px solid #547f00;border-radius:4px 0 0 4px;outline:none}.swagger-ui .topbar .download-url-wrapper .download-url-button{font-size:16px;font-weight:700;padding:4px 40px;border:none;border-radius:0 4px 4px 0;background:#547f00;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .info{margin:50px 0}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info p{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info code{padding:3px 5px;border-radius:4px;background:rgba(0,0,0,.05);font-family:Source Code Pro,monospace;font-weight:600;color:#9012fe}.swagger-ui .info a{font-size:14px;transition:all .4s;font-family:Open Sans,sans-serif;color:#4990e2}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{font-size:12px;font-weight:300!important;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .info .title{font-size:36px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info .title small{font-size:10px;position:relative;top:-5px;display:inline-block;margin:0 0 0 5px;padding:2px 4px;vertical-align:super;border-radius:57px;background:#7d8492}.swagger-ui .info .title small pre{margin:0;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .auth-btn-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 0;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.swagger-ui .auth-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{padding-right:20px}.swagger-ui .auth-container{margin:0 0 10px;padding:10px 20px;border-bottom:1px solid #ebebeb}.swagger-ui .auth-container:last-of-type{margin:0;padding:10px 20px;border:0}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{font-size:12px;padding:10px;border-radius:4px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .scopes h2{font-size:14px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{margin:20px;padding:10px 20px;-webkit-animation:scaleUp .5s;animation:scaleUp .5s;border:2px solid #f93e3e;border-radius:4px;background:rgba(249,62,62,.1)}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{font-size:14px;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .errors-wrapper .errors small{color:#666}.swagger-ui .errors-wrapper hgroup{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .errors-wrapper hgroup h4{font-size:20px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}@-webkit-keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.swagger-ui .Resizer.vertical.disabled{display:none} 2 | /*# sourceMappingURL=swagger-ui.css.map*/ -------------------------------------------------------------------------------- /src/main/resources/public/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""} -------------------------------------------------------------------------------- /src/main/resources/public/swagger-ui.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAwxCA;AAoyHA;AAuxHA;AAy4FA;AA2sCA;AAmgCA;AA0iCA;AA+3BA","sourceRoot":""} -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/application/ReactiveMsApplicationTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.application; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | import org.learning.by.example.reactive.microservices.model.LocationRequest; 7 | import org.learning.by.example.reactive.microservices.model.LocationResponse; 8 | import org.learning.by.example.reactive.microservices.test.BasicIntegrationTest; 9 | import org.learning.by.example.reactive.microservices.test.tags.SystemTest; 10 | import org.springframework.boot.web.server.LocalServerPort; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.*; 14 | 15 | 16 | @SystemTest 17 | @DisplayName("ReactiveMsApplication System Tests") 18 | class ReactiveMsApplicationTest extends BasicIntegrationTest { 19 | 20 | private static final String LOCATION_PATH = "/api/location"; 21 | private static final String ADDRESS_ARG = "{address}"; 22 | private static final String GOOGLE_ADDRESS = "1600 Amphitheatre Parkway, Mountain View, CA"; 23 | 24 | @LocalServerPort 25 | private int port; 26 | 27 | @BeforeEach 28 | void setup() { 29 | bindToServerPort(port); 30 | } 31 | 32 | @Test 33 | @DisplayName("get location from URL") 34 | void getLocationTest() { 35 | final LocationResponse response = get( 36 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 37 | LocationResponse.class); 38 | 39 | assertThat(response.getGeographicCoordinates(), not(nullValue())); 40 | assertThat(response.getGeographicCoordinates().getLatitude(), not(nullValue())); 41 | assertThat(response.getGeographicCoordinates().getLongitude(), not(nullValue())); 42 | 43 | assertThat(response.getSunriseSunset(), not(nullValue())); 44 | assertThat(response.getSunriseSunset().getSunrise(), not(isEmptyOrNullString())); 45 | assertThat(response.getSunriseSunset().getSunset(), not(isEmptyOrNullString())); 46 | } 47 | 48 | @Test 49 | @DisplayName("post location") 50 | void postLocationTest() { 51 | final LocationResponse response = post( 52 | builder -> builder.path(LOCATION_PATH).build(), 53 | new LocationRequest(GOOGLE_ADDRESS), 54 | LocationResponse.class); 55 | 56 | assertThat(response.getGeographicCoordinates(), not(nullValue())); 57 | assertThat(response.getGeographicCoordinates().getLatitude(), not(nullValue())); 58 | assertThat(response.getGeographicCoordinates().getLongitude(), not(nullValue())); 59 | 60 | assertThat(response.getSunriseSunset(), not(nullValue())); 61 | assertThat(response.getSunriseSunset().getSunrise(), not(isEmptyOrNullString())); 62 | assertThat(response.getSunriseSunset().getSunset(), not(isEmptyOrNullString())); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/application/ReactiveMsApplicationUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.application; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.learning.by.example.reactive.microservices.test.tags.UnitTest; 6 | 7 | @UnitTest 8 | @DisplayName("ReactiveMsApplication Unit Tests") 9 | class ReactiveMsApplicationUnitTest { 10 | 11 | @Test 12 | void ReactiveMsApplication() { 13 | final String[] args = {}; 14 | ReactiveMsApplication.main(args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/handlers/ApiHandlerTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.handlers; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.learning.by.example.reactive.microservices.exceptions.GeoLocationNotFoundException; 6 | import org.learning.by.example.reactive.microservices.exceptions.GetGeoLocationException; 7 | import org.learning.by.example.reactive.microservices.exceptions.GetSunriseSunsetException; 8 | import org.learning.by.example.reactive.microservices.model.*; 9 | import org.learning.by.example.reactive.microservices.services.GeoLocationService; 10 | import org.learning.by.example.reactive.microservices.services.SunriseSunsetService; 11 | import org.learning.by.example.reactive.microservices.test.HandlersHelper; 12 | import org.learning.by.example.reactive.microservices.test.tags.UnitTest; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.mock.mockito.SpyBean; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.web.reactive.function.server.ServerRequest; 17 | import org.springframework.web.reactive.function.server.ServerResponse; 18 | import reactor.core.publisher.Mono; 19 | 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.Matchers.is; 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.Mockito.*; 24 | 25 | @UnitTest 26 | @DisplayName("ApiHandler Unit Tests") 27 | class ApiHandlerTest { 28 | 29 | private static final String ADDRESS_VARIABLE = "address"; 30 | private static final String GOOGLE_ADDRESS = "1600 Amphitheatre Parkway, Mountain View, CA"; 31 | private static final String SUNRISE_TIME = "12:55:17 PM"; 32 | private static final String SUNSET_TIME = "3:14:28 AM"; 33 | private static final double GOOGLE_LAT = 37.4224082; 34 | private static final double GOOGLE_LNG = -122.0856086; 35 | private static final String NOT_FOUND = "not found"; 36 | private static final String CANT_GET_LOCATION = "cant get location"; 37 | private static final String CANT_GET_SUNRISE_SUNSET = "can't get sunrise sunset"; 38 | 39 | private static final Mono GOOGLE_LOCATION = Mono.just(new GeographicCoordinates(GOOGLE_LAT, GOOGLE_LNG)); 40 | private static final Mono SUNRISE_SUNSET = Mono.just(new SunriseSunset(SUNRISE_TIME, SUNSET_TIME)); 41 | private static final Mono LOCATION_NOT_FOUND = Mono.error(new GeoLocationNotFoundException(NOT_FOUND)); 42 | private static final Mono LOCATION_EXCEPTION = Mono.error(new GetGeoLocationException(CANT_GET_LOCATION)); 43 | private static final Mono SUNRISE_SUNSET_ERROR = Mono.error(new GetSunriseSunsetException(CANT_GET_SUNRISE_SUNSET)); 44 | 45 | 46 | @Autowired 47 | private ApiHandler apiHandler; 48 | 49 | @SpyBean 50 | private GeoLocationService geoLocationService; 51 | 52 | @SpyBean 53 | private SunriseSunsetService sunriseSunsetService; 54 | 55 | private Mono getData(final GeographicCoordinates ignore) { 56 | return SUNRISE_SUNSET; 57 | } 58 | 59 | @Test 60 | void combineTest() { 61 | GOOGLE_LOCATION.zipWhen(this::getData, LocationResponse::new) 62 | .subscribe(this::verifyLocationResponse); 63 | } 64 | 65 | private void verifyLocationResponse(final LocationResponse locationResponse) { 66 | 67 | assertThat(locationResponse.getGeographicCoordinates().getLatitude(), is(GOOGLE_LAT)); 68 | assertThat(locationResponse.getGeographicCoordinates().getLongitude(), is(GOOGLE_LNG)); 69 | 70 | assertThat(locationResponse.getSunriseSunset().getSunrise(), is(SUNRISE_TIME)); 71 | assertThat(locationResponse.getSunriseSunset().getSunset(), is(SUNSET_TIME)); 72 | } 73 | 74 | @Test 75 | void serverResponseTest() { 76 | GOOGLE_LOCATION.zipWhen(this::getData, LocationResponse::new) 77 | .transform(apiHandler::serverResponse).subscribe(this::verifyServerResponse); 78 | } 79 | 80 | private void verifyServerResponse(final ServerResponse serverResponse) { 81 | 82 | assertThat(serverResponse.statusCode(), is(HttpStatus.OK)); 83 | 84 | final LocationResponse locationResponse = HandlersHelper.extractEntity(serverResponse, LocationResponse.class); 85 | 86 | verifyLocationResponse(locationResponse); 87 | } 88 | 89 | @Test 90 | void buildResponseTest() { 91 | final ServerRequest serverRequest = mock(ServerRequest.class); 92 | when(serverRequest.pathVariable(ADDRESS_VARIABLE)).thenReturn(GOOGLE_ADDRESS); 93 | 94 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 95 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 96 | 97 | Mono.just(GOOGLE_ADDRESS).transform(apiHandler::buildResponse).subscribe(this::verifyServerResponse); 98 | 99 | reset(geoLocationService); 100 | reset(sunriseSunsetService); 101 | } 102 | 103 | @Test 104 | void getLocationTest() { 105 | ServerRequest serverRequest = mock(ServerRequest.class); 106 | when(serverRequest.pathVariable(ADDRESS_VARIABLE)).thenReturn(GOOGLE_ADDRESS); 107 | 108 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 109 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 110 | 111 | apiHandler.getLocation(serverRequest).subscribe(this::verifyServerResponse); 112 | 113 | reset(geoLocationService); 114 | reset(sunriseSunsetService); 115 | } 116 | 117 | @Test 118 | void postLocationTest() { 119 | ServerRequest serverRequest = mock(ServerRequest.class); 120 | when(serverRequest.bodyToMono(LocationRequest.class)).thenReturn(Mono.just(new LocationRequest(GOOGLE_ADDRESS))); 121 | 122 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 123 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 124 | 125 | apiHandler.postLocation(serverRequest).subscribe(this::verifyServerResponse); 126 | 127 | reset(geoLocationService); 128 | reset(sunriseSunsetService); 129 | } 130 | 131 | @Test 132 | void getLocationNotFoundTest() { 133 | ServerRequest serverRequest = mock(ServerRequest.class); 134 | when(serverRequest.pathVariable(ADDRESS_VARIABLE)).thenReturn(GOOGLE_ADDRESS); 135 | 136 | doReturn(LOCATION_NOT_FOUND).when(geoLocationService).fromAddress(any()); 137 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 138 | 139 | ServerResponse serverResponse = apiHandler.getLocation(serverRequest).block(); 140 | 141 | assertThat(serverResponse.statusCode(), is(HttpStatus.NOT_FOUND)); 142 | 143 | ErrorResponse error = HandlersHelper.extractEntity(serverResponse, ErrorResponse.class); 144 | 145 | assertThat(error.getError(), is(NOT_FOUND)); 146 | 147 | reset(geoLocationService); 148 | reset(sunriseSunsetService); 149 | } 150 | 151 | @Test 152 | void getLocationErrorSunriseSunsetTest() { 153 | ServerRequest serverRequest = mock(ServerRequest.class); 154 | when(serverRequest.pathVariable(ADDRESS_VARIABLE)).thenReturn(GOOGLE_ADDRESS); 155 | 156 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 157 | doReturn(SUNRISE_SUNSET_ERROR).when(sunriseSunsetService).fromGeographicCoordinates(any()); 158 | 159 | ServerResponse serverResponse = apiHandler.getLocation(serverRequest).block(); 160 | 161 | assertThat(serverResponse.statusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); 162 | 163 | ErrorResponse error = HandlersHelper.extractEntity(serverResponse, ErrorResponse.class); 164 | 165 | assertThat(error.getError(), is(CANT_GET_SUNRISE_SUNSET)); 166 | 167 | reset(geoLocationService); 168 | reset(sunriseSunsetService); 169 | } 170 | 171 | @Test 172 | void getLocationBothServiceErrorTest() { 173 | ServerRequest serverRequest = mock(ServerRequest.class); 174 | when(serverRequest.pathVariable(ADDRESS_VARIABLE)).thenReturn(GOOGLE_ADDRESS); 175 | 176 | doReturn(LOCATION_EXCEPTION).when(geoLocationService).fromAddress(any()); 177 | doReturn(SUNRISE_SUNSET_ERROR).when(sunriseSunsetService).fromGeographicCoordinates(any()); 178 | 179 | ServerResponse serverResponse = apiHandler.getLocation(serverRequest).block(); 180 | 181 | assertThat(serverResponse.statusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); 182 | 183 | ErrorResponse error = HandlersHelper.extractEntity(serverResponse, ErrorResponse.class); 184 | 185 | assertThat(error.getError(), is(CANT_GET_LOCATION)); 186 | 187 | reset(geoLocationService); 188 | reset(sunriseSunsetService); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/handlers/ErrorHandlerTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.handlers; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.learning.by.example.reactive.microservices.exceptions.PathNotFoundException; 6 | import org.learning.by.example.reactive.microservices.model.ErrorResponse; 7 | import org.learning.by.example.reactive.microservices.test.HandlersHelper; 8 | import org.learning.by.example.reactive.microservices.test.tags.UnitTest; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.reactive.function.server.ServerResponse; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.util.function.Consumer; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.is; 18 | 19 | 20 | @UnitTest 21 | @DisplayName("ErrorHandler Unit Tests") 22 | class ErrorHandlerTest { 23 | 24 | private static final String NOT_FOUND = "not found"; 25 | 26 | @Autowired 27 | private ErrorHandler errorHandler; 28 | 29 | @Test 30 | void notFoundTest() { 31 | errorHandler.notFound(null) 32 | .subscribe(checkResponse(HttpStatus.NOT_FOUND, NOT_FOUND)); 33 | } 34 | 35 | @Test 36 | void throwableErrorTest() { 37 | errorHandler.throwableError(new PathNotFoundException(NOT_FOUND)) 38 | .subscribe(checkResponse(HttpStatus.NOT_FOUND, NOT_FOUND)); 39 | } 40 | 41 | @Test 42 | void getResponseTest() { 43 | Mono.just(new PathNotFoundException(NOT_FOUND)).transform(errorHandler::getResponse) 44 | .subscribe(checkResponse(HttpStatus.NOT_FOUND, NOT_FOUND)); 45 | } 46 | 47 | private static Consumer checkResponse(final HttpStatus httpStatus, final String message) { 48 | return serverResponse -> { 49 | assertThat(serverResponse.statusCode(), is(httpStatus)); 50 | assertThat(HandlersHelper.extractEntity(serverResponse, ErrorResponse.class).getError(), is(message)); 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/handlers/ThrowableTranslatorTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.handlers; 2 | 3 | import org.hamcrest.Description; 4 | import org.hamcrest.DiagnosingMatcher; 5 | import org.hamcrest.Factory; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | import org.learning.by.example.reactive.microservices.exceptions.*; 9 | import org.learning.by.example.reactive.microservices.test.tags.UnitTest; 10 | import org.springframework.http.HttpStatus; 11 | import reactor.core.publisher.Mono; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | import static org.hamcrest.Matchers.is; 17 | 18 | @UnitTest 19 | @DisplayName("ThrowableTranslator Unit Tests") 20 | class ThrowableTranslatorTest { 21 | 22 | @Factory 23 | private static DiagnosingMatcher translateTo(HttpStatus status) { 24 | return new DiagnosingMatcher() { 25 | private static final String EXCEPTION = "EXCEPTION"; 26 | 27 | @Override 28 | public void describeTo(final Description description) { 29 | description.appendText("does not translate to ").appendText(status.toString()); 30 | } 31 | 32 | @SuppressWarnings("unchecked") 33 | protected boolean matches(final Object item, final Description mismatch) { 34 | 35 | if (item instanceof Class) { 36 | if (((Class) item).getClass().isInstance(Throwable.class)) { 37 | Class type = (Class) item; 38 | try { 39 | Throwable exception = type.getConstructor(String.class).newInstance(EXCEPTION); 40 | Mono.just(exception).transform(ThrowableTranslator::translate).subscribe(translator -> { 41 | assertThat(translator.getMessage(), is(EXCEPTION)); 42 | assertThat(translator.getHttpStatus(), is(status)); 43 | }); 44 | } catch (InstantiationException | IllegalAccessException | 45 | InvocationTargetException | NoSuchMethodException cause) { 46 | throw new AssertionError("This exception class has not constructor with a String", cause); 47 | } 48 | return true; 49 | } 50 | } 51 | mismatch.appendText(item.toString()); 52 | return false; 53 | } 54 | }; 55 | } 56 | 57 | @Test 58 | void translateInvalidParametersExceptionTest() throws Exception { 59 | assertThat(InvalidParametersException.class, translateTo(HttpStatus.BAD_REQUEST)); 60 | } 61 | 62 | @Test 63 | void translatePathNotFoundExceptionTest() throws Exception { 64 | assertThat(PathNotFoundException.class, translateTo(HttpStatus.NOT_FOUND)); 65 | } 66 | 67 | @Test 68 | void translateLocationNotFoundExceptionTest() throws Exception { 69 | assertThat(GeoLocationNotFoundException.class, translateTo(HttpStatus.NOT_FOUND)); 70 | } 71 | 72 | @Test 73 | void translateGetGetLocationExceptionTest() throws Exception { 74 | assertThat(GetGeoLocationException.class, translateTo(HttpStatus.INTERNAL_SERVER_ERROR)); 75 | } 76 | 77 | @Test 78 | void translateGetSunriseSunsetExceptionExceptionTest() throws Exception { 79 | assertThat(GetSunriseSunsetException.class, translateTo(HttpStatus.INTERNAL_SERVER_ERROR)); 80 | } 81 | 82 | @Test 83 | void translateGenericExceptionTest() throws Exception { 84 | assertThat(Exception.class, translateTo(HttpStatus.INTERNAL_SERVER_ERROR)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/model/WrongRequest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public class WrongRequest { 7 | 8 | private final String surname; 9 | 10 | @JsonCreator 11 | public WrongRequest(@JsonProperty("surname") final String surname) { 12 | this.surname = surname; 13 | } 14 | 15 | public String getSurname() { 16 | return surname; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/routers/ApiRouterTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.routers; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.learning.by.example.reactive.microservices.exceptions.GeoLocationNotFoundException; 8 | import org.learning.by.example.reactive.microservices.handlers.ApiHandler; 9 | import org.learning.by.example.reactive.microservices.handlers.ErrorHandler; 10 | import org.learning.by.example.reactive.microservices.model.*; 11 | import org.learning.by.example.reactive.microservices.services.GeoLocationService; 12 | import org.learning.by.example.reactive.microservices.services.SunriseSunsetService; 13 | import org.learning.by.example.reactive.microservices.test.BasicIntegrationTest; 14 | import org.learning.by.example.reactive.microservices.test.tags.IntegrationTest; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.mock.mockito.SpyBean; 17 | import org.springframework.http.HttpStatus; 18 | import reactor.core.publisher.Mono; 19 | 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.Matchers.is; 22 | import static org.hamcrest.Matchers.isEmptyOrNullString; 23 | import static org.hamcrest.core.IsNot.not; 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.Mockito.doReturn; 26 | import static org.mockito.Mockito.reset; 27 | 28 | @IntegrationTest 29 | @DisplayName("ApiRouter Integration Tests") 30 | class ApiRouterTest extends BasicIntegrationTest { 31 | 32 | private static final String LOCATION_PATH = "/api/location"; 33 | private static final String ADDRESS_ARG = "{address}"; 34 | private static final String WRONG_PATH = "/api/wrong"; 35 | private static final String GOOGLE_ADDRESS = "1600 Amphitheatre Parkway, Mountain View, CA"; 36 | private static final double GOOGLE_LAT = 37.4224082; 37 | private static final double GOOGLE_LNG = -122.0856086; 38 | private static final String NOT_FOUND = "not found"; 39 | private static final String BIG_ERROR = "big error"; 40 | private static final String SUNRISE_TIME = "12:55:17 PM"; 41 | private static final String SUNSET_TIME = "3:14:28 AM"; 42 | 43 | private static final Mono GOOGLE_LOCATION = Mono.just(new GeographicCoordinates(GOOGLE_LAT, GOOGLE_LNG)); 44 | private static final Mono LOCATION_NOT_FOUND = Mono.error(new GeoLocationNotFoundException(NOT_FOUND)); 45 | private static final Mono GENERIC_ERROR = Mono.error(new RuntimeException(BIG_ERROR)); 46 | private static final Mono SUNRISE_SUNSET = Mono.just(new SunriseSunset(SUNRISE_TIME, SUNSET_TIME)); 47 | 48 | @Autowired 49 | private ApiHandler apiHandler; 50 | 51 | @Autowired 52 | private ErrorHandler errorHandler; 53 | 54 | 55 | @SpyBean 56 | private GeoLocationService geoLocationService; 57 | 58 | @SpyBean 59 | private SunriseSunsetService sunriseSunsetService; 60 | 61 | @BeforeEach 62 | void setup() { 63 | super.bindToRouterFunction(ApiRouter.doRoute(apiHandler, errorHandler)); 64 | } 65 | 66 | @BeforeAll 67 | static void setupAll() { 68 | final ApiRouter apiRouter = new ApiRouter(); 69 | } 70 | 71 | @Test 72 | void getWrongPath() { 73 | final ErrorResponse response = get( 74 | builder -> builder.path(WRONG_PATH).build(), 75 | HttpStatus.NOT_FOUND, 76 | ErrorResponse.class); 77 | 78 | assertThat(response.getError(), not(isEmptyOrNullString())); 79 | } 80 | 81 | @Test 82 | void postWrongPath() { 83 | final ErrorResponse response = post( 84 | builder -> builder.path(WRONG_PATH).build(), 85 | HttpStatus.NOT_FOUND, 86 | new LocationRequest(GOOGLE_ADDRESS), 87 | ErrorResponse.class); 88 | 89 | assertThat(response.getError(), not(isEmptyOrNullString())); 90 | } 91 | 92 | @Test 93 | void postWrongObject() { 94 | final ErrorResponse response = post( 95 | builder -> builder.path(LOCATION_PATH).build(), 96 | HttpStatus.BAD_REQUEST, 97 | new WrongRequest(GOOGLE_ADDRESS), 98 | ErrorResponse.class); 99 | 100 | assertThat(response.getError(), not(isEmptyOrNullString())); 101 | } 102 | 103 | @Test 104 | void postLocationWrongObject() { 105 | final ErrorResponse response = post( 106 | builder -> builder.path(LOCATION_PATH).build(), 107 | HttpStatus.BAD_REQUEST, 108 | new WrongRequest(GOOGLE_ADDRESS), 109 | ErrorResponse.class); 110 | 111 | assertThat(response.getError(), not(isEmptyOrNullString())); 112 | } 113 | 114 | @Test 115 | void getLocationTest() { 116 | 117 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 118 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 119 | 120 | final LocationResponse location = get( 121 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 122 | LocationResponse.class); 123 | 124 | assertThat(location.getGeographicCoordinates().getLatitude(), is(GOOGLE_LAT)); 125 | assertThat(location.getGeographicCoordinates().getLongitude(), is(GOOGLE_LNG)); 126 | 127 | reset(geoLocationService); 128 | reset(sunriseSunsetService); 129 | } 130 | 131 | @Test 132 | void postLocationTest() { 133 | 134 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 135 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 136 | 137 | final LocationResponse location = post( 138 | builder -> builder.path(LOCATION_PATH).build(), 139 | new LocationRequest(GOOGLE_ADDRESS), 140 | LocationResponse.class); 141 | 142 | assertThat(location.getGeographicCoordinates().getLatitude(), is(GOOGLE_LAT)); 143 | assertThat(location.getGeographicCoordinates().getLongitude(), is(GOOGLE_LNG)); 144 | 145 | reset(geoLocationService); 146 | reset(sunriseSunsetService); 147 | } 148 | 149 | @Test 150 | void getLocationNotFoundTest() { 151 | 152 | doReturn(LOCATION_NOT_FOUND).when(geoLocationService).fromAddress(any()); 153 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 154 | 155 | final ErrorResponse errorResponse = get( 156 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 157 | HttpStatus.NOT_FOUND, 158 | ErrorResponse.class); 159 | 160 | assertThat(errorResponse.getError(), is(NOT_FOUND)); 161 | 162 | reset(geoLocationService); 163 | reset(sunriseSunsetService); 164 | } 165 | 166 | @Test 167 | void getLocationExceptionTest() { 168 | 169 | doReturn(GENERIC_ERROR).when(geoLocationService).fromAddress(any()); 170 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 171 | 172 | final ErrorResponse errorResponse = get( 173 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 174 | HttpStatus.INTERNAL_SERVER_ERROR, 175 | ErrorResponse.class); 176 | 177 | assertThat(errorResponse.getError(), is(BIG_ERROR)); 178 | 179 | reset(geoLocationService); 180 | reset(sunriseSunsetService); 181 | } 182 | 183 | @Test 184 | void getLocationSunriseSunsetExceptionTest() { 185 | 186 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 187 | doReturn(GENERIC_ERROR).when(sunriseSunsetService).fromGeographicCoordinates(any()); 188 | 189 | final ErrorResponse errorResponse = get( 190 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 191 | HttpStatus.INTERNAL_SERVER_ERROR, 192 | ErrorResponse.class); 193 | 194 | assertThat(errorResponse.getError(), is(BIG_ERROR)); 195 | 196 | reset(geoLocationService); 197 | reset(sunriseSunsetService); 198 | } 199 | 200 | @Test 201 | void getLocationBothServiceExceptionTest() { 202 | 203 | doReturn(GENERIC_ERROR).when(geoLocationService).fromAddress(any()); 204 | doReturn(GENERIC_ERROR).when(sunriseSunsetService).fromGeographicCoordinates(any()); 205 | 206 | final ErrorResponse errorResponse = get( 207 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 208 | HttpStatus.INTERNAL_SERVER_ERROR, 209 | ErrorResponse.class); 210 | 211 | assertThat(errorResponse.getError(), is(BIG_ERROR)); 212 | 213 | reset(geoLocationService); 214 | reset(sunriseSunsetService); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/routers/MainRouterTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.routers; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 8 | import org.learning.by.example.reactive.microservices.model.SunriseSunset; 9 | import org.learning.by.example.reactive.microservices.services.GeoLocationService; 10 | import org.learning.by.example.reactive.microservices.services.SunriseSunsetService; 11 | import org.learning.by.example.reactive.microservices.test.BasicIntegrationTest; 12 | import org.learning.by.example.reactive.microservices.test.tags.IntegrationTest; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.mock.mockito.SpyBean; 15 | import org.springframework.web.reactive.function.server.RouterFunction; 16 | import reactor.core.publisher.Mono; 17 | 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.Mockito.doReturn; 20 | import static org.mockito.Mockito.reset; 21 | 22 | @IntegrationTest 23 | @DisplayName("MainRouter Integration Tests") 24 | class MainRouterTest extends BasicIntegrationTest { 25 | 26 | private static final String STATIC_ROUTE = "/index.html"; 27 | private static final String LOCATION_PATH = "/api/location"; 28 | private static final String ADDRESS_ARG = "{address}"; 29 | private static final double GOOGLE_LAT = 37.4224082; 30 | private static final double GOOGLE_LNG = -122.0856086; 31 | private static final String GOOGLE_ADDRESS = "1600 Amphitheatre Parkway, Mountain View, CA"; 32 | private static final String SUNRISE_TIME = "12:55:17 PM"; 33 | private static final String SUNSET_TIME = "3:14:28 AM"; 34 | 35 | private static final Mono GOOGLE_LOCATION = Mono.just(new GeographicCoordinates(GOOGLE_LAT, GOOGLE_LNG)); 36 | private static final Mono SUNRISE_SUNSET = Mono.just(new SunriseSunset(SUNRISE_TIME, SUNSET_TIME)); 37 | 38 | @SpyBean 39 | private GeoLocationService geoLocationService; 40 | 41 | @SpyBean 42 | private SunriseSunsetService sunriseSunsetService; 43 | 44 | @Autowired 45 | private RouterFunction mainRouterFunction; 46 | 47 | 48 | @BeforeEach 49 | void setup() { 50 | super.bindToRouterFunction(mainRouterFunction); 51 | } 52 | 53 | @BeforeAll 54 | static void setupAll() { 55 | final MainRouter mainRouter = new MainRouter(); 56 | } 57 | 58 | @Test 59 | void staticRouteTest() { 60 | get(builder -> builder.path(STATIC_ROUTE).build()); 61 | } 62 | 63 | @Test 64 | void apiRouteTest() { 65 | 66 | doReturn(GOOGLE_LOCATION).when(geoLocationService).fromAddress(any()); 67 | doReturn(SUNRISE_SUNSET).when(sunriseSunsetService).fromGeographicCoordinates(any()); 68 | 69 | get( 70 | builder -> builder.path(LOCATION_PATH).path("/").path(ADDRESS_ARG).build(GOOGLE_ADDRESS), 71 | String.class 72 | ); 73 | 74 | reset(geoLocationService); 75 | reset(sunriseSunsetService); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/routers/StaticRouterTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.routers; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.jsoup.nodes.Document; 5 | import org.jsoup.nodes.Element; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.learning.by.example.reactive.microservices.test.BasicIntegrationTest; 11 | import org.learning.by.example.reactive.microservices.test.tags.IntegrationTest; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.is; 15 | import static org.hamcrest.Matchers.isEmptyOrNullString; 16 | import static org.hamcrest.core.IsNot.not; 17 | 18 | @IntegrationTest 19 | @DisplayName(" StaticRouter Integration Tests") 20 | class StaticRouterTest extends BasicIntegrationTest { 21 | 22 | private static final String STATIC_PATH = "/index.html"; 23 | private static final String DEFAULT_TITLE = "Swagger UI"; 24 | private static final String TITLE_TAG = "title"; 25 | 26 | @BeforeEach 27 | void setup() { 28 | super.bindToRouterFunction(StaticRouter.doRoute()); 29 | } 30 | 31 | @BeforeAll 32 | static void setupAll() { 33 | final StaticRouter staticRouter = new StaticRouter(); 34 | } 35 | 36 | @Test 37 | void staticContentTest() { 38 | String result = get(builder -> builder.path(STATIC_PATH).build()); 39 | assertThat(result, not(isEmptyOrNullString())); 40 | verifyTitleIs(result, DEFAULT_TITLE); 41 | } 42 | 43 | private void verifyTitleIs(final String html, final String title) { 44 | Document doc = Jsoup.parse(html); 45 | Element element = doc.head().getElementsByTag(TITLE_TAG).get(0); 46 | String text = element.text(); 47 | assertThat(text, is(title)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/services/GeoLocationServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.services; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.learning.by.example.reactive.microservices.exceptions.GeoLocationNotFoundException; 6 | import org.learning.by.example.reactive.microservices.exceptions.GetGeoLocationException; 7 | import org.learning.by.example.reactive.microservices.exceptions.InvalidParametersException; 8 | import org.learning.by.example.reactive.microservices.model.GeoLocationResponse; 9 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 10 | import org.learning.by.example.reactive.microservices.test.tags.UnitTest; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.boot.test.mock.mockito.SpyBean; 13 | import reactor.core.publisher.Mono; 14 | 15 | import static org.hamcrest.CoreMatchers.notNullValue; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.instanceOf; 18 | import static org.hamcrest.Matchers.nullValue; 19 | import static org.hamcrest.core.Is.is; 20 | import static org.learning.by.example.reactive.microservices.test.RestServiceHelper.getMonoFromJsonPath; 21 | import static org.learning.by.example.reactive.microservices.test.RestServiceHelper.mockWebClient; 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.Mockito.*; 24 | 25 | 26 | @UnitTest 27 | @DisplayName("GeoLocationServiceImpl Unit Tests") 28 | class GeoLocationServiceImplTest { 29 | 30 | private static final String GOOGLE_ADDRESS = "1600 Amphitheatre Parkway, Mountain View, CA"; 31 | private static final String GOOGLE_ADDRESS_IN_PARAMS = "?address=" + GOOGLE_ADDRESS; 32 | private static final Mono GOOGLE_ADDRESS_MONO = Mono.just(GOOGLE_ADDRESS); 33 | private static final String BAD_EXCEPTION = "bad exception"; 34 | private static final double GOOGLE_LAT = 37.4224082; 35 | private static final double GOOGLE_LNG = -122.0856086; 36 | private static final String OK_STATUS = "OK"; 37 | 38 | @SpyBean(GeoLocationService.class) 39 | private GeoLocationServiceImpl locationService; 40 | 41 | private static final String JSON_OK = "/json/GeoLocationResponse_OK.json"; 42 | private static final String JSON_NOT_FOUND = "/json/GeoLocationResponse_NOT_FOUND.json"; 43 | private static final String JSON_EMPTY = "/json/GeoLocationResponse_EMPTY.json"; 44 | private static final String JSON_WRONG_STATUS = "/json/GeoLocationResponse_WRONG_STATUS.json"; 45 | 46 | private static final Mono LOCATION_OK = getMonoFromJsonPath(JSON_OK, GeoLocationResponse.class); 47 | private static final Mono LOCATION_NOT_FOUND = getMonoFromJsonPath(JSON_NOT_FOUND, GeoLocationResponse.class); 48 | private static final Mono LOCATION_EMPTY = getMonoFromJsonPath(JSON_EMPTY, GeoLocationResponse.class); 49 | private static final Mono LOCATION_WRONG_STATUS = getMonoFromJsonPath(JSON_WRONG_STATUS, GeoLocationResponse.class); 50 | private static final Mono LOCATION_EXCEPTION = Mono.error(new GetGeoLocationException(BAD_EXCEPTION)); 51 | private static final Mono BIG_EXCEPTION = Mono.error(new RuntimeException(BAD_EXCEPTION)); 52 | 53 | @Value("${GeoLocationServiceImpl.endPoint}") 54 | private String endPoint; 55 | 56 | @Test 57 | void getBeamTest() { 58 | assertThat(locationService, is(notNullValue())); 59 | } 60 | 61 | @Test 62 | void getMockingWebClientTest() { 63 | locationService.webClient = mockWebClient(locationService.webClient, LOCATION_OK); 64 | 65 | final GeoLocationResponse location = GOOGLE_ADDRESS_MONO.transform(locationService::get).block(); 66 | assertThat(location.getStatus(), is(OK_STATUS)); 67 | 68 | reset(locationService.webClient); 69 | } 70 | 71 | @Test 72 | void fromAddressTest() { 73 | doReturn(LOCATION_OK).when(locationService).get(any()); 74 | 75 | final GeographicCoordinates geographicCoordinates = GOOGLE_ADDRESS_MONO.transform(locationService::fromAddress).block(); 76 | 77 | assertThat(geographicCoordinates, is(notNullValue())); 78 | assertThat(geographicCoordinates.getLatitude(), is(GOOGLE_LAT)); 79 | assertThat(geographicCoordinates.getLongitude(), is(GOOGLE_LNG)); 80 | 81 | verify(locationService, times(1)).fromAddress(any()); 82 | verify(locationService, times(1)).buildUrl(any()); 83 | verify(locationService, times(1)).get(any()); 84 | verify(locationService, times(1)).geometryLocation(any()); 85 | 86 | reset(locationService); 87 | } 88 | 89 | @Test 90 | void fromAddressNotFoundTest() { 91 | doReturn(LOCATION_NOT_FOUND).when(locationService).get(any()); 92 | 93 | final GeographicCoordinates geographicCoordinates = GOOGLE_ADDRESS_MONO.transform(locationService::fromAddress) 94 | .onErrorResume(throwable -> { 95 | assertThat(throwable, instanceOf(GeoLocationNotFoundException.class)); 96 | return Mono.empty(); 97 | }).block(); 98 | 99 | assertThat(geographicCoordinates, is(nullValue())); 100 | 101 | verify(locationService, times(1)).fromAddress(any()); 102 | verify(locationService, times(1)).buildUrl(any()); 103 | verify(locationService, times(1)).get(any()); 104 | verify(locationService, times(1)).geometryLocation(any()); 105 | 106 | reset(locationService); 107 | } 108 | 109 | @Test 110 | void fromAddressExceptionTest() { 111 | doReturn(LOCATION_EXCEPTION).when(locationService).get(any()); 112 | 113 | final GeographicCoordinates geographicCoordinates = GOOGLE_ADDRESS_MONO.transform(locationService::fromAddress) 114 | .onErrorResume(throwable -> { 115 | assertThat(throwable, instanceOf(GetGeoLocationException.class)); 116 | return Mono.empty(); 117 | }).block(); 118 | 119 | assertThat(geographicCoordinates, is(nullValue())); 120 | 121 | verify(locationService, times(1)).fromAddress(any()); 122 | verify(locationService, times(1)).buildUrl(any()); 123 | verify(locationService, times(1)).get(any()); 124 | verify(locationService, times(1)).geometryLocation(any()); 125 | 126 | reset(locationService); 127 | } 128 | 129 | @Test 130 | void fromAddressBigExceptionTest() { 131 | doReturn(BIG_EXCEPTION).when(locationService).get(any()); 132 | 133 | final GeographicCoordinates geographicCoordinates = GOOGLE_ADDRESS_MONO.transform(locationService::fromAddress) 134 | .onErrorResume(throwable -> { 135 | assertThat(throwable, instanceOf(GetGeoLocationException.class)); 136 | return Mono.empty(); 137 | }).block(); 138 | 139 | assertThat(geographicCoordinates, is(nullValue())); 140 | 141 | verify(locationService, times(1)).fromAddress(any()); 142 | verify(locationService, times(1)).buildUrl(any()); 143 | verify(locationService, times(1)).get(any()); 144 | verify(locationService, times(1)).geometryLocation(any()); 145 | 146 | reset(locationService); 147 | } 148 | 149 | @Test 150 | void fromAddressEmptyTest() { 151 | doReturn(LOCATION_EMPTY).when(locationService).get(any()); 152 | 153 | final GeographicCoordinates geographicCoordinates = GOOGLE_ADDRESS_MONO.transform(locationService::fromAddress) 154 | .onErrorResume(throwable -> { 155 | assertThat(throwable, instanceOf(GetGeoLocationException.class)); 156 | return Mono.empty(); 157 | }).block(); 158 | 159 | assertThat(geographicCoordinates, is(nullValue())); 160 | 161 | verify(locationService, times(1)).fromAddress(any()); 162 | verify(locationService, times(1)).buildUrl(any()); 163 | verify(locationService, times(1)).get(any()); 164 | verify(locationService, times(1)).geometryLocation(any()); 165 | 166 | reset(locationService); 167 | } 168 | 169 | @Test 170 | void fromAddressWrongStatusTest() { 171 | doReturn(LOCATION_WRONG_STATUS).when(locationService).get(any()); 172 | 173 | final GeographicCoordinates geographicCoordinates = GOOGLE_ADDRESS_MONO.transform(locationService::fromAddress) 174 | .onErrorResume(throwable -> { 175 | assertThat(throwable, instanceOf(GetGeoLocationException.class)); 176 | return Mono.empty(); 177 | }).block(); 178 | 179 | assertThat(geographicCoordinates, is(nullValue())); 180 | 181 | verify(locationService, times(1)).fromAddress(any()); 182 | verify(locationService, times(1)).buildUrl(any()); 183 | verify(locationService, times(1)).get(any()); 184 | verify(locationService, times(1)).geometryLocation(any()); 185 | 186 | reset(locationService); 187 | } 188 | 189 | @Test 190 | void buildUrlTest() { 191 | final String url = GOOGLE_ADDRESS_MONO.transform(locationService::buildUrl).block(); 192 | 193 | assertThat(url, is(notNullValue())); 194 | assertThat(url, is(endPoint.concat(GOOGLE_ADDRESS_IN_PARAMS))); 195 | } 196 | 197 | @Test 198 | void buildUrlEmptyAddressTest() { 199 | final String url = Mono.just("").transform(locationService::buildUrl) 200 | .onErrorResume(throwable -> { 201 | assertThat(throwable, instanceOf(InvalidParametersException.class)); 202 | return Mono.empty(); 203 | }).block(); 204 | 205 | assertThat(url, is(nullValue())); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/services/SunriseSunsetServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.services; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.learning.by.example.reactive.microservices.exceptions.GetSunriseSunsetException; 6 | import org.learning.by.example.reactive.microservices.model.GeographicCoordinates; 7 | import org.learning.by.example.reactive.microservices.model.SunriseSunset; 8 | import org.learning.by.example.reactive.microservices.model.GeoTimesResponse; 9 | import org.learning.by.example.reactive.microservices.test.tags.UnitTest; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.test.mock.mockito.SpyBean; 12 | import reactor.core.publisher.Mono; 13 | 14 | import static org.hamcrest.CoreMatchers.notNullValue; 15 | import static org.hamcrest.CoreMatchers.nullValue; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.instanceOf; 18 | import static org.hamcrest.core.Is.is; 19 | import static org.learning.by.example.reactive.microservices.test.RestServiceHelper.getMonoFromJsonPath; 20 | import static org.learning.by.example.reactive.microservices.test.RestServiceHelper.mockWebClient; 21 | import static org.mockito.ArgumentMatchers.any; 22 | import static org.mockito.Mockito.*; 23 | 24 | @UnitTest 25 | @DisplayName("SunriseSunsetServiceImpl Unit Tests") 26 | class SunriseSunsetServiceImplTest { 27 | 28 | private static final String STATUS_OK = "OK"; 29 | private static final String BAD_EXCEPTION = "bad exception"; 30 | private static final String SUNRISE_TIME = "2017-05-21T12:53:56+00:00"; 31 | private static final String SUNSET_TIME = "2017-05-22T03:16:05+00:00"; 32 | private static final double GOOGLE_LAT = 37.4224082; 33 | private static final double GOOGLE_LNG = -122.0856086; 34 | private static final GeographicCoordinates GOOGLE_LOCATION = new GeographicCoordinates(GOOGLE_LAT, GOOGLE_LNG); 35 | private static final Mono GOOGLE_LOCATION_MONO = Mono.just(GOOGLE_LOCATION); 36 | private static final String GOOGLE_LOCATION_IN_PARAMS = "?lat=" + Double.toString(GOOGLE_LAT) + 37 | "&lng=" + Double.toString(GOOGLE_LNG)+"&date=today&formatted=0"; 38 | 39 | private static final String JSON_OK = "/json/GeoTimesResponse_OK.json"; 40 | private static final String JSON_KO = "/json/GeoTimesResponse_KO.json"; 41 | private static final String JSON_EMPTY = "/json/GeoTimesResponse_EMPTY.json"; 42 | private static final Mono SUNRISE_SUNSET_OK = getMonoFromJsonPath(JSON_OK, GeoTimesResponse.class); 43 | private static final Mono SUNRISE_SUNSET_KO = getMonoFromJsonPath(JSON_KO, GeoTimesResponse.class); 44 | private static final Mono SUNRISE_SUNSET_EMPTY = getMonoFromJsonPath(JSON_EMPTY, GeoTimesResponse.class); 45 | private static final Mono LOCATION_EXCEPTION = Mono.error(new GetSunriseSunsetException(BAD_EXCEPTION)); 46 | private static final Mono BIG_EXCEPTION = Mono.error(new RuntimeException(BAD_EXCEPTION)); 47 | 48 | 49 | @Value("${SunriseSunsetServiceImpl.endPoint}") 50 | private String endPoint; 51 | 52 | @SpyBean(SunriseSunsetService.class) 53 | private SunriseSunsetServiceImpl sunriseSunsetService; 54 | 55 | @Test 56 | void getBeamTest() { 57 | assertThat(sunriseSunsetService, is(notNullValue())); 58 | } 59 | 60 | @Test 61 | void getMockingWebClientTest() { 62 | sunriseSunsetService.webClient = mockWebClient(sunriseSunsetService.webClient, SUNRISE_SUNSET_OK); 63 | 64 | final GeoTimesResponse result = Mono.just(endPoint.concat(GOOGLE_LOCATION_IN_PARAMS)) 65 | .transform(sunriseSunsetService::get).block(); 66 | 67 | assertThat(result, is(notNullValue())); 68 | assertThat(result.getStatus(), is(STATUS_OK)); 69 | assertThat(result.getResults().getSunrise(), is(SUNRISE_TIME)); 70 | assertThat(result.getResults().getSunset(), is(SUNSET_TIME)); 71 | 72 | reset(sunriseSunsetService.webClient); 73 | } 74 | 75 | @Test 76 | void fromLocationTest() { 77 | 78 | doReturn(SUNRISE_SUNSET_OK).when(sunriseSunsetService).get(any()); 79 | 80 | final SunriseSunset result = GOOGLE_LOCATION_MONO.transform(sunriseSunsetService::fromGeographicCoordinates).block(); 81 | 82 | assertThat(result, is(notNullValue())); 83 | assertThat(result.getSunrise(), is(SUNRISE_TIME)); 84 | assertThat(result.getSunset(), is(SUNSET_TIME)); 85 | 86 | verify(sunriseSunsetService, times(1)).fromGeographicCoordinates(any()); 87 | verify(sunriseSunsetService, times(1)).buildUrl(any()); 88 | verify(sunriseSunsetService, times(1)).get(any()); 89 | verify(sunriseSunsetService, times(1)).createResult(any()); 90 | 91 | reset(sunriseSunsetService); 92 | } 93 | 94 | @Test 95 | void fromLocationKOTest() { 96 | 97 | doReturn(SUNRISE_SUNSET_KO).when(sunriseSunsetService).get(any()); 98 | 99 | final SunriseSunset result = GOOGLE_LOCATION_MONO.transform(sunriseSunsetService::fromGeographicCoordinates) 100 | .onErrorResume(throwable -> { 101 | assertThat(throwable, instanceOf(GetSunriseSunsetException.class)); 102 | return Mono.empty(); 103 | }).block(); 104 | 105 | assertThat(result, is(nullValue())); 106 | 107 | verify(sunriseSunsetService, times(1)).fromGeographicCoordinates(any()); 108 | verify(sunriseSunsetService, times(1)).buildUrl(any()); 109 | verify(sunriseSunsetService, times(1)).get(any()); 110 | verify(sunriseSunsetService, times(1)).createResult(any()); 111 | 112 | reset(sunriseSunsetService); 113 | } 114 | 115 | @Test 116 | void fromLocationExceptionTest() { 117 | 118 | doReturn(LOCATION_EXCEPTION).when(sunriseSunsetService).get(any()); 119 | 120 | final SunriseSunset result = GOOGLE_LOCATION_MONO.transform(sunriseSunsetService::fromGeographicCoordinates) 121 | .onErrorResume(throwable -> { 122 | assertThat(throwable, instanceOf(GetSunriseSunsetException.class)); 123 | return Mono.empty(); 124 | }).block(); 125 | 126 | assertThat(result, is(nullValue())); 127 | 128 | verify(sunriseSunsetService, times(1)).fromGeographicCoordinates(any()); 129 | verify(sunriseSunsetService, times(1)).buildUrl(any()); 130 | verify(sunriseSunsetService, times(1)).get(any()); 131 | verify(sunriseSunsetService, times(1)).createResult(any()); 132 | 133 | reset(sunriseSunsetService); 134 | } 135 | 136 | @Test 137 | void fromLocationBigExceptionTest() { 138 | 139 | doReturn(BIG_EXCEPTION).when(sunriseSunsetService).get(any()); 140 | 141 | final SunriseSunset result = GOOGLE_LOCATION_MONO.transform(sunriseSunsetService::fromGeographicCoordinates) 142 | .onErrorResume(throwable -> { 143 | assertThat(throwable, instanceOf(GetSunriseSunsetException.class)); 144 | return Mono.empty(); 145 | }).block(); 146 | 147 | assertThat(result, is(nullValue())); 148 | 149 | verify(sunriseSunsetService, times(1)).fromGeographicCoordinates(any()); 150 | verify(sunriseSunsetService, times(1)).buildUrl(any()); 151 | verify(sunriseSunsetService, times(1)).get(any()); 152 | verify(sunriseSunsetService, times(1)).createResult(any()); 153 | 154 | reset(sunriseSunsetService); 155 | } 156 | 157 | 158 | @Test 159 | void fromLocationErrorTest() { 160 | 161 | doReturn(SUNRISE_SUNSET_EMPTY).when(sunriseSunsetService).get(any()); 162 | 163 | final SunriseSunset result = GOOGLE_LOCATION_MONO.transform(sunriseSunsetService::fromGeographicCoordinates) 164 | .onErrorResume(throwable -> { 165 | assertThat(throwable, instanceOf(GetSunriseSunsetException.class)); 166 | return Mono.empty(); 167 | }).block(); 168 | 169 | assertThat(result, is(nullValue())); 170 | 171 | verify(sunriseSunsetService, times(1)).fromGeographicCoordinates(any()); 172 | verify(sunriseSunsetService, times(1)).buildUrl(any()); 173 | verify(sunriseSunsetService, times(1)).get(any()); 174 | verify(sunriseSunsetService, times(1)).createResult(any()); 175 | 176 | reset(sunriseSunsetService); 177 | } 178 | 179 | @Test 180 | void buildUrlTest() { 181 | final String url = GOOGLE_LOCATION_MONO.transform(sunriseSunsetService::buildUrl).block(); 182 | 183 | assertThat(url, is(notNullValue())); 184 | assertThat(url, is(endPoint.concat(GOOGLE_LOCATION_IN_PARAMS))); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/test/BasicIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.test; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.test.web.reactive.server.WebTestClient; 5 | import org.springframework.web.reactive.function.BodyInserters; 6 | import org.springframework.web.reactive.function.server.RouterFunction; 7 | import org.springframework.web.util.UriBuilder; 8 | 9 | import java.net.URI; 10 | import java.util.function.Function; 11 | 12 | import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; 13 | import static org.springframework.http.MediaType.TEXT_HTML; 14 | 15 | public abstract class BasicIntegrationTest { 16 | private WebTestClient client; 17 | 18 | protected void bindToRouterFunction(final RouterFunction routerFunction) { 19 | client = WebTestClient.bindToRouterFunction(routerFunction).build(); 20 | } 21 | 22 | protected void bindToServerPort(final int port) { 23 | client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); 24 | } 25 | 26 | protected String get(final Function builder) { 27 | return get(builder, HttpStatus.OK); 28 | } 29 | 30 | private String get(final Function builder, final HttpStatus status) { 31 | return new String(client.get() 32 | .uri(builder) 33 | .accept(TEXT_HTML).exchange() 34 | .expectStatus().isEqualTo(status) 35 | .expectHeader().contentType(TEXT_HTML) 36 | .expectBody().returnResult().getResponseBody()); 37 | } 38 | 39 | protected T get(final Function builder, final HttpStatus status, final Class type) { 40 | return client.get() 41 | .uri(builder) 42 | .accept(APPLICATION_JSON_UTF8).exchange() 43 | .expectStatus().isEqualTo(status) 44 | .expectHeader().contentType(APPLICATION_JSON_UTF8) 45 | .expectBody(type) 46 | .returnResult().getResponseBody(); 47 | } 48 | 49 | protected T get(final Function builder, final Class type) { 50 | return get(builder, HttpStatus.OK, type); 51 | } 52 | 53 | protected T post(final Function builder, final HttpStatus status, final K object, final Class type) { 54 | return client.post() 55 | .uri(builder) 56 | .body(BodyInserters.fromObject(object)) 57 | .accept(APPLICATION_JSON_UTF8).exchange() 58 | .expectStatus().isEqualTo(status) 59 | .expectHeader().contentType(APPLICATION_JSON_UTF8) 60 | .expectBody(type) 61 | .returnResult().getResponseBody(); 62 | } 63 | 64 | protected T post(final Function builder, final K object, final Class type) { 65 | return post(builder, HttpStatus.OK, object, type); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/test/HandlersHelper.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.test; 2 | 3 | import org.springframework.web.reactive.function.server.EntityResponse; 4 | import org.springframework.web.reactive.function.server.ServerResponse; 5 | import reactor.core.publisher.Mono; 6 | 7 | public class HandlersHelper { 8 | @SuppressWarnings("unchecked") 9 | public static T extractEntity(final ServerResponse response, final Class type) { 10 | 11 | EntityResponse> entityResponse = (EntityResponse>) response; 12 | 13 | return type.cast(entityResponse.entity().block()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/test/RestServiceHelper.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.test; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.mockito.Mockito; 5 | import org.springframework.web.reactive.function.client.ClientResponse; 6 | import org.springframework.web.reactive.function.client.WebClient; 7 | import reactor.core.publisher.Mono; 8 | 9 | import java.io.IOException; 10 | import java.net.URISyntaxException; 11 | import java.net.URL; 12 | import java.nio.file.Path; 13 | 14 | import static org.mockito.ArgumentMatchers.any; 15 | import static org.mockito.ArgumentMatchers.anyString; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class RestServiceHelper { 19 | private static Mono getMonoFromJson(final String json, final Class type) { 20 | final ObjectMapper objectMapper = new ObjectMapper(); 21 | try { 22 | return Mono.just(objectMapper.readValue(json, type)); 23 | } catch (IOException e) { 24 | throw new RuntimeException(e); 25 | } 26 | } 27 | 28 | public static Mono getMonoFromJsonPath(final String jsonPath, final Class type) { 29 | try { 30 | final URL url = RestServiceHelper.class.getResource(jsonPath); 31 | final Path resPath = java.nio.file.Paths.get(url.toURI()); 32 | final String json = new String(java.nio.file.Files.readAllBytes(resPath), "UTF8"); 33 | return getMonoFromJson(json, type); 34 | } catch (IOException | URISyntaxException e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | 39 | public static WebClient mockWebClient(final WebClient originalClient, final Mono mono){ 40 | WebClient client = spy(originalClient); 41 | 42 | WebClient.RequestHeadersUriSpec uriSpec = mock(WebClient.RequestHeadersUriSpec.class); 43 | doReturn(uriSpec).when(client).get(); 44 | 45 | WebClient.RequestHeadersSpec headerSpec = mock(WebClient.RequestHeadersSpec.class); 46 | doReturn(headerSpec).when(uriSpec).uri(anyString()); 47 | doReturn(headerSpec).when(headerSpec).accept(any()); 48 | 49 | ClientResponse clientResponse = mock(ClientResponse.class); 50 | doReturn(mono).when(clientResponse).bodyToMono((Class) Mockito.any()); 51 | doReturn(Mono.just(clientResponse)).when(headerSpec).exchange(); 52 | 53 | return client; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/test/tags/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.test.tags; 2 | 3 | import org.junit.jupiter.api.Tag; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.learning.by.example.reactive.microservices.application.ReactiveMsApplication; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | import java.lang.annotation.ElementType; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.RetentionPolicy; 13 | import java.lang.annotation.Target; 14 | 15 | @Target({ ElementType.TYPE, ElementType.METHOD }) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Tag("IntegrationTest") 18 | @SpringBootTest(classes = ReactiveMsApplication.class) 19 | @ExtendWith(SpringExtension.class) 20 | @ActiveProfiles("test") 21 | public @interface IntegrationTest { 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/test/tags/SystemTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.test.tags; 2 | 3 | import org.junit.jupiter.api.Tag; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.learning.by.example.reactive.microservices.application.ReactiveMsApplication; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | import java.lang.annotation.ElementType; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.RetentionPolicy; 13 | import java.lang.annotation.Target; 14 | 15 | @Target({ ElementType.TYPE, ElementType.METHOD }) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Tag("SystemTest") 18 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ReactiveMsApplication.class) 19 | @ExtendWith(SpringExtension.class) 20 | @ActiveProfiles("test") 21 | public @interface SystemTest { 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/org/learning/by/example/reactive/microservices/test/tags/UnitTest.java: -------------------------------------------------------------------------------- 1 | package org.learning.by.example.reactive.microservices.test.tags; 2 | 3 | import org.junit.jupiter.api.Tag; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.learning.by.example.reactive.microservices.application.ReactiveMsApplication; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | import java.lang.annotation.ElementType; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.RetentionPolicy; 13 | import java.lang.annotation.Target; 14 | 15 | @Target({ ElementType.TYPE, ElementType.METHOD }) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Tag("UnitTest") 18 | @SpringBootTest(classes = ReactiveMsApplication.class) 19 | @ExtendWith(SpringExtension.class) 20 | @ActiveProfiles("test") 21 | public @interface UnitTest { 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yaml: -------------------------------------------------------------------------------- 1 | logging.level.org.learning.by.example.reactive.microservices: DEBUG -------------------------------------------------------------------------------- /src/test/resources/json/GeoLocationResponse_EMPTY.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /src/test/resources/json/GeoLocationResponse_NOT_FOUND.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [], 3 | "status" : "ZERO_RESULTS" 4 | } -------------------------------------------------------------------------------- /src/test/resources/json/GeoLocationResponse_OK.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [ 3 | { 4 | "address_components" : [ 5 | { 6 | "long_name" : "Google Building 41", 7 | "short_name" : "Google Bldg 41", 8 | "types" : [ "premise" ] 9 | }, 10 | { 11 | "long_name" : "1600", 12 | "short_name" : "1600", 13 | "types" : [ "street_number" ] 14 | }, 15 | { 16 | "long_name" : "Amphitheatre Parkway", 17 | "short_name" : "Amphitheatre Pkwy", 18 | "types" : [ "route" ] 19 | }, 20 | { 21 | "long_name" : "Mountain View", 22 | "short_name" : "Mountain View", 23 | "types" : [ "locality", "political" ] 24 | }, 25 | { 26 | "long_name" : "Santa Clara County", 27 | "short_name" : "Santa Clara County", 28 | "types" : [ "administrative_area_level_2", "political" ] 29 | }, 30 | { 31 | "long_name" : "California", 32 | "short_name" : "CA", 33 | "types" : [ "administrative_area_level_1", "political" ] 34 | }, 35 | { 36 | "long_name" : "United States", 37 | "short_name" : "US", 38 | "types" : [ "country", "political" ] 39 | }, 40 | { 41 | "long_name" : "94043", 42 | "short_name" : "94043", 43 | "types" : [ "postal_code" ] 44 | } 45 | ], 46 | "formatted_address" : "Google Bldg 41, 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA", 47 | "geometry" : { 48 | "bounds" : { 49 | "northeast" : { 50 | "lat" : 37.4228642, 51 | "lng" : -122.0851557 52 | }, 53 | "southwest" : { 54 | "lat" : 37.4221145, 55 | "lng" : -122.0859841 56 | } 57 | }, 58 | "location" : { 59 | "lat" : 37.4224082, 60 | "lng" : -122.0856086 61 | }, 62 | "location_type" : "ROOFTOP", 63 | "viewport" : { 64 | "northeast" : { 65 | "lat" : 37.4238383302915, 66 | "lng" : -122.0842209197085 67 | }, 68 | "southwest" : { 69 | "lat" : 37.4211403697085, 70 | "lng" : -122.0869188802915 71 | } 72 | } 73 | }, 74 | "place_id" : "ChIJxQvW8wK6j4AR3ukttGy3w2s", 75 | "types" : [ "premise" ] 76 | } 77 | ], 78 | "status" : "OK" 79 | } -------------------------------------------------------------------------------- /src/test/resources/json/GeoLocationResponse_WRONG_STATUS.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [], 3 | "status" : "WRONG_STATUS" 4 | } -------------------------------------------------------------------------------- /src/test/resources/json/GeoTimesResponse_EMPTY.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /src/test/resources/json/GeoTimesResponse_KO.json: -------------------------------------------------------------------------------- 1 | { 2 | "status":"KO" 3 | } -------------------------------------------------------------------------------- /src/test/resources/json/GeoTimesResponse_OK.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "sunrise": "2017-05-21T12:53:56+00:00", 4 | "sunset": "2017-05-22T03:16:05+00:00", 5 | "solar_noon": "2017-05-21T20:05:01+00:00", 6 | "day_length": 51729, 7 | "civil_twilight_begin": "2017-05-21T12:24:13+00:00", 8 | "civil_twilight_end": "2017-05-22T03:45:48+00:00", 9 | "nautical_twilight_begin": "2017-05-21T11:47:29+00:00", 10 | "nautical_twilight_end": "2017-05-22T04:22:32+00:00", 11 | "astronomical_twilight_begin": "2017-05-21T11:07:07+00:00", 12 | "astronomical_twilight_end": "2017-05-22T05:02:54+00:00" 13 | }, 14 | "status": "OK" 15 | } --------------------------------------------------------------------------------