├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.adoc ├── mvnw ├── mvnw.cmd ├── pom.xml ├── sample ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── ApplicationClient.java │ │ │ ├── Foo.java │ │ │ └── SocketsApplication.java │ └── resources │ │ └── application.properties │ └── test │ ├── java │ └── com │ │ └── example │ │ └── SocketsApplicationTests.java │ └── resources │ └── catalog │ ├── channel.json │ ├── dump.json │ ├── long.json │ └── msgs.json └── server ├── pom.xml └── src ├── main └── java │ └── org │ └── springframework │ └── mock │ └── rsocket │ ├── MessageMapping.java │ ├── MessageMappingSpec.java │ ├── RSocketMessageCatalog.java │ ├── RSocketMessageRegistry.java │ ├── RSocketServerExtension.java │ ├── json │ ├── JsonRSocketMessageCatalog.java │ └── MessageMappingData.java │ └── server │ ├── GenericRequestHandler.java │ ├── RSocketMessageHeaders.java │ ├── RSocketRouter.java │ └── RSocketServerConfiguration.java └── test ├── java └── org │ └── springframework │ └── mock │ └── rsocket │ ├── server │ ├── Foo.java │ ├── JsonRSocketMessageCatalogTests.java │ └── RSocketServerApplication.java │ └── test │ ├── CborTests.java │ └── DynamicRouteTests.java └── resources ├── application.properties └── catalog ├── channel.json ├── forget.json ├── long.json └── response.json /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/rsocket-test-server/fafc529cd65485e61ac7f16348ea95bb0933f281/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | # RSocket Test Server 2 | 3 | If you have an application that connects to an RSocket server at runtime, how do you test it? We need a way for a test to start a server and tell us where it is listening, and then we need to be able to register request and response examples (a.k.a. "contracts"). That's what this [project](https://github.com/dsyer/rsocket-test-server) provides - it's like Wiremock but for RSocket. 4 | 5 | ## Getting Started 6 | 7 | The easiest way to use this project is as a JUnit (Jupiter) extension, e.g: 8 | 9 | ```java 10 | @SpringBootTest 11 | @ExtendWith(RSocketServerExtension.class) 12 | class SocketsApplicationTests { 13 | ... 14 | } 15 | ``` 16 | 17 | With this extension installed the Spring Boot tests will run with an RSocket server listening on a port given by `test.rsocket.server.port`, so the test can connect directly to it, or (more likely) the code that it is testing will connect to it. You might need to tell it where to connect via the `@SpringBootTest` annotation, e.g. if the application is looking for a property at runtime called `rsocket.port`: 18 | 19 | ```java 20 | @SpringBootTest("rsocket.port=${test.rsocket.server.port}") 21 | @ExtendWith(RSocketServerExtension.class) 22 | class SocketsApplicationTests { 23 | ... 24 | } 25 | ``` 26 | 27 | ## Defining message mappings in JSON 28 | 29 | Test methods can inject an `RSocketMessageCatalog` or `RSocketMessageRegistry` and then use those to set up or inspect the state of the server. By default the server reads JSON contracts from the classpath at `/catalog/*.json`, so you can set those up locally or in a test library that you share with the sarver. The structure of the JSON mirrors the `MessageMapping` that is stored in the `RSocketMessageCatalog`. Here's an example (the request and response are just JSON objects): 30 | 31 | ```json 32 | { 33 | "pattern": "events.response.*", 34 | "frameType": "REQUEST_RESPONSE", 35 | "request": { 36 | "origin": "Client" 37 | }, 38 | "response": { 39 | "origin": "Server", 40 | "interaction": "Response", 41 | "index": 0 42 | } 43 | } 44 | ``` 45 | 46 | This mapping will match any `REQUEST_RESPONSE` frame type on a route that matches the pattern, and which also has a request with a field "origin" equal to "Client". You can also match on a pattern in a field in the request by adding wildcards. Or you can leave the request out to match only on the route. If the frame type is `REQUEST_RESPONSE` then the response is single valued. If the frametype is `REQUEST_STREAM` you can provide a multi-valued "responses", for example: 47 | 48 | ```json 49 | { 50 | "pattern": "my.stream.route", 51 | "frameType": "REQUEST_STREAM", 52 | "responses": [ 53 | { 54 | "origin": "Server", 55 | "interaction": "Stream", 56 | "index": 0 57 | }, 58 | { 59 | "origin": "Server", 60 | "interaction": "Stream", 61 | "index": 1 62 | }, 63 | { 64 | "origin": "Server", 65 | "interaction": "Stream", 66 | "index": 2 67 | } 68 | ] 69 | } 70 | ``` 71 | 72 | In addition you can specify an integer field "repeat" to repeat the responses to form a longer stream. If the frame type is `REQUEST_FNF` then there is no response, and the request will be ignored. And finally, if the frame type is `REQUEST_CHANNEL` then the format of the mapping JSON is the same as the `REQUEST_STREAM`, except that every time a message comes in from the input stream, the output stream is emitted again. 73 | 74 | ## Manipulating and inspecting the message catalog 75 | 76 | If you need more flexibility with the processing rules you can create your own `MessageMapping` from the convenient static factory methods in the interface. You can supply a handler function (except for the fire and forget case), a pattern to match, and optionally a request to match. These methods can be used to dynamically register mappings to define the expected behaviour of the server at runtime. 77 | 78 | You can inspect the state of the server by acquiring a `MessageMapping` from the catalog, and then calling one of the `drain()` methods to drain off the requests that have been received. For example: 79 | 80 | ```java 81 | @SpringBootTest 82 | @ExtendWith(RSocketServerExtension.class) 83 | class DynamicRouteTests { 84 | 85 | private RSocketRequester rsocketRequester; 86 | 87 | public DynamicRouteTests(@Autowired RSocketRequester.Builder rsocketRequesterBuilder, 88 | @Value("${test.rsocket.server.port:7000}") int port) { 89 | rsocketRequester = rsocketRequesterBuilder.tcp("localhost", port); 90 | } 91 | 92 | @Test 93 | void response(RSocketMessageRegistry catalog) { 94 | MessageMapping response = MessageMapping.response("response") 95 | .response(new Foo("Server", "Response")); 96 | catalog.register(response); 97 | assertThat(rsocketRequester.route("response").data(new Foo("Client", "Request")) 98 | .retrieveMono(Foo.class).doOnNext(foo -> { 99 | System.err.println(foo); 100 | assertThat(foo.getOrigin()).isEqualTo("Server"); 101 | }).block()).isNotNull(); 102 | assertThat(response.drain()).hasSize(1); 103 | assertThat(response.drain()).hasSize(0); 104 | } 105 | 106 | } 107 | ``` 108 | 109 | The code here is still really a prototype, but it's already potentially quite useful. So try it out and sens feedback and maybe we can mature it to the point where we can release it. -------------------------------------------------------------------------------- /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 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 2.5.0 8 | 9 | 10 | com.example 11 | rsocket-demo-parent 12 | 0.0.1-SNAPSHOT 13 | pom 14 | Spring RSocket Fake Demo 15 | 16 | 17 | server 18 | sample 19 | 20 | 21 | 22 | 1.8 23 | 24 | 25 | 26 | 27 | spring-libs-snapshot 28 | http://repo.spring.io/libs-snapshot 29 | 30 | true 31 | 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | spring-libs-snapshot 41 | http://repo.spring.io/libs-snapshot 42 | 43 | true 44 | 45 | 46 | true 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | maven-deploy-plugin 55 | 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | rsocket-demo-parent 8 | 0.0.1-SNAPSHOT 9 | .. 10 | 11 | rsocket-sample 12 | rsocket-sample 13 | Demo project for Spring Boot 14 | 15 | 11 16 | 2020.0.3 17 | 0.1.0 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-rsocket 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-webflux 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | io.projectreactor 36 | reactor-test 37 | test 38 | 39 | 40 | org.springframework.experimental 41 | fake-rsocket-server 42 | ${project.version} 43 | test 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-maven-plugin 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/ApplicationClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import reactor.core.publisher.Flux; 19 | import reactor.core.publisher.Mono; 20 | 21 | import org.springframework.beans.factory.annotation.Value; 22 | import org.springframework.messaging.rsocket.RSocketRequester; 23 | import org.springframework.stereotype.Component; 24 | import org.springframework.util.MimeType; 25 | 26 | /** 27 | * @author Dave Syer 28 | * 29 | */ 30 | @Component 31 | public class ApplicationClient { 32 | 33 | private RSocketRequester rsocketRequester; 34 | 35 | public ApplicationClient(RSocketRequester.Builder rsocketRequesterBuilder, 36 | @Value("${rsocket.host:localhost}") String host, 37 | @Value("${rsocket.port:7000}") int port) { 38 | rsocketRequester = rsocketRequesterBuilder 39 | .dataMimeType(MimeType.valueOf("application/json")).tcp(host, port); 40 | } 41 | 42 | public Mono sendAndReceive(Foo foo) { 43 | return rsocketRequester.route("hello").data(foo).retrieveMono(Foo.class); 44 | } 45 | 46 | public Flux stream(Foo foo) { 47 | return rsocketRequester.route("msgs").data(foo).retrieveFlux(Foo.class); 48 | } 49 | 50 | public Flux longStream(Foo foo) { 51 | return rsocketRequester.route("long").data(foo).retrieveFlux(Foo.class); 52 | } 53 | 54 | public Mono forget(Foo foo) { 55 | return rsocketRequester.route("dump").data(foo).send(); 56 | } 57 | 58 | public Flux channel(Flux foos) { 59 | return rsocketRequester.route("channel").data(foos).retrieveFlux(Foo.class); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /sample/src/main/java/com/example/Foo.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import java.time.Instant; 4 | 5 | public class Foo { 6 | 7 | private String origin; 8 | private String interaction; 9 | private long index; 10 | private long created = Instant.now().getEpochSecond(); 11 | 12 | public Foo() { 13 | } 14 | 15 | public Foo(String origin, String interaction) { 16 | this.origin = origin; 17 | this.interaction = interaction; 18 | this.index = 0; 19 | } 20 | 21 | public Foo(String origin, String interaction, long index) { 22 | this.origin = origin; 23 | this.interaction = interaction; 24 | this.index = index; 25 | } 26 | 27 | public String getOrigin() { 28 | return origin; 29 | } 30 | 31 | public void setOrigin(String origin) { 32 | this.origin = origin; 33 | } 34 | 35 | public String getInteraction() { 36 | return interaction; 37 | } 38 | 39 | public void setInteraction(String interaction) { 40 | this.interaction = interaction; 41 | } 42 | 43 | public long getIndex() { 44 | return index; 45 | } 46 | 47 | public void setIndex(long index) { 48 | this.index = index; 49 | } 50 | 51 | public long getCreated() { 52 | return created; 53 | } 54 | 55 | public void setCreated(long created) { 56 | this.created = created; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "Foo [origin=" + origin + ", interaction=" + interaction + ", index=" 62 | + index + ", created=" + created + "]"; 63 | } 64 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/example/SocketsApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import reactor.core.publisher.Flux; 4 | import reactor.core.publisher.Mono; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @SpringBootApplication 12 | @RestController 13 | public class SocketsApplication { 14 | 15 | private ApplicationClient client; 16 | 17 | public SocketsApplication(ApplicationClient client) { 18 | this.client = client; 19 | } 20 | 21 | public static void main(String[] args) { 22 | SpringApplication.run(SocketsApplication.class, args); 23 | } 24 | 25 | @GetMapping("/") 26 | public Mono foo() { 27 | return client.sendAndReceive(new Foo("Client", "Request")); 28 | } 29 | 30 | @GetMapping("/stream") 31 | public Flux stream() { 32 | return client.stream(new Foo("Client", "Request")); 33 | } 34 | 35 | @GetMapping("/long") 36 | public Flux longStream() { 37 | return client.longStream(new Foo("Client", "Request")); 38 | } 39 | 40 | @GetMapping("/forget") 41 | public Mono forget() { 42 | return client.forget(new Foo("Client", "Request")); 43 | } 44 | 45 | @GetMapping("/channel") 46 | public Flux channel() { 47 | return client.channel( 48 | Flux.just(new Foo("Client", "Request"), new Foo("Client", "Request"))); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /sample/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.lazy-initialization=true -------------------------------------------------------------------------------- /sample/src/test/java/com/example/SocketsApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.mock.rsocket.MessageMapping; 10 | import org.springframework.mock.rsocket.RSocketMessageRegistry; 11 | import org.springframework.mock.rsocket.RSocketServerExtension; 12 | import org.springframework.test.web.reactive.server.WebTestClient; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @SpringBootTest("rsocket.port=${test.rsocket.server.port}") 17 | @AutoConfigureWebTestClient 18 | @ExtendWith(RSocketServerExtension.class) 19 | class SocketsApplicationTests { 20 | 21 | @Autowired 22 | private WebTestClient http; 23 | 24 | @Test 25 | void requestResponse(RSocketMessageRegistry catalog) { 26 | catalog.register( 27 | MessageMapping.response("hello").response(new Foo("Server", "response"))); 28 | http.get().uri("/").exchange().expectStatus().isOk().expectBody(Foo.class) 29 | .value(foo -> assertThat(foo.getOrigin()).isEqualTo("Server")); 30 | } 31 | 32 | @Test 33 | void forget() { 34 | http.get().uri("/forget").exchange().expectStatus().isOk() 35 | .expectBody(String.class).value(foo -> assertThat(foo).isNull()); 36 | } 37 | 38 | @Test 39 | void stream() { 40 | assertThat(http.get().uri("/stream").exchange().expectStatus().isOk() 41 | .returnResult(Foo.class).getResponseBody().take(3).doOnNext(foo -> { 42 | System.err.println(foo); 43 | assertThat(foo.getOrigin()).isEqualTo("Server"); 44 | }).count().block()).isEqualTo(3); 45 | } 46 | 47 | @Test 48 | void channel() { 49 | assertThat(http.get().uri("/channel").exchange().expectStatus().isOk() 50 | .returnResult(Foo.class).getResponseBody().take(2).doOnNext(foo -> { 51 | System.err.println(foo); 52 | assertThat(foo.getOrigin()).isEqualTo("Server"); 53 | }).count().block()).isEqualTo(2); 54 | } 55 | 56 | @Test 57 | void longStream() { 58 | assertThat(http.get().uri("/long").exchange().expectStatus().isOk() 59 | .returnResult(Foo.class).getResponseBody().take(6).count().block()) 60 | .isEqualTo(6); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /sample/src/test/resources/catalog/channel.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "channel", 3 | "frameType": "REQUEST_CHANNEL", 4 | "request": {}, 5 | "responses": [{ 6 | "origin": "Server", 7 | "interaction": "Stream", 8 | "index": 0 9 | }] 10 | } -------------------------------------------------------------------------------- /sample/src/test/resources/catalog/dump.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern" : "dump", 3 | "frameType": "REQUEST_FNF", 4 | "request": {} 5 | } -------------------------------------------------------------------------------- /sample/src/test/resources/catalog/long.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "long", 3 | "frameType": "REQUEST_STREAM", 4 | "request": {}, 5 | "responses": [ 6 | { 7 | "origin": "Server", 8 | "interaction": "Stream", 9 | "index": 0 10 | }, 11 | { 12 | "origin": "Server", 13 | "interaction": "Stream", 14 | "index": 1 15 | }, 16 | { 17 | "origin": "Server", 18 | "interaction": "Stream", 19 | "index": 2 20 | } 21 | ], 22 | "repeat": 5 23 | } -------------------------------------------------------------------------------- /sample/src/test/resources/catalog/msgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "msgs", 3 | "frameType": "REQUEST_STREAM", 4 | "request": {}, 5 | "responses": [ 6 | { 7 | "origin": "Server", 8 | "interaction": "Stream", 9 | "index": 0 10 | }, 11 | { 12 | "origin": "Server", 13 | "interaction": "Stream", 14 | "index": 1 15 | }, 16 | { 17 | "origin": "Server", 18 | "interaction": "Stream", 19 | "index": 2 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.5.0 10 | 11 | 12 | org.springframework.experimental 13 | fake-rsocket-server 14 | 0.0.1-SNAPSHOT 15 | fake-rsocket-server 16 | Demo project for Spring Boot 17 | 18 | 1.8 19 | 2020.0.3 20 | 0.1.0 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-rsocket 26 | 27 | 28 | org.junit.jupiter 29 | junit-jupiter-api 30 | 31 | 32 | org.springframework.cloud 33 | spring-cloud-function-rsocket 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | 39 | 40 | io.projectreactor 41 | reactor-test 42 | test 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-configuration-processor 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.cloud 55 | spring-cloud-dependencies 56 | ${spring-cloud.version} 57 | pom 58 | import 59 | 60 | 61 | 62 | 63 | 64 | 65 | spring-libs-snapshot 66 | https://repo.spring.io/snapshot 67 | 68 | true 69 | 70 | 71 | true 72 | 73 | 74 | 75 | spring-libs-milestone 76 | https://repo.spring.io/milestone 77 | 78 | false 79 | 80 | 81 | true 82 | 83 | 84 | 85 | 86 | 87 | 88 | spring-libs-snapshot 89 | https://repo.spring.io/snapshot 90 | 91 | true 92 | 93 | 94 | true 95 | 96 | 97 | 98 | spring-libs-milestone 99 | https://repo.spring.io/milestone 100 | 101 | true 102 | 103 | 104 | true 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/MessageMapping.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket; 17 | 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | import io.rsocket.frame.FrameType; 22 | import reactor.core.publisher.Flux; 23 | 24 | import org.springframework.mock.rsocket.MessageMappingSpec.ChannelBuilder; 25 | import org.springframework.mock.rsocket.MessageMappingSpec.ResponseBuilder; 26 | import org.springframework.mock.rsocket.MessageMappingSpec.StreamBuilder; 27 | 28 | /** 29 | * A mapping from request to a response. The mapping can have any of the standard RSocket 30 | * frame types, and only a request with that framing will match. Matching is also applied 31 | * against a destination (route) pattern, and also optionally on the incoming request 32 | * body, matching field by field. For many applications it will suffice to match only on 33 | * frame type and destination. 34 | * 35 | * @author Dave Syer 36 | * 37 | */ 38 | public interface MessageMapping { 39 | 40 | /** 41 | * Special form of {@link MessageMapping#drain(Class)} where the requests are left as 42 | * typeless map, reflecting the underlying JSON. 43 | */ 44 | List> drain(); 45 | 46 | /** 47 | * Inspect the requests that the server has so far received, converting to the 48 | * specified type. Calling this method resets the state so calling it again before any 49 | * requests are sent will return an empty list. 50 | */ 51 | List drain(Class type); 52 | 53 | /** 54 | * Query if this mapping matches a specific incoming request and destination. 55 | */ 56 | boolean matches(Map request, String destination); 57 | 58 | /** 59 | * The frame type that will match this mapping. 60 | */ 61 | FrameType getFrameType(); 62 | 63 | /** 64 | * A rule for processing the incoming request. The type of request and result depends 65 | * on the frame type. For fire and forget the response is empty, for request-response 66 | * the response is single valued. For "request-channel" the input and output are both 67 | * in principle multiple valued. 68 | */ 69 | Flux> handle(Flux> input); 70 | 71 | /** 72 | * A pattern that matches the destination (route) of the incoming request. May contain 73 | * wildcards. 74 | */ 75 | String getPattern(); 76 | 77 | /** 78 | * Factory method for a mapping with frame type REQUEST_FNF (fire and forget). 79 | */ 80 | public static MessageMapping forget(String pattern) { 81 | FireAndForget result = new FireAndForget(); 82 | result.setPattern(pattern); 83 | return result; 84 | } 85 | 86 | /** 87 | * Factory method for a mapping with frame type REQUEST_CHANNEL (2-way stream). To 88 | * complete the mapping you must supply a handler, mapping requests to responses, or 89 | * response prototype if the response doesn't depend on the request content. 90 | */ 91 | public static ChannelBuilder channel(String pattern) { 92 | return new ChannelBuilder(pattern); 93 | } 94 | 95 | /** 96 | * Factory method for a mapping with frame type REQUEST_STREAM, where the response is 97 | * a stream. To complete the mapping you must supply a handler or response prototype 98 | * (optionally multiple valued). 99 | */ 100 | public static StreamBuilder stream(String pattern) { 101 | return new StreamBuilder(pattern); 102 | } 103 | 104 | /** 105 | * Factory method for a mapping with frame type REQUEST_RESPONSE (single-valued 106 | * response). To complete the mapping you must supply a handler or response prototype. 107 | */ 108 | public static ResponseBuilder response(String pattern) { 109 | return new ResponseBuilder(pattern); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/MessageMappingSpec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.Collection; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.function.Function; 25 | 26 | import com.fasterxml.jackson.databind.ObjectMapper; 27 | import io.rsocket.frame.FrameType; 28 | import reactor.core.publisher.Flux; 29 | import reactor.core.publisher.Mono; 30 | 31 | import org.springframework.util.ObjectUtils; 32 | import org.springframework.web.util.pattern.PathPatternRouteMatcher; 33 | 34 | /** 35 | * Core generic implementation of {@link MessageMapping}. Users don't interact directly 36 | * with this class or its instances, but instead use the {@link MessageMapping} interface 37 | * and the factory methods defined there. 38 | * 39 | * @author Dave Syer 40 | * 41 | */ 42 | abstract class MessageMappingSpec implements MessageMapping { 43 | 44 | private Map request = new HashMap<>(); 45 | 46 | private PathPatternRouteMatcher matcher = new PathPatternRouteMatcher(); 47 | 48 | private FrameType frameType; 49 | 50 | private String pattern; 51 | 52 | private Function>, Flux>> handler; 53 | 54 | volatile private List> seen = new ArrayList<>(); 55 | 56 | private ObjectMapper objectMapper = new ObjectMapper(); 57 | 58 | public void setObjectMapper(ObjectMapper objectMapper) { 59 | this.objectMapper = objectMapper; 60 | } 61 | 62 | @Override 63 | public List> drain() { 64 | List> items = this.seen; 65 | this.seen = new ArrayList<>(); 66 | return items; 67 | } 68 | 69 | @Override 70 | public List drain(Class type) { 71 | List> items = drain(); 72 | List result = new ArrayList<>(); 73 | for (Map map : items) { 74 | result.add(objectMapper.convertValue(map, type)); 75 | } 76 | return result; 77 | } 78 | 79 | protected ObjectMapper getObjectMapper() { 80 | return this.objectMapper; 81 | } 82 | 83 | protected MessageMappingSpec handler( 84 | Function>, Flux>> handler) { 85 | this.handler = maps -> handler.apply(maps.doOnNext(map -> seen.add(map))); 86 | return this; 87 | } 88 | 89 | public MessageMappingSpec request(Object input) { 90 | @SuppressWarnings("unchecked") 91 | Map map = objectMapper.convertValue(input, Map.class); 92 | this.request = map; 93 | return this; 94 | } 95 | 96 | private boolean matches(Map source, Map target) { 97 | if (source == null || target == null) { 98 | return true; 99 | } 100 | for (String key : source.keySet()) { 101 | if (!target.containsKey(key)) { 102 | return false; 103 | } 104 | Object object = target.get(key); 105 | if (object instanceof String) { 106 | Object pattern = source.get(key); 107 | if (pattern instanceof String) { 108 | if (!matcher.match((String) pattern, matcher.parseRoute(key))) { 109 | return false; 110 | } 111 | } 112 | else if (object != null) { 113 | return object.equals(pattern); 114 | } 115 | } 116 | else if (object instanceof Map) { 117 | Object other = source.get(key); 118 | if (target instanceof Map) { 119 | @SuppressWarnings({ "unchecked", "rawtypes" }) 120 | boolean matches = matches((Map) other, (Map) object); 121 | if (!matches) { 122 | return false; 123 | } 124 | } 125 | else { 126 | return false; 127 | } 128 | } 129 | else { 130 | return source.get(key) == null; 131 | } 132 | } 133 | return true; 134 | } 135 | 136 | @Override 137 | public boolean matches(Map request, String destination) { 138 | return matches(this.request, request) 139 | && matcher.match(this.pattern, matcher.parseRoute(destination)); 140 | } 141 | 142 | @Override 143 | public Flux> handle(Flux> input) { 144 | return handler.apply(input); 145 | } 146 | 147 | public PathPatternRouteMatcher getMatcher() { 148 | return matcher; 149 | } 150 | 151 | public Map getRequest() { 152 | return request; 153 | } 154 | 155 | public void setMatcher(PathPatternRouteMatcher matcher) { 156 | this.matcher = matcher; 157 | } 158 | 159 | @Override 160 | public FrameType getFrameType() { 161 | return frameType; 162 | } 163 | 164 | public void setFrameType(FrameType frameType) { 165 | this.frameType = frameType; 166 | } 167 | 168 | @Override 169 | public String getPattern() { 170 | return pattern; 171 | } 172 | 173 | public void setPattern(String pattern) { 174 | this.pattern = pattern; 175 | } 176 | 177 | public static class ChannelBuilder { 178 | 179 | private RequestChannel response; 180 | 181 | public ChannelBuilder request(Object input) { 182 | response.request(input); 183 | return this; 184 | } 185 | 186 | public ChannelBuilder(String pattern) { 187 | RequestChannel result = new RequestChannel(); 188 | result.setPattern(pattern); 189 | this.response = result; 190 | } 191 | 192 | public MessageMapping response(O value) { 193 | response.handler(maps -> maps.map(any -> { 194 | @SuppressWarnings("unchecked") 195 | Map map = response.getObjectMapper().convertValue(value, 196 | Map.class); 197 | return map; 198 | })); 199 | return response; 200 | } 201 | 202 | public MessageMapping handler(Class input, 203 | Function, Flux> handler) { 204 | response.handler(maps -> handler 205 | .apply(maps.map( 206 | map -> response.getObjectMapper().convertValue(map, input))) 207 | .flatMap(result -> { 208 | Object value = result; 209 | if (ObjectUtils.isArray(result)) { 210 | value = Arrays.asList((Object[]) result); 211 | } 212 | if (value instanceof Collection) { 213 | return Flux.fromStream( 214 | ((Collection) value).stream().map(item -> { 215 | @SuppressWarnings("unchecked") 216 | Map map = response 217 | .getObjectMapper() 218 | .convertValue(item, Map.class); 219 | return map; 220 | })); 221 | } 222 | else { 223 | @SuppressWarnings("unchecked") 224 | Map map = response.getObjectMapper() 225 | .convertValue(value, Map.class); 226 | return Mono.just(map); 227 | } 228 | })); 229 | return response; 230 | } 231 | } 232 | 233 | public static class StreamBuilder { 234 | 235 | private RequestStream response; 236 | 237 | public StreamBuilder(String pattern) { 238 | RequestStream result = new RequestStream(); 239 | result.setPattern(pattern); 240 | this.response = result; 241 | } 242 | 243 | public StreamBuilder request(Object input) { 244 | response.request(input); 245 | return this; 246 | } 247 | 248 | public MessageMapping response(O value) { 249 | response.handler(maps -> maps.map(any -> { 250 | @SuppressWarnings("unchecked") 251 | Map map = response.getObjectMapper().convertValue(value, 252 | Map.class); 253 | return map; 254 | })); 255 | return response; 256 | } 257 | 258 | public MessageMapping response(O[] result) { 259 | response.handler(maps -> maps.flatMap(any -> { 260 | Object value = Arrays.asList((Object[]) result); 261 | return Flux.fromStream(((Collection) value).stream().map(item -> { 262 | @SuppressWarnings("unchecked") 263 | Map map = response.getObjectMapper() 264 | .convertValue(item, Map.class); 265 | return map; 266 | })); 267 | })); 268 | return response; 269 | } 270 | 271 | public MessageMapping handler(Class input, Function handler) { 272 | response.handler(maps -> maps 273 | .map(map -> handler 274 | .apply(response.getObjectMapper().convertValue(map, input))) 275 | .flatMap(result -> { 276 | Object value = Arrays.asList((Object[]) result); 277 | return Flux 278 | .fromStream(((Collection) value).stream().map(item -> { 279 | @SuppressWarnings("unchecked") 280 | Map map = response.getObjectMapper() 281 | .convertValue(item, Map.class); 282 | return map; 283 | })); 284 | })); 285 | return response; 286 | } 287 | 288 | } 289 | 290 | public static class ResponseBuilder { 291 | 292 | private RequestResponse response; 293 | 294 | public ResponseBuilder(String pattern) { 295 | RequestResponse result = new RequestResponse(); 296 | result.setPattern(pattern); 297 | this.response = result; 298 | } 299 | 300 | public ResponseBuilder request(Object input) { 301 | response.request(input); 302 | return this; 303 | } 304 | 305 | public MessageMapping response(O value) { 306 | response.handler(maps -> maps.map(any -> { 307 | @SuppressWarnings("unchecked") 308 | Map map = response.getObjectMapper().convertValue(value, 309 | Map.class); 310 | return map; 311 | })); 312 | return response; 313 | } 314 | 315 | public MessageMapping handler(Class input, Function handler) { 316 | response.handler(maps -> maps 317 | .map(map -> handler 318 | .apply(response.getObjectMapper().convertValue(map, input))) 319 | .map(result -> { 320 | @SuppressWarnings("unchecked") 321 | Map map = response.getObjectMapper() 322 | .convertValue(result, Map.class); 323 | return map; 324 | })); 325 | return response; 326 | } 327 | } 328 | 329 | } 330 | 331 | class RequestResponse extends MessageMappingSpec { 332 | private Map response = new HashMap<>(); 333 | 334 | public RequestResponse() { 335 | handler(input -> input.map(request -> response)); 336 | setFrameType(FrameType.REQUEST_RESPONSE); 337 | } 338 | 339 | public Map getResponse() { 340 | return this.response; 341 | } 342 | 343 | } 344 | 345 | class FireAndForget extends MessageMappingSpec { 346 | public FireAndForget() { 347 | handler(input -> input.thenMany(Flux.empty())); 348 | setFrameType(FrameType.REQUEST_FNF); 349 | } 350 | } 351 | 352 | class RequestStream extends MessageMappingSpec { 353 | 354 | private int repeat = 1; 355 | 356 | private List> responses = new ArrayList<>(); 357 | 358 | public RequestStream() { 359 | handler(input -> input.flatMap(request -> Flux.fromIterable(getResponses()))); 360 | setFrameType(FrameType.REQUEST_STREAM); 361 | } 362 | 363 | public Map getResponse() { 364 | if (responses.isEmpty()) { 365 | responses.add(new HashMap<>()); 366 | } 367 | return this.responses.get(0); 368 | } 369 | 370 | public List> getResponses() { 371 | List> result = new ArrayList<>(); 372 | int total = repeat <= 0 ? 0 : repeat; 373 | for (int i = 0; i < total; i++) { 374 | result.addAll(responses); 375 | } 376 | return result; 377 | } 378 | 379 | public int getRepeat() { 380 | return repeat; 381 | } 382 | 383 | public void setRepeat(int repeat) { 384 | this.repeat = repeat; 385 | } 386 | } 387 | 388 | class RequestChannel extends MessageMappingSpec { 389 | 390 | private int repeat = 1; 391 | 392 | private List> responses = new ArrayList<>(); 393 | 394 | public RequestChannel() { 395 | handler(input -> input.flatMap(request -> Flux.fromIterable(getResponses()))); 396 | setFrameType(FrameType.REQUEST_CHANNEL); 397 | } 398 | 399 | public Map getResponse() { 400 | if (responses.isEmpty()) { 401 | responses.add(new HashMap<>()); 402 | } 403 | return this.responses.get(0); 404 | } 405 | 406 | public List> getResponses() { 407 | List> result = new ArrayList<>(); 408 | int total = repeat <= 0 ? 0 : repeat; 409 | for (int i = 0; i < total; i++) { 410 | result.addAll(responses); 411 | } 412 | return result; 413 | } 414 | 415 | public int getRepeat() { 416 | return repeat; 417 | } 418 | 419 | public void setRepeat(int repeat) { 420 | this.repeat = repeat; 421 | } 422 | } -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/RSocketMessageCatalog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket; 17 | 18 | import java.util.Collection; 19 | 20 | /** 21 | * A catalog of {@link MessageMapping} instances. You can inject a catalog into a test 22 | * method if the test has the {@link RSocketServerExtension}. 23 | * 24 | * @author Dave Syer 25 | * 26 | */ 27 | public interface RSocketMessageCatalog { 28 | 29 | Collection getMappings(); 30 | 31 | MessageMapping getMapping(String name); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/RSocketMessageRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket; 17 | 18 | /** 19 | * Defines a registration API for {@link MessageMapping} instances. The RSocket server 20 | * uses the registered mappings to respond to requests. You can inject a catalog into a 21 | * test method. 22 | * 23 | * @author Dave Syer 24 | * 25 | */ 26 | public interface RSocketMessageRegistry extends RSocketMessageCatalog { 27 | 28 | void register(MessageMapping map); 29 | } 30 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/RSocketServerExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.concurrent.TimeoutException; 21 | 22 | import org.junit.jupiter.api.extension.AfterAllCallback; 23 | import org.junit.jupiter.api.extension.BeforeAllCallback; 24 | import org.junit.jupiter.api.extension.ExtensionContext; 25 | import org.junit.jupiter.api.extension.ParameterContext; 26 | import org.junit.jupiter.api.extension.ParameterResolutionException; 27 | import org.junit.jupiter.api.extension.ParameterResolver; 28 | 29 | import org.springframework.boot.builder.SpringApplicationBuilder; 30 | import org.springframework.boot.rsocket.context.RSocketServerBootstrap; 31 | import org.springframework.context.ApplicationContext; 32 | import org.springframework.context.ConfigurableApplicationContext; 33 | import org.springframework.core.env.ConfigurableEnvironment; 34 | import org.springframework.core.env.MapPropertySource; 35 | import org.springframework.core.env.MutablePropertySources; 36 | import org.springframework.core.env.PropertySource; 37 | import org.springframework.test.context.junit.jupiter.SpringExtension; 38 | import org.springframework.util.ClassUtils; 39 | 40 | /** 41 | * A JUnit (Jupiter) extension that starts an RSocket server and provides a couple of 42 | * mechanisms to control the way it responds to requests from the test code. One way is to 43 | * read JSON definitions of "contracts" (really mock requests and responses) from the 44 | * classpath. Another is to register those mock behaviours dynamically via a 45 | * MessageMappingRegistry, which can be injected into a test method as a 46 | * parameter. The server listens on a TCP port that can be injected into a Spring Boot 47 | * integration test as @Value("test.rsocket.server.port"). 48 | * 49 | * @author Dave Syer 50 | * 51 | */ 52 | public class RSocketServerExtension 53 | implements BeforeAllCallback, AfterAllCallback, ParameterResolver { 54 | 55 | public static String PROPERTY_NAME = "test.rsocket.server.port"; 56 | 57 | private static final int MAX_COUNT = 180; 58 | 59 | private ConfigurableApplicationContext application; 60 | 61 | @Override 62 | public void afterAll(ExtensionContext context) throws Exception { 63 | if (application != null && application.isRunning()) { 64 | application.close(); 65 | } 66 | application = null; 67 | } 68 | 69 | @Override 70 | public void beforeAll(ExtensionContext context) throws Exception { 71 | application = RSocketServerExtension.run(); 72 | RSocketServerBootstrap server = application.getBean(RSocketServerBootstrap.class); 73 | int count = 0; 74 | while (!server.isRunning() && count++ < MAX_COUNT) { 75 | try { 76 | Thread.sleep(20); 77 | } 78 | catch (InterruptedException e) { 79 | Thread.currentThread().interrupt(); 80 | } 81 | } 82 | if (count >= MAX_COUNT) { 83 | throw new TimeoutException("Timed out waiting for RSocket server to start"); 84 | } 85 | setPortProperty(SpringExtension.getApplicationContext(context), 86 | application.getEnvironment().getProperty("local.rsocket.server.port", 87 | Integer.class)); 88 | } 89 | 90 | @Override 91 | public boolean supportsParameter(ParameterContext parameterContext, 92 | ExtensionContext extensionContext) throws ParameterResolutionException { 93 | return RSocketMessageCatalog.class 94 | .isAssignableFrom(parameterContext.getParameter().getType()); 95 | } 96 | 97 | @Override 98 | public Object resolveParameter(ParameterContext parameterContext, 99 | ExtensionContext extensionContext) { 100 | return getRSocketMessageCatalog(extensionContext); 101 | } 102 | 103 | private RSocketMessageRegistry getRSocketMessageCatalog(ExtensionContext context) { 104 | return application != null ? application.getBean(RSocketMessageRegistry.class) 105 | : null; 106 | } 107 | 108 | private void setPortProperty(ApplicationContext context, int port) { 109 | if (context instanceof ConfigurableApplicationContext) { 110 | setPortProperty(((ConfigurableApplicationContext) context).getEnvironment(), 111 | port); 112 | } 113 | if (context.getParent() != null) { 114 | setPortProperty(context.getParent(), port); 115 | } 116 | } 117 | 118 | private void setPortProperty(ConfigurableEnvironment environment, int port) { 119 | MutablePropertySources sources = environment.getPropertySources(); 120 | PropertySource source = sources.get("server.ports"); 121 | if (source == null) { 122 | source = new MapPropertySource("server.ports", new HashMap<>()); 123 | sources.addFirst(source); 124 | } 125 | setPortProperty(port, source); 126 | } 127 | 128 | @SuppressWarnings("unchecked") 129 | private void setPortProperty(int port, PropertySource source) { 130 | ((Map) source.getSource()).put(PROPERTY_NAME, port); 131 | } 132 | 133 | public static ConfigurableApplicationContext run(String... args) { 134 | // Break cycle using reflection 135 | Class mainClass = ClassUtils.resolveClassName( 136 | "org.springframework.mock.rsocket.server.RSocketServerConfiguration", 137 | null); 138 | return new SpringApplicationBuilder(mainClass).properties( 139 | "spring.rsocket.server.port=0", "spring.main.web-application-type=none", 140 | "spring.main.banner-mode=off").run(args); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/json/JsonRSocketMessageCatalog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket.json; 17 | 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.ArrayList; 20 | import java.util.Collection; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import com.fasterxml.jackson.databind.ObjectMapper; 26 | 27 | import org.springframework.beans.factory.InitializingBean; 28 | import org.springframework.core.io.Resource; 29 | import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 30 | import org.springframework.mock.rsocket.MessageMapping; 31 | import org.springframework.mock.rsocket.RSocketMessageRegistry; 32 | import org.springframework.util.StreamUtils; 33 | 34 | /** 35 | * @author Dave Syer 36 | * 37 | */ 38 | public class JsonRSocketMessageCatalog 39 | implements RSocketMessageRegistry, InitializingBean { 40 | 41 | private ObjectMapper json = new ObjectMapper(); 42 | 43 | private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); 44 | 45 | private Map maps = new HashMap<>(); 46 | 47 | @Override 48 | public void afterPropertiesSet() throws Exception { 49 | for (Resource resource : resolver.getResources("catalog/**/*.json")) { 50 | MessageMappingData map = json.readValue(StreamUtils 51 | .copyToString(resource.getInputStream(), StandardCharsets.UTF_8), 52 | MessageMappingData.class); 53 | maps.put(map.getPattern(), map.mapping()); 54 | } 55 | } 56 | 57 | @Override 58 | public Collection getMappings() { 59 | List values = new ArrayList<>(maps.values()); 60 | return values; 61 | } 62 | 63 | @Override 64 | public MessageMapping getMapping(String name) { 65 | for (MessageMapping map : getMappings()) { 66 | if (map.matches(null, name)) { 67 | return map; 68 | } 69 | } 70 | return null; 71 | } 72 | 73 | @Override 74 | public void register(MessageMapping map) { 75 | maps.put(map.getPattern(), map); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/json/MessageMappingData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket.json; 17 | 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | import io.rsocket.frame.FrameType; 24 | import reactor.core.publisher.Flux; 25 | 26 | import org.springframework.mock.rsocket.MessageMapping; 27 | 28 | /** 29 | * @author Dave Syer 30 | * 31 | */ 32 | class MessageMappingData { 33 | 34 | private Map request = new HashMap<>(); 35 | 36 | private FrameType frameType; 37 | 38 | private String pattern; 39 | 40 | private int repeat = 1; 41 | 42 | private List> responses = new ArrayList<>(); 43 | 44 | public Map getResponse() { 45 | if (responses.isEmpty()) { 46 | responses.add(new HashMap<>()); 47 | } 48 | return this.responses.get(0); 49 | } 50 | 51 | public List> getResponses() { 52 | List> result = new ArrayList<>(); 53 | int total = repeat <= 0 ? 0 : repeat; 54 | for (int i = 0; i < total; i++) { 55 | result.addAll(responses); 56 | } 57 | return result; 58 | } 59 | 60 | public int getRepeat() { 61 | return repeat; 62 | } 63 | 64 | public void setRepeat(int repeat) { 65 | this.repeat = repeat; 66 | } 67 | 68 | public Map getRequest() { 69 | return request; 70 | } 71 | 72 | public FrameType getFrameType() { 73 | return frameType; 74 | } 75 | 76 | public void setFrameType(FrameType frameType) { 77 | this.frameType = frameType; 78 | } 79 | 80 | public String getPattern() { 81 | return pattern; 82 | } 83 | 84 | public void setPattern(String pattern) { 85 | this.pattern = pattern; 86 | } 87 | 88 | public MessageMapping mapping() { 89 | switch (frameType) { 90 | case REQUEST_FNF: 91 | return MessageMapping.forget(pattern); 92 | case REQUEST_RESPONSE: 93 | return MessageMapping.response(pattern).request(this.request) 94 | .handler(Object.class, input -> getResponse()); 95 | case REQUEST_STREAM: 96 | return MessageMapping.stream(pattern).request(this.request).handler( 97 | Object.class, input -> getResponses().toArray(new Object[0])); 98 | case REQUEST_CHANNEL: 99 | return MessageMapping.channel(pattern).request(this.request).handler( 100 | Object.class, 101 | input -> input.flatMap(item -> Flux.fromIterable(getResponses()))); 102 | default: 103 | throw new IllegalStateException("Cannot create: " + frameType); 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/server/GenericRequestHandler.java: -------------------------------------------------------------------------------- 1 | package org.springframework.mock.rsocket.server; 2 | 3 | import java.util.Map; 4 | import java.util.function.Function; 5 | 6 | import io.rsocket.frame.FrameType; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import reactor.core.publisher.Flux; 10 | 11 | import org.springframework.messaging.Message; 12 | import org.springframework.messaging.support.MessageBuilder; 13 | import org.springframework.mock.rsocket.MessageMapping; 14 | import org.springframework.mock.rsocket.RSocketMessageCatalog; 15 | 16 | public class GenericRequestHandler implements 17 | Function>>, Flux>>> { 18 | 19 | private static final Logger log = LoggerFactory 20 | .getLogger(GenericRequestHandler.class); 21 | 22 | private final RSocketMessageCatalog catalog; 23 | 24 | private FrameType frameType; 25 | 26 | public GenericRequestHandler(FrameType frameType, RSocketMessageCatalog catalog) { 27 | this.frameType = frameType; 28 | this.catalog = catalog; 29 | } 30 | 31 | @Override 32 | public Flux>> apply( 33 | Flux>> input) { 34 | return input.flatMap(message -> { 35 | log.info("Incoming: " + message); 36 | RSocketMessageHeaders headers = new RSocketMessageHeaders( 37 | message.getHeaders()); 38 | String destination = headers.getDestination(); 39 | for (MessageMapping map : catalog.getMappings()) { 40 | if (map.getFrameType() == frameType 41 | && map.matches(message.getPayload(), destination)) { 42 | return map.handle(input.map(msg -> msg.getPayload())) 43 | .map(response -> MessageBuilder.withPayload(response) 44 | .copyHeadersIfAbsent(message.getHeaders()).build()); 45 | } 46 | } 47 | throw new IllegalStateException( 48 | "No MessageMapping found to match: " + destination); 49 | }); 50 | } 51 | } -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/server/RSocketMessageHeaders.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket.server; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import io.rsocket.frame.FrameType; 22 | 23 | /** 24 | * @author Dave Syer 25 | * 26 | */ 27 | public class RSocketMessageHeaders extends HashMap { 28 | 29 | public RSocketMessageHeaders(Map map) { 30 | super(map); 31 | } 32 | 33 | public String getDestination() { 34 | return (String) get("lookupDestination"); 35 | } 36 | 37 | public FrameType getFrameType() { 38 | return (FrameType) get("rsocketFrameType"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/server/RSocketRouter.java: -------------------------------------------------------------------------------- 1 | package org.springframework.mock.rsocket.server; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.cloud.function.context.MessageRoutingCallback; 7 | import org.springframework.messaging.Message; 8 | 9 | public class RSocketRouter implements MessageRoutingCallback { 10 | 11 | private static final Logger log = LoggerFactory.getLogger(RSocketRouter.class); 12 | 13 | @Override 14 | public String functionDefinition(Message message) { 15 | log.info("Routing: " + message); 16 | RSocketMessageHeaders headers = new RSocketMessageHeaders(message.getHeaders()); 17 | switch (headers.getFrameType()) { 18 | case REQUEST_RESPONSE: 19 | return "request-response"; 20 | case REQUEST_STREAM: 21 | return "request-stream"; 22 | case REQUEST_FNF: 23 | return "fire-and-forget"; 24 | case REQUEST_CHANNEL: 25 | return "request-channel"; 26 | default: 27 | throw new IllegalStateException("Cannot route: " + headers.getFrameType()); 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /server/src/main/java/org/springframework/mock/rsocket/server/RSocketServerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket.server; 17 | 18 | import io.rsocket.frame.FrameType; 19 | 20 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 21 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 22 | import org.springframework.context.annotation.Bean; 23 | import org.springframework.context.annotation.ComponentScan; 24 | import org.springframework.context.annotation.Configuration; 25 | import org.springframework.mock.rsocket.RSocketMessageCatalog; 26 | import org.springframework.mock.rsocket.RSocketMessageRegistry; 27 | import org.springframework.mock.rsocket.json.JsonRSocketMessageCatalog; 28 | 29 | /** 30 | * @author Dave Syer 31 | * 32 | */ 33 | @EnableAutoConfiguration 34 | @ComponentScan 35 | @Configuration(proxyBeanMethods = false) 36 | public class RSocketServerConfiguration { 37 | 38 | @Bean 39 | @ConditionalOnMissingBean 40 | public RSocketMessageRegistry rSocketMessageCatalog() { 41 | return new JsonRSocketMessageCatalog(); 42 | } 43 | 44 | @Bean 45 | public RSocketRouter customFunctionRouter() { 46 | return new RSocketRouter(); 47 | } 48 | 49 | @Bean("fire-and-forget") 50 | public GenericRequestHandler fireAndForgetHandler(RSocketMessageCatalog catalog) { 51 | return new GenericRequestHandler(FrameType.REQUEST_FNF, catalog); 52 | } 53 | 54 | @Bean("request-response") 55 | public GenericRequestHandler requestResponseHandler(RSocketMessageCatalog catalog) { 56 | return new GenericRequestHandler(FrameType.REQUEST_RESPONSE, catalog); 57 | } 58 | 59 | @Bean("request-stream") 60 | public GenericRequestHandler requestStreamHandler(RSocketMessageCatalog catalog) { 61 | return new GenericRequestHandler(FrameType.REQUEST_STREAM, catalog); 62 | } 63 | 64 | @Bean("request-channel") 65 | public GenericRequestHandler requestChannelHandler(RSocketMessageCatalog catalog) { 66 | return new GenericRequestHandler(FrameType.REQUEST_CHANNEL, catalog); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/src/test/java/org/springframework/mock/rsocket/server/Foo.java: -------------------------------------------------------------------------------- 1 | package org.springframework.mock.rsocket.server; 2 | 3 | import java.time.Instant; 4 | 5 | public class Foo { 6 | 7 | private String origin; 8 | private String interaction; 9 | private long index; 10 | private long created = Instant.now().getEpochSecond(); 11 | 12 | public Foo() { 13 | } 14 | 15 | public Foo(String origin, String interaction) { 16 | this.origin = origin; 17 | this.interaction = interaction; 18 | this.index = 0; 19 | } 20 | 21 | public Foo(String origin, String interaction, long index) { 22 | this.origin = origin; 23 | this.interaction = interaction; 24 | this.index = index; 25 | } 26 | 27 | public String getOrigin() { 28 | return origin; 29 | } 30 | 31 | public void setOrigin(String origin) { 32 | this.origin = origin; 33 | } 34 | 35 | public String getInteraction() { 36 | return interaction; 37 | } 38 | 39 | public void setInteraction(String interaction) { 40 | this.interaction = interaction; 41 | } 42 | 43 | public long getIndex() { 44 | return index; 45 | } 46 | 47 | public void setIndex(long index) { 48 | this.index = index; 49 | } 50 | 51 | public long getCreated() { 52 | return created; 53 | } 54 | 55 | public void setCreated(long created) { 56 | this.created = created; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "Foo [origin=" + origin + ", interaction=" + interaction + ", index=" 62 | + index + ", created=" + created + "]"; 63 | } 64 | } -------------------------------------------------------------------------------- /server/src/test/java/org/springframework/mock/rsocket/server/JsonRSocketMessageCatalogTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.mock.rsocket.server; 17 | 18 | import java.util.HashMap; 19 | 20 | import io.rsocket.frame.FrameType; 21 | import org.junit.jupiter.api.Test; 22 | import reactor.core.publisher.Flux; 23 | 24 | import org.springframework.mock.rsocket.MessageMapping; 25 | import org.springframework.mock.rsocket.json.JsonRSocketMessageCatalog; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | /** 30 | * @author Dave Syer 31 | * 32 | */ 33 | public class JsonRSocketMessageCatalogTests { 34 | 35 | private JsonRSocketMessageCatalog catalog = new JsonRSocketMessageCatalog(); 36 | 37 | @Test 38 | public void channel() throws Exception { 39 | catalog.afterPropertiesSet(); 40 | MessageMapping mapping = catalog.getMapping("channel"); 41 | assertThat(mapping.getFrameType()).isEqualTo(FrameType.REQUEST_CHANNEL); 42 | assertThat(mapping.handle(Flux.just(new HashMap<>())).toIterable()).hasSize(1); 43 | assertThat(mapping.matches(new HashMap<>(), "channel")).isTrue(); 44 | } 45 | 46 | @Test 47 | public void stream() throws Exception { 48 | catalog.afterPropertiesSet(); 49 | MessageMapping mapping = catalog.getMapping("long"); 50 | assertThat(mapping.getFrameType()).isEqualTo(FrameType.REQUEST_STREAM); 51 | assertThat(mapping.handle(Flux.just(new HashMap<>())).toIterable()).hasSize(15); 52 | } 53 | 54 | @Test 55 | public void response() throws Exception { 56 | catalog.afterPropertiesSet(); 57 | MessageMapping mapping = catalog.getMapping("response"); 58 | assertThat(mapping.getFrameType()).isEqualTo(FrameType.REQUEST_RESPONSE); 59 | assertThat(mapping.matches(new HashMap<>(), "response")).isFalse(); 60 | } 61 | 62 | @Test 63 | public void forget() throws Exception { 64 | catalog.afterPropertiesSet(); 65 | MessageMapping mapping = catalog.getMapping("forget"); 66 | assertThat(mapping.getFrameType()).isEqualTo(FrameType.REQUEST_FNF); 67 | assertThat(mapping.matches(new HashMap<>(), "forget")).isTrue(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /server/src/test/java/org/springframework/mock/rsocket/server/RSocketServerApplication.java: -------------------------------------------------------------------------------- 1 | package org.springframework.mock.rsocket.server; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | import org.springframework.context.annotation.Import; 5 | import org.springframework.mock.rsocket.RSocketServerExtension; 6 | 7 | @SpringBootApplication 8 | @Import(RSocketServerConfiguration.class) 9 | public class RSocketServerApplication { 10 | 11 | public static void main(String[] args) { 12 | RSocketServerExtension.run(args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /server/src/test/java/org/springframework/mock/rsocket/test/CborTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.mock.rsocket.test; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.messaging.rsocket.RSocketRequester; 12 | import org.springframework.mock.rsocket.MessageMapping; 13 | import org.springframework.mock.rsocket.RSocketMessageRegistry; 14 | import org.springframework.mock.rsocket.RSocketServerExtension; 15 | import org.springframework.mock.rsocket.server.Foo; 16 | import org.springframework.util.MimeType; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @SpringBootTest 21 | @ExtendWith(RSocketServerExtension.class) 22 | class CborTests { 23 | 24 | private RSocketRequester rsocketRequester; 25 | 26 | public CborTests(@Autowired RSocketRequester.Builder rsocketRequesterBuilder, 27 | @Value("${test.rsocket.server.port:7000}") int port) { 28 | rsocketRequester = rsocketRequesterBuilder 29 | .dataMimeType(MimeType.valueOf("application/cbor")) 30 | .tcp("localhost", port); 31 | } 32 | 33 | @Test 34 | void response(RSocketMessageRegistry catalog) { 35 | MessageMapping response = MessageMapping.response("response") 36 | .response(new Foo("Server", "Response")); 37 | catalog.register(response); 38 | assertThat(rsocketRequester.route("response").data(new Foo("Client", "Request")) 39 | .retrieveMono(Foo.class).doOnNext(foo -> { 40 | System.err.println(foo); 41 | assertThat(foo.getOrigin()).isEqualTo("Server"); 42 | }).block()).isNotNull(); 43 | assertThat(response.drain()).hasSize(1); 44 | assertThat(response.drain()).hasSize(0); 45 | } 46 | 47 | @EnableAutoConfiguration 48 | @Configuration 49 | static class Application { 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /server/src/test/java/org/springframework/mock/rsocket/test/DynamicRouteTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.mock.rsocket.test; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.messaging.rsocket.RSocketRequester; 12 | import org.springframework.mock.rsocket.MessageMapping; 13 | import org.springframework.mock.rsocket.RSocketMessageCatalog; 14 | import org.springframework.mock.rsocket.RSocketMessageRegistry; 15 | import org.springframework.mock.rsocket.RSocketServerExtension; 16 | import org.springframework.mock.rsocket.server.Foo; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @SpringBootTest 21 | @ExtendWith(RSocketServerExtension.class) 22 | class DynamicRouteTests { 23 | 24 | private RSocketRequester rsocketRequester; 25 | 26 | public DynamicRouteTests(@Autowired RSocketRequester.Builder rsocketRequesterBuilder, 27 | @Value("${test.rsocket.server.port:7000}") int port) { 28 | rsocketRequester = rsocketRequesterBuilder.tcp("localhost", port); 29 | } 30 | 31 | @Test 32 | void inject(RSocketMessageCatalog catalog) { 33 | assertThat(catalog).isNotNull(); 34 | } 35 | 36 | @Test 37 | void response(RSocketMessageRegistry catalog) { 38 | MessageMapping response = MessageMapping.response("response") 39 | .response(new Foo("Server", "Response")); 40 | catalog.register(response); 41 | assertThat(rsocketRequester.route("response").data(new Foo("Client", "Request")) 42 | .retrieveMono(Foo.class).doOnNext(foo -> { 43 | System.err.println(foo); 44 | assertThat(foo.getOrigin()).isEqualTo("Server"); 45 | }).block()).isNotNull(); 46 | assertThat(response.drain()).hasSize(1); 47 | assertThat(response.drain()).hasSize(0); 48 | } 49 | 50 | @Test 51 | void handler(RSocketMessageRegistry catalog) { 52 | MessageMapping response = MessageMapping.response("handler") 53 | .handler(Foo.class, foo -> new Foo("Server", "Response")); 54 | catalog.register(response); 55 | assertThat(rsocketRequester.route("handler").data(new Foo("Client", "Request")) 56 | .retrieveMono(Foo.class).doOnNext(foo -> { 57 | System.err.println(foo); 58 | assertThat(foo.getOrigin()).isEqualTo("Server"); 59 | }).block()).isNotNull(); 60 | assertThat(response.drain(Foo.class)).hasSize(1); 61 | } 62 | 63 | @Test 64 | void stream(RSocketMessageRegistry catalog) { 65 | MessageMapping stream = MessageMapping.stream("dynamic") 66 | .handler(Foo.class, foo -> new Foo[] { new Foo("Server", "Stream") }); 67 | catalog.register(stream); 68 | assertThat(rsocketRequester.route("dynamic").data(new Foo("Client", "Request")) 69 | .retrieveFlux(Foo.class).take(3).doOnNext(foo -> { 70 | System.err.println(foo); 71 | assertThat(foo.getOrigin()).isEqualTo("Server"); 72 | }).count().block()).isEqualTo(1); 73 | assertThat(stream.drain()).hasSize(1); 74 | } 75 | 76 | @Test 77 | void multi(RSocketMessageRegistry catalog) { 78 | MessageMapping stream = MessageMapping.stream("other").response(new Foo[] { 79 | new Foo("Server", "Stream", 0), new Foo("Server", "Stream", 1) }); 80 | catalog.register(stream); 81 | assertThat(rsocketRequester.route("other").data(new Foo("Client", "Request")) 82 | .retrieveFlux(Foo.class).take(3).doOnNext(foo -> { 83 | System.err.println(foo); 84 | assertThat(foo.getOrigin()).isEqualTo("Server"); 85 | }).count().block()).isEqualTo(2); 86 | assertThat(stream.drain()).hasSize(1); 87 | } 88 | 89 | @EnableAutoConfiguration 90 | @Configuration 91 | static class Application { 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /server/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | #spring.cloud.function.expected-content-type=application/cbor -------------------------------------------------------------------------------- /server/src/test/resources/catalog/channel.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "channel", 3 | "frameType": "REQUEST_CHANNEL", 4 | "request": {}, 5 | "response": { 6 | "origin": "Server", 7 | "interaction": "Stream", 8 | "index": 0 9 | } 10 | } -------------------------------------------------------------------------------- /server/src/test/resources/catalog/forget.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "forget", 3 | "frameType": "REQUEST_FNF", 4 | "request": { 5 | "origin": "Client" 6 | } 7 | } -------------------------------------------------------------------------------- /server/src/test/resources/catalog/long.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "long", 3 | "frameType": "REQUEST_STREAM", 4 | "request": {}, 5 | "responses": [ 6 | { 7 | "origin": "Server", 8 | "interaction": "Stream", 9 | "index": 0 10 | }, 11 | { 12 | "origin": "Server", 13 | "interaction": "Stream", 14 | "index": 1 15 | }, 16 | { 17 | "origin": "Server", 18 | "interaction": "Stream", 19 | "index": 2 20 | } 21 | ], 22 | "repeat": 5 23 | } -------------------------------------------------------------------------------- /server/src/test/resources/catalog/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "response", 3 | "frameType": "REQUEST_RESPONSE", 4 | "request": { 5 | "origin": "Client" 6 | }, 7 | "response": { 8 | "origin": "Server", 9 | "interaction": "Response", 10 | "index": 0 11 | } 12 | } --------------------------------------------------------------------------------