├── .github └── dependabot.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE.txt ├── README.md ├── Servirtium Logo.graffle ├── Servirtium-Square.png ├── Servirtium.graffle ├── core ├── pom.xml └── src │ ├── .DS_Store │ ├── main │ ├── .DS_Store │ └── java │ │ └── com │ │ └── paulhammant │ │ └── servirtium │ │ ├── InteractionManipulations.java │ │ ├── InteractionMonitor.java │ │ ├── JsonAndXmlUtilities.java │ │ ├── MarkdownRecorder.java │ │ ├── MarkdownReplayer.java │ │ ├── NonRecordingPassThrough.java │ │ ├── ServiceInteropViaOkHttp.java │ │ ├── ServiceInteroperation.java │ │ ├── ServiceMonitor.java │ │ ├── ServiceResponse.java │ │ ├── ServirtiumServer.java │ │ ├── SimpleInteractionManipulations.java │ │ ├── logging │ │ └── Log4JServiceMonitor.java │ │ └── svn │ │ └── SubversionInteractionManipulations.java │ └── test │ ├── java │ └── com │ │ └── paulhammant │ │ └── servirtium │ │ ├── IsJsonEqualTest.java │ │ ├── MarkdownRecorderTest.java │ │ ├── MarkdownReplayerTest.java │ │ ├── SimpleGetCentricBinaryTests.java │ │ ├── SimpleGetCentricTextTests.java │ │ ├── SimplePostCentricTests.java │ │ └── UtilityTests.java │ └── resources │ ├── ExampleSubversionCheckoutRecording.md │ ├── TodobackendDotComServiceRecording.md │ ├── png-transparent.png │ └── test.json ├── docs └── SvnMerkleizer_More_Info.md ├── jetty ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── paulhammant │ │ └── servirtium │ │ └── jetty │ │ └── JettyServirtiumServer.java │ └── test │ └── java │ └── com │ └── paulhammant │ └── servirtium │ └── jetty │ ├── SimpleGetCentricBinaryWithJettyTests.java │ ├── SimpleGetCentricTextWithJettyTests.java │ ├── SimplePostCentricWithJettyTests.java │ ├── TodobackendDotComRecorderMain.java │ └── TodobackendDotComReplayerMain.java ├── mvnw ├── mvnw.cmd ├── pom.xml └── undertow ├── pom.xml └── src ├── main └── java │ └── com │ └── paulhammant │ └── servirtium │ └── undertow │ └── UndertowServirtiumServer.java └── test └── java └── com └── paulhammant └── servirtium └── undertow ├── SimpleGetCentricBinaryWithUndertowTests.java ├── SimpleGetCentricTextWithUndertowTests.java └── SimplePostCentricWithUndertowTests.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | *.idea/ 4 | 5 | .servirtium_tmp/ -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.5"; 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/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/.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.1/apache-maven-3.6.1-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Two clause BSD license - see top of each source file. 2 | 3 | https://opensource.org/licenses/BSD-2-Clause -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Servirtium 2 | 3 | ![](Servirtium-Square.png?raw=true) 4 | 5 | Servirtium == Service Virtualized HTTP (for Java) in a record/playback style, with plain 6 | Markdown recordings 7 | 8 | Utilization of "Service Virtualization" is best practice towards fast and 9 | consistent test automation. This tech should be used in conjunction with 10 | JUnit/TestNG, etc. Versus alternate technologies, Servirtium utilizes Markdown 11 | for recorded HTTP conversations, which aids readability allows for diffing 12 | to quickly determine if contracts are broken. That last is an important aspect 13 | when Service Virtualization is part of a **Technology Compatibility Kit** 14 | 15 | ## Design goals 16 | 17 | 1. By being a "man in the middle" it enables the recording of HTTP conversations and store them in Markdown under 18 | source-control co-located with the automated tests themselves. 19 | 2. In playback, Servirtium allows the functionality tested in the service tests to be isolated from potentially flaky 20 | and unquestionably slower "down stack" and external remote services. 21 | 3. A diffable format (regular Markdown files) to clearly show the differences between two recordings of the same 22 | conversation, that is co-located with test logic (no database of any sort) 23 | 4. Agnostic about other test frameworks: use JUnit 4, JUnit5, TestNG, Cucumber for Java, or JBehave. 24 | 5. No process spawning/killing orchestration. 25 | 6. One recording per test method, even if that means duplicate sections of markdown over many tests 26 | 7. No conditionals or flow control in the recording - no DSL at all. 27 | 8. Allowance for modification of recording or playback for simplification/redaction purposes. 28 | 9. For use **in the same process** as the test-runner. It is not designed to be a 29 | standalone server, although it can be used that way. 30 | 31 | ## Design Limitations 32 | 33 | 1. Just for Java teams presently (needs porting) 34 | 3. Not for playback use in "for humans" environments like QA or UAT 35 | 36 | ## What do recordings look like? 37 | 38 | ### Raw recording source (Markdown) 39 | 40 | Here's a shorted source form for a recorded conversation 41 | 42 | ![](https://user-images.githubusercontent.com/82182/66556432-21473c00-eb48-11e9-8fb3-06259d79ff2b.png) 43 | 44 | ### Rendered Markdown in the GitHub UI 45 | 46 | Best to see a real one here [in situ on GitHub](https://github.com/paul-hammant/climate-data-tck/blob/master/src/test/mocks/averageRainfallForEgyptFrom1980to1999Exists.md) 47 | rather than the minimal example above. You can see the rendering there (best for human eyes), but also a snap-shot of 48 | that here: 49 | 50 | ![](https://user-images.githubusercontent.com/82182/66568199-df76bf80-eb60-11e9-83a8-61be277a9fae.png) 51 | 52 | ### More info 53 | 54 | See [ExampleSubversionCheckoutRecording.md](https://github.com/paul-hammant/servirtium/blob/master/src/test/resources/ExampleSubversionCheckoutRecording.md) 55 | which was recorded from a real Subversion 'svn' command line client doing it's thing, but 56 | thru Servirtium as a HTTP-proxy. After the recording of that, the replay side of Servirtium was able 57 | to pretend to be Apache+Subversion for a fresh 'svn checkout' command. 58 | [This one](https://github.com/paul-hammant/servirtium/blob/master/src/test/java/com/paulhammant/servirtium/SubversionCheckoutRecorderMain.java) 59 | was the recorder, and [this one](https://github.com/paul-hammant/servirtium/blob/master/src/test/java/com/paulhammant/servirtium/SubversionCheckoutReplayerMain.java) 60 | the replayer for that recorded conversation. 61 | 62 | ## Implementation Limitations 63 | 64 | 1. Java only for now, though usable in the broader JVM ecosystem. Ports to other languages 65 | is a direction I'd like to go in. Perhaps a rewrite in Rust, and then bindings back to Java, C#, 66 | Python, Ruby and NodeJs would be a more sustainable route long term. 67 | 68 | 2. The recorder **isn't very good at handling parallel requests**. Most of the 69 | things you want to test will be serial (and short) but if your client is a browser, 70 | then you should half expect for parallelized operations. 71 | 72 | 3. Servirtium can't yet listen on over HTTPS. 73 | 74 | 4. Servirtium can't yet function as a HTTP Proxy server. It must be a "man in the middle", 75 | meaning you have to be able to override the endpoints of services during JUnit/TestNG invocation 76 | in order to be able to record them (and play them back). 77 | 78 | 5. Some server technologies (like Amazon S3) sign payloads in a way that breaks for middle-man 79 | deployments. See [S3](https://github.com/paul-hammant/servirtium/wiki/S3). 80 | 81 | # Notable examples of use 82 | 83 | ## SvnMerkleizer project - emulation of Subversion in tests 84 | 85 | [Read more about two seprate uses of Servirtium for this project](docs/SvnMerkleizer_More_Info.md) 86 | 87 | ## Climate API demo 88 | 89 | The World Bank's Climate Data service turned into a Java library with Servirtium tests: 90 | https://github.com/paul-hammant/climate-data-tck. Direct, record and playback modes of 91 | operation for the same tests. 92 | 93 | ## Todobackend record and playback 94 | 95 | [TodobackendDotComServiceRecording.md](https://github.com/paul-hammant/servirtium/blob/master/src/test/resources/TodobackendDotComServiceRecording.md) 96 | is a recording of the Mocha test site of "TodoBackend.com" against a real Ruby/Sinatra/Heroku 97 | endpoint. This is not an example of something you'd orchestrate in Java/JUnit, but it is 98 | an example of a sophisticated series of interactions over HTTP between a client (the browser) 99 | and that Heroku server. Indeed, the intent of the site is show that multiple backends should be 100 | compatible with that JavaScript/Browser test suite. 101 | 102 | [Here's the code for the recorder](https://github.com/paul-hammant/servirtium/blob/master/src/test/java/com/paulhammant/servirtium/SubversionCheckoutRecorderMain.java) 103 | of that, and [here's the code for the replayer](https://github.com/paul-hammant/servirtium/blob/master/src/test/java/com/paulhammant/servirtium/SubversionCheckoutReplayerMain.java) 104 | for that. 105 | 106 | Note: playback does not pass all the tests because there's a randomized GUID in the request 107 | payload that changes every time you run the test suite. It gets one third of the way through though. 108 | 109 | **Note: this limitation is being resolved, presently** 110 | 111 | ## Readiness for general industry by lovers of test automation? 112 | 113 | A pre 1.0 release is used by a startup Paul is involved with for multiple unrelated external services. 114 | 115 | ## Servirtium's default listening port 116 | 117 | As per [the default port calculator](https://paul-hammant.github.io/default-port-calculator/#servirtium) for 'servirtium': 61417 118 | 119 | # Further Wiki Documentation 120 | 121 | [Servirtium in Technology Compatibility Kits](../../wiki/Servirtium-in-Technology-Compatibility-Kits) 122 | [Adding-notes-to-a-recording](../../wiki/Adding-notes-to-a-recording) 123 | 124 | # Building Servirtium 125 | 126 | This builds the binaries, but skips integration tests as they rely on Wikipedia, Reddit 127 | and others which are moving targets sometimes. 128 | 129 | ``` 130 | mvn clean install 131 | ``` 132 | 133 | This builds the binaries, and includes integration tests (that use various services on the web) 134 | 135 | ``` 136 | mvn clean install -Ptests 137 | ``` 138 | 139 | ## License 140 | 141 | BSD 2-Clause license (open source). Refer to [LICENSE.txt](/paul-hammant/servirtium/blob/master/LICENSE.txt) 142 | 143 | ## Legal warning 144 | 145 | Be careful: your contracts and EULAs with service providers 146 | (as well as application/server makers for on-premises) might not allow you to 147 | reverse engineer their over-the-wire APIs. 148 | 149 | A real case: [Reverse engineering of competitor’s software cost company big](http://blog.internetcases.com/2017/10/24/reverse-engineering-of-competitors-software-cost-company-big/) - and you might say that such clauses are needed to prevent licensees from competing with the original company with arguably "stolen" IP. 150 | 151 | We (developers and test engineers) might morally think that we should be OK for this, as we're just doing it for 152 | test-automation purposes. No matter, the contracts that are signed often make no such distinction, but 153 | the case above was where the original maker of an API went after a company that was trying to make 154 | something for the same ecosystem without a commercial relation on that specifically. 155 | 156 | ## Code of Conduct 157 | 158 | Be Nice (in lieu of something longer) -------------------------------------------------------------------------------- /Servirtium Logo.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/Servirtium Logo.graffle -------------------------------------------------------------------------------- /Servirtium-Square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/Servirtium-Square.png -------------------------------------------------------------------------------- /Servirtium.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/Servirtium.graffle -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | 7 | com.paulhammant.servirtium 8 | servirtium-pom 9 | 0.9.10-SNAPSHOT 10 | 11 | 12 | com.paulhammant 13 | servirtium-core 14 | jar 15 | 16 | 17 | 18 | 19 | com.squareup.okhttp3 20 | okhttp 21 | 3.14.9 22 | 23 | 24 | 25 | com.github.javadev 26 | underscore 27 | 1.82 28 | 29 | 30 | 31 | org.hamcrest 32 | hamcrest-all 33 | 1.3 34 | 35 | 36 | 37 | 38 | 39 | org.apache.logging.log4j 40 | log4j-api 41 | 2.19.0 42 | true 43 | 44 | 45 | 46 | org.apache.logging.log4j 47 | log4j-core 48 | 2.17.1 49 | true 50 | 51 | 52 | 53 | 54 | 55 | junit 56 | junit 57 | 4.13.2 58 | test 59 | 60 | 61 | 62 | io.rest-assured 63 | rest-assured 64 | 5.2.0 65 | test 66 | 67 | 68 | 69 | org.eclipse.jetty 70 | jetty-server 71 | 11.0.12 72 | test 73 | 74 | 75 | 76 | org.mockito 77 | mockito-core 78 | 4.8.0 79 | test 80 | 81 | 82 | 83 | org.mockito 84 | mockito-inline 85 | 4.8.1 86 | test 87 | 88 | 89 | 90 | org.json 91 | json 92 | 20220924 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-compiler-plugin 102 | 3.10.1 103 | 104 | 1.8 105 | 1.8 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-surefire-plugin 111 | 2.22.2 112 | 113 | 114 | all-tests 115 | 116 | test 117 | 118 | 119 | 120 | **/*Tests.java 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-jar-plugin 129 | 3.3.0 130 | 131 | 132 | 133 | test-jar 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /core/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/core/src/.DS_Store -------------------------------------------------------------------------------- /core/src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/core/src/main/.DS_Store -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/InteractionManipulations.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium; 33 | 34 | import java.util.List; 35 | 36 | public interface InteractionManipulations { 37 | 38 | default void changeSingleHeaderForRequestToRealService(String currentHeader, List clientRequestHeaders) { 39 | } 40 | 41 | default String headerValueManipulation(String hdrKey, String hdrVal) { 42 | return hdrVal; 43 | } 44 | 45 | default String changeUrlForRequestToRealService(String url) { 46 | return url; 47 | } 48 | 49 | default String changeSingleHeaderReturnedBackFromRealServiceForRecording(int ix, String headerBackFromService) { 50 | return headerBackFromService; 51 | } 52 | 53 | default void changeAnyHeadersReturnedBackFromRealServiceForRecording(List serviceResponseHeaders) { 54 | } 55 | 56 | /** 57 | * Change things in the body returned from the server before: a) making a recording, and b) playing back a recording. 58 | * If you're using the same InteractionManipulations instance for record and playback, there could be some 59 | * potentially double changing going on here, but you could view that as harmless. This is called before any 60 | * pretty printing happens. 61 | * 62 | * @param bodyFromService the string representation of the body returned from the server 63 | * @return the modified (or not) string representation of the body returned from the server 64 | */ 65 | default String changeBodyReturnedBackFromRealServiceForRecording(String bodyFromService) { 66 | return bodyFromService; 67 | } 68 | 69 | /** 70 | * Change things in the body returned from the server as it was recorded (and potentially changed 71 | * in changeBodyReturnedBackFromRealServiceForRecording() but before responding to the client. 72 | * This is called after any pretty printing happened (because that's in the recording too). 73 | * 74 | * @param bodyAsRecorded the string representation of the body as recorded. 75 | * @return the modified (or not) string representation of the body as recorded 76 | */ 77 | default String changeBodyForClientResponseAfterRecording(String bodyAsRecorded) { 78 | return bodyAsRecorded; 79 | } 80 | 81 | default String[] changeHeadersForClientResponseAfterRecording(String[] headers) { 82 | return headers; 83 | } 84 | 85 | default void changeAnyHeadersForRequestToRealService(List clientRequestHeaders) { 86 | } 87 | 88 | /** This may be Base84 encoded binary, but you're seldom going to want to change that */ 89 | default String changeBodyForRequestToRealService(String body) { 90 | return body; 91 | } 92 | 93 | class NullObject implements InteractionManipulations { 94 | 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/InteractionMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium; 33 | 34 | import java.io.IOException; 35 | import java.util.ArrayList; 36 | import java.util.List; 37 | 38 | public interface InteractionMonitor { 39 | 40 | default void finishedScript(int interactionNum, boolean failed) {} 41 | 42 | /** 43 | * Set the filename for the source of the conversation 44 | * @param filename the filename 45 | */ 46 | default void setScriptFilename(String filename) {} 47 | 48 | ServiceResponse getServiceResponseForRequest(String method, String url, 49 | Interaction interaction, 50 | boolean lowerCaseHeaders) throws IOException; 51 | 52 | Interaction newInteraction(int interactionNum, String context, String method, String path, String url); 53 | 54 | default void codeNoteForNextInteraction(String title, String multiline) {} 55 | 56 | default void noteForNextInteraction(String title, String multiline) {} 57 | 58 | abstract class Interaction { 59 | 60 | final int interactionNum; 61 | public final String context; 62 | List clientRequestHeaders; 63 | Object clientRequestBody; 64 | String clientRequestContentType; 65 | 66 | Interaction(int interactionNum, String context) { 67 | this.interactionNum = interactionNum; 68 | this.context = context; 69 | } 70 | 71 | public void complete() {} 72 | 73 | 74 | public abstract List noteClientRequestHeadersAndBody(InteractionManipulations interactionManipulations, 75 | List clientRequestHeaders, 76 | Object clientRequestBody, String clientRequestContentType, 77 | String method, boolean lowerCaseHeaders); 78 | 79 | protected void setClientRequestBodyAndContentType(Object clientRequestBody, String clientRequestContentType) { 80 | this.clientRequestBody = clientRequestBody; 81 | this.clientRequestContentType = clientRequestContentType; 82 | } 83 | 84 | protected List changeRequestHeadersIfNeeded(InteractionManipulations interactionManipulations, List clientRequestHeaders, String method, boolean lowerCaseHeaders) { 85 | List clientRequestHeaders2 = new ArrayList<>(); 86 | for (String s : clientRequestHeaders) { 87 | String hdrName = s.split(": ")[0]; 88 | String hdrVal = s.split(": ")[1]; 89 | hdrVal = interactionManipulations.headerValueManipulation(hdrName, hdrVal); 90 | final String fullHeader = (lowerCaseHeaders ? hdrName.toLowerCase() : hdrName) + ": " + hdrVal; 91 | clientRequestHeaders2.add(fullHeader); 92 | interactionManipulations.changeSingleHeaderForRequestToRealService(fullHeader, clientRequestHeaders2); 93 | } 94 | return clientRequestHeaders2; 95 | } 96 | 97 | public void debugOriginalServiceResponseHeaders(String... headers) {} 98 | 99 | public void debugOriginalServiceResponseBody(Object body, int statusCode, String contentType) {} 100 | 101 | public void debugClientsServiceResponseHeaders(String... headers) {} 102 | 103 | public void debugClientsServiceResponseBody(Object body, int statusCode, String contentType) {} 104 | 105 | public void noteServiceResponseHeaders(String... headers) {} 106 | 107 | public void noteServiceResponseBody(Object body, int statusCode, String contentType) {} 108 | 109 | public void noteChangedResourceForRequestToClient(String from, String to) {} 110 | } 111 | 112 | public class NullObject implements InteractionMonitor { 113 | @Override 114 | public void finishedScript(int interactionNum, boolean failed) { 115 | } 116 | 117 | @Override 118 | public void setScriptFilename(String filename) { 119 | } 120 | 121 | @Override 122 | public ServiceResponse getServiceResponseForRequest(String method, String url, Interaction interaction, boolean lowerCaseHeaders) throws IOException { 123 | return null; 124 | } 125 | 126 | @Override 127 | public Interaction newInteraction(int interactionNum, String context, String method, String path, String url) { 128 | return null; 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/JsonAndXmlUtilities.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import com.github.underscore.U; 4 | import org.hamcrest.BaseMatcher; 5 | import org.hamcrest.Description; 6 | import org.hamcrest.Factory; 7 | import org.hamcrest.Matcher; 8 | 9 | public class JsonAndXmlUtilities extends BaseMatcher { 10 | 11 | private final String expectedValue; 12 | 13 | public JsonAndXmlUtilities(String equalArg) { 14 | expectedValue = prettifyJson(equalArg); 15 | } 16 | 17 | @Override 18 | public boolean matches(Object actualValue) { 19 | if (actualValue == null) { 20 | return expectedValue == null; 21 | } 22 | 23 | String actual = prettifyJson((String) actualValue); 24 | final boolean equals = actual.equals(expectedValue); 25 | return equals; 26 | } 27 | 28 | @Override 29 | public void describeTo(Description description) { 30 | description.appendValue(expectedValue); 31 | } 32 | 33 | public static String prettifyJson(String doc) { 34 | try { 35 | return U.formatJson(doc); 36 | } catch (Exception e2) { 37 | throw new AssertionError("Underscore-Java didn't think that was JSON"); 38 | } 39 | } 40 | 41 | public static String prettifyDocOrNot(String doc) { 42 | if (doc == null | "".equals(doc)) { 43 | return doc; 44 | } 45 | char firstNonBlankChar = doc.trim().charAt(0); 46 | if (firstNonBlankChar == '{' || firstNonBlankChar == '[') { 47 | try { 48 | return U.formatJson(doc); 49 | } catch (Exception e) { 50 | } 51 | } 52 | if (firstNonBlankChar == '<') { 53 | try { 54 | return U.formatXml(doc); 55 | } catch (Exception e) { 56 | } 57 | } 58 | return doc; 59 | } 60 | 61 | @Factory 62 | public static Matcher jsonEqualTo(String operand) { 63 | return new JsonAndXmlUtilities(operand); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/MarkdownRecorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium; 33 | 34 | import java.io.FileNotFoundException; 35 | import java.io.FileOutputStream; 36 | import java.io.OutputStream; 37 | import java.io.PrintStream; 38 | import java.util.ArrayList; 39 | import java.util.Arrays; 40 | import java.util.Base64; 41 | import java.util.HashMap; 42 | import java.util.List; 43 | import java.util.Map; 44 | 45 | public class MarkdownRecorder implements InteractionMonitor { 46 | 47 | private final ServiceInteroperation serviceInteroperation; 48 | private final InteractionManipulations interactionManipulations; 49 | private PrintStream out; 50 | private Map interactions = new HashMap<>(); 51 | private Map> notes = new HashMap<>(); 52 | private Map replacements = new HashMap<>(); 53 | private boolean alphaSortHeaders; 54 | private boolean extraDebugOutput; 55 | 56 | public static class Note { 57 | String title; 58 | private final String multiline; 59 | String body; 60 | 61 | public Note(String title, String multiline) { 62 | this.title = title; 63 | this.multiline = multiline; 64 | } 65 | } 66 | 67 | public MarkdownRecorder(ServiceInteroperation serviceInteroperation, InteractionManipulations interactionManipulations) { 68 | this.serviceInteroperation = serviceInteroperation; 69 | this.interactionManipulations = interactionManipulations; 70 | } 71 | 72 | public MarkdownRecorder withAlphaSortingOfHeaders() { 73 | alphaSortHeaders = true; 74 | return this; 75 | } 76 | 77 | public MarkdownRecorder withExtraDebugOutput() { 78 | extraDebugOutput = true; 79 | return this; 80 | } 81 | 82 | public ServiceResponse getServiceResponseForRequest(String method, String url, 83 | Interaction interaction, boolean lowerCaseHeaders) { 84 | 85 | return serviceInteroperation.invokeServiceEndpoint(method, interaction.clientRequestBody, 86 | interaction.clientRequestContentType, url, ((RecordingInteraction) interaction).clientRequestHeaders, 87 | interactionManipulations, lowerCaseHeaders); 88 | } 89 | 90 | /** 91 | * In the recording, some things that will be recorded differently to 92 | * what was sent/received to/from the real. 93 | * Note for request headers: replacements will be tried once "as is" and a second after the 94 | * whole header has been dropped to lower case (if applicable) 95 | * Note for response headers: case is "as is" from the real service 96 | * @param regex - something that may be in the read data sent to/from the real. 97 | * @param replacement - something that will replace the above in the recording. 98 | * @return this 99 | */ 100 | public MarkdownRecorder withReplacementInRecording(String regex, String replacement) { 101 | replacements.put(regex, replacement); 102 | return this; 103 | } 104 | 105 | /** 106 | * In the recording, some things that will be recorded differently to 107 | * what was sent/received to/from the real. 108 | * @param terms - an even number of 'regex' and 'replacement' pairs. 109 | * @return this 110 | */ 111 | public MarkdownRecorder withReplacementsInRecording(String... terms) { 112 | final int i = terms.length / 2; 113 | for (int x = 0; x < i; x++) { 114 | withReplacementInRecording(terms[x*2], terms[(x*2)+1]); 115 | } 116 | return this; 117 | } 118 | 119 | public void codeNoteForNextInteraction(String title, String multiline) { 120 | noteForNextInteraction(title, "```\n" + multiline + "\n```"); 121 | } 122 | 123 | public void noteForNextInteraction(String title, String multiline) { 124 | 125 | int key = interactions.size() + 1; 126 | List n = notes.get(key); 127 | if (n == null) { 128 | n = new ArrayList(); 129 | notes.put(key, n); 130 | } 131 | n.add(new Note(title, multiline)); 132 | } 133 | 134 | public class RecordingInteraction extends Interaction { 135 | 136 | private StringBuilder recording = new StringBuilder(); 137 | 138 | RecordingInteraction(int interactionNumber, String context) { 139 | super(interactionNumber, context); 140 | } 141 | 142 | @Override 143 | public void complete() { 144 | MarkdownRecorder.this.addInteraction(this); 145 | } 146 | 147 | public List noteClientRequestHeadersAndBody(InteractionManipulations interactionManipulations, 148 | List clientRequestHeaders, Object clientRequestBody, 149 | String clientRequestContentType, String method, boolean lowerCaseHeaders) { 150 | 151 | if (clientRequestBody == null) { 152 | clientRequestBody = ""; 153 | } 154 | 155 | guardOut(); 156 | 157 | if (extraDebugOutput) { 158 | 159 | // Debug of original client headers 160 | 161 | blockStart("DEBUG: Request headers as received from client, WITHOUT ALPHA-SORT, REDACTIONS, ETC"); 162 | for (String s : clientRequestHeaders) { 163 | this.recording.append(s).append("\n"); 164 | } 165 | blockEnd(); 166 | } 167 | 168 | // Headers recorded for playback 169 | 170 | List clientRequestHeaders2 = changeRequestHeadersIfNeeded(interactionManipulations, clientRequestHeaders, method, lowerCaseHeaders); 171 | 172 | interactionManipulations.changeAnyHeadersForRequestToRealService(clientRequestHeaders2); 173 | 174 | final String[] headersToRecord = clientRequestHeaders2.toArray(new String[0]); 175 | 176 | if (alphaSortHeaders) { 177 | Arrays.sort(headersToRecord); 178 | } 179 | final List headersToRecord2 = new ArrayList<>(); 180 | for (String h : headersToRecord) { 181 | for (String replacementRegex : replacements.keySet()) { 182 | h = h.replaceAll(replacementRegex, replacements.get(replacementRegex)); 183 | } 184 | if (lowerCaseHeaders) { 185 | h = h.toLowerCase(); 186 | // Redo replacements for case change scenario 187 | for (String replacementRegex : replacements.keySet()) { 188 | h = h.replaceAll(replacementRegex, replacements.get(replacementRegex)); 189 | } 190 | // Redo case change in case of replacement above 191 | h = h.toLowerCase(); 192 | } 193 | headersToRecord2.add(h); 194 | } 195 | 196 | this.clientRequestHeaders = headersToRecord2; 197 | 198 | blockStart("Request headers recorded for playback"); 199 | for (String s : headersToRecord2) { 200 | this.recording.append(s).append("\n"); 201 | } 202 | blockEnd(); 203 | 204 | // Body 205 | 206 | if (extraDebugOutput) { 207 | blockStart("DEBUG: Request body as received from client (" + clientRequestContentType + "), WITHOUT REDACTIONS, ETC"); 208 | if (clientRequestBody instanceof String) { 209 | this.recording.append(clientRequestBody).append("\n"); 210 | 211 | } else { 212 | this.recording.append(objectToStringForRecording((byte[]) clientRequestBody)).append("\n"); 213 | } 214 | blockEnd(); 215 | } 216 | 217 | if (clientRequestBody instanceof String) { 218 | clientRequestBody = interactionManipulations.changeBodyForRequestToRealService((String) clientRequestBody); 219 | } 220 | 221 | super.setClientRequestBodyAndContentType(clientRequestBody, clientRequestContentType); 222 | 223 | String forRecording = null; 224 | if (clientRequestBody == null) { 225 | forRecording = ""; 226 | } else if (clientRequestBody instanceof String) { 227 | forRecording = (String) clientRequestBody; 228 | for (String redactionRegex : replacements.keySet()) { 229 | forRecording = forRecording.replaceAll(redactionRegex, replacements.get(redactionRegex)); 230 | } 231 | } else { 232 | forRecording = objectToStringForRecording((byte[]) clientRequestBody); 233 | } 234 | 235 | blockStart("Request body recorded for playback (" + clientRequestContentType + ")"); 236 | this.recording.append(forRecording).append("\n"); 237 | blockEnd(); 238 | 239 | return Arrays.asList(headersToRecord); 240 | } 241 | 242 | private String objectToStringForRecording(byte[] clientRequestBody) { 243 | return "//SERVIRTIUM+Base64: " + Base64.getEncoder() 244 | .encodeToString(clientRequestBody).replaceAll("(.{60})", "$1\n"); 245 | } 246 | 247 | private void blockStart(String s) { 248 | this.recording.append("### ").append(s).append(":\n") 249 | .append("\n") 250 | .append("```\n"); 251 | } 252 | 253 | private void blockEnd() { 254 | this.recording.append("```\n"); 255 | this.recording.append("\n"); 256 | } 257 | 258 | @Override 259 | public void debugOriginalServiceResponseHeaders(String[] headers) { 260 | if (extraDebugOutput) { 261 | doServiceResponseHeaders(headers, "DEBUG: Response headers from real service, unchanged"); 262 | } 263 | } 264 | 265 | @Override 266 | public void debugClientsServiceResponseHeaders(String[] headers) { 267 | if (extraDebugOutput) { 268 | doServiceResponseHeaders(headers, "DEBUG: Response Headers for client, possibly changed after recording"); 269 | } 270 | } 271 | 272 | @Override 273 | public void debugOriginalServiceResponseBody(Object serviceResponseBody, int statusCode, String serviceResponseContentType) { 274 | if (extraDebugOutput) { 275 | doServiceResponseBody(serviceResponseBody, statusCode, serviceResponseContentType, "DEBUG: Response body from real service, unchanged"); 276 | } 277 | } 278 | 279 | @Override 280 | public void debugClientsServiceResponseBody(Object serviceResponseBody, int statusCode, String serviceResponseContentType) { 281 | if (extraDebugOutput) { 282 | doServiceResponseBody(serviceResponseBody, statusCode, serviceResponseContentType, "DEBUG: Response body for client, possibly changed after recording"); 283 | } 284 | } 285 | 286 | @Override 287 | public void noteServiceResponseHeaders(String[] headers) { 288 | 289 | doServiceResponseHeaders(headers, "Response headers recorded for playback"); 290 | 291 | } 292 | 293 | @Override 294 | public void noteServiceResponseBody(Object serviceResponseBody, int statusCode, 295 | String serviceResponseContentType) { 296 | 297 | doServiceResponseBody(serviceResponseBody, statusCode, serviceResponseContentType, "Response body recorded for playback"); 298 | } 299 | 300 | @Override 301 | public void noteChangedResourceForRequestToClient(String from, String to) { 302 | if (extraDebugOutput) { 303 | blockStart("DEBUG Note: Resource changed for call to real server"); 304 | this.recording.append("From:").append(from).append("\n"); 305 | this.recording.append("To:").append(to).append("\n"); 306 | blockEnd(); 307 | } 308 | } 309 | 310 | private void doServiceResponseHeaders(String[] headers, String title) { 311 | 312 | guardOut(); 313 | 314 | blockStart(title); 315 | 316 | if (alphaSortHeaders) { 317 | Arrays.sort(headers); 318 | } 319 | 320 | for (String hdrLine : headers) { 321 | int ix = hdrLine.indexOf(": "); 322 | for (String next : replacements.keySet()) { 323 | hdrLine = hdrLine.replaceAll(next, replacements.get(next)); 324 | } 325 | String hdrKey = hdrLine.substring(0, ix); 326 | this.recording.append(hdrKey).append(": ") 327 | .append(interactionManipulations.headerValueManipulation(hdrKey, hdrLine.substring(ix + 2))) 328 | .append("\n"); 329 | } 330 | 331 | blockEnd(); 332 | 333 | } 334 | 335 | private void doServiceResponseBody(Object serviceResponseBody, int statusCode, String serviceResponseContentType, String title) { 336 | 337 | guardOut(); 338 | 339 | String xtra = ""; 340 | if (serviceResponseBody instanceof byte[]) { 341 | xtra = " - Base64 below"; 342 | } 343 | 344 | blockStart(title + " (" + statusCode + ": " + serviceResponseContentType + xtra + ")"); 345 | 346 | if (serviceResponseBody instanceof String) { 347 | for (String next : replacements.keySet()) { 348 | serviceResponseBody = ((String) serviceResponseBody).replaceAll(next, replacements.get(next)); 349 | } 350 | this.recording.append(serviceResponseBody).append("\n"); 351 | } else if (serviceResponseBody instanceof byte[]) { 352 | this.recording.append(Base64.getEncoder().encodeToString((byte[]) serviceResponseBody)).append("\n"); 353 | } else { 354 | throw new UnsupportedOperationException(); 355 | } 356 | 357 | blockEnd(); 358 | } 359 | 360 | } 361 | 362 | private void addInteraction(Interaction interaction) { 363 | this.interactions.put(interaction.interactionNum, ((RecordingInteraction) interaction).recording.toString()); 364 | } 365 | 366 | @Override 367 | public RecordingInteraction newInteraction(int interactionNum, String context, String method, String path, String url) { 368 | guardOut(); 369 | 370 | String pathWithReplacements = path; 371 | 372 | for (String replacementRegex : replacements.keySet()) { 373 | pathWithReplacements = pathWithReplacements.replaceAll(replacementRegex, replacements.get(replacementRegex)); 374 | } 375 | 376 | RecordingInteraction recordingInteraction = new RecordingInteraction(interactionNum, context); 377 | 378 | recordingInteraction.recording.append("## Interaction ") 379 | .append(interactionNum).append(": ").append(method) 380 | .append(" ").append(pathWithReplacements).append("\n\n"); 381 | 382 | return recordingInteraction; 383 | } 384 | 385 | private void guardOut() { 386 | if (out == null) { 387 | throw new AssertionError("Recording in progress, but no PrintStream set up for " + 388 | "the recording. See setScriptFilename(..)"); 389 | } 390 | } 391 | 392 | public void finishedScript(int interactionNum, boolean failed) { 393 | if (this.out != null) { 394 | int i = 0; 395 | while (this.interactions.size() >0) { 396 | 397 | String interaction = this.interactions.remove(i++); 398 | 399 | List n = notes.get(i); 400 | if (n != null) { 401 | StringBuilder sb = new StringBuilder(); 402 | for (Note note : n) { 403 | sb.append("## [Note] ").append(note.title).append(":\n") 404 | .append("\n") 405 | .append(note.multiline) 406 | .append("\n"); 407 | 408 | } 409 | interaction = interaction.replaceAll("### Request headers recorded for playback:", sb.toString() + "\n### Request headers recorded for playback:"); 410 | } 411 | 412 | this.out.print(interaction); 413 | } 414 | if (failed) { 415 | this.out.println("# Failure noted during recording.\n\nMeaning this recording may be shorter than intended. " + 416 | "That all depends on how the test was coded though."); 417 | } 418 | this.out.close(); 419 | this.out = null; 420 | } 421 | } 422 | 423 | public void setScriptFilename(String filename) { 424 | try { 425 | setOutputStream(filename, new FileOutputStream(filename)); 426 | } catch (FileNotFoundException e) { 427 | throw new UnsupportedOperationException("Can't write to " + filename + ". Does the directory exist?"); 428 | } 429 | } 430 | 431 | public void setOutputStream(String filename, OutputStream out) { 432 | if (out != null) { 433 | this.out = new PrintStream(out); 434 | } 435 | } 436 | 437 | } 438 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/NonRecordingPassThrough.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium; 33 | 34 | import java.util.Arrays; 35 | import java.util.List; 36 | 37 | public class NonRecordingPassThrough implements InteractionMonitor { 38 | 39 | private final ServiceInteroperation serviceInteroperation; 40 | private final InteractionManipulations interactionManipulations; 41 | private boolean alphaSortHeaders; 42 | 43 | public NonRecordingPassThrough(ServiceInteroperation serviceInteroperation, InteractionManipulations interactionManipulations) { 44 | this.serviceInteroperation = serviceInteroperation; 45 | this.interactionManipulations = interactionManipulations; 46 | } 47 | 48 | public NonRecordingPassThrough withAlphaSortingOfHeaders() { 49 | alphaSortHeaders = true; 50 | return this; 51 | } 52 | 53 | public ServiceResponse getServiceResponseForRequest(String method, String url, 54 | Interaction interaction, boolean lowerCaseHeaders) { 55 | return serviceInteroperation.invokeServiceEndpoint(method, 56 | interaction.clientRequestBody, 57 | interaction.clientRequestContentType, 58 | url, interaction.clientRequestHeaders, 59 | interactionManipulations, lowerCaseHeaders); 60 | } 61 | 62 | public class NonRecordingInteraction extends Interaction { 63 | 64 | NonRecordingInteraction(int interactionNumber, String context) { 65 | super(interactionNumber, context); 66 | } 67 | 68 | public List noteClientRequestHeadersAndBody(InteractionManipulations interactionManipulations, 69 | List clientRequestHeaders, Object clientRequestBody, 70 | String clientRequestContentType, String method, boolean lowerCaseHeaders) { 71 | 72 | if (clientRequestBody == null) { 73 | clientRequestBody = ""; 74 | } 75 | 76 | // Headers recorded for playback 77 | 78 | List clientRequestHeaders2 = changeRequestHeadersIfNeeded(interactionManipulations, clientRequestHeaders, method, lowerCaseHeaders); 79 | 80 | interactionManipulations.changeAnyHeadersForRequestToRealService(clientRequestHeaders2); 81 | 82 | this.clientRequestHeaders = clientRequestHeaders2; 83 | 84 | final String[] headersToRecord = clientRequestHeaders2.toArray(new String[0]); 85 | 86 | if (alphaSortHeaders) { 87 | Arrays.sort(headersToRecord); 88 | } 89 | 90 | if (clientRequestBody instanceof String) { 91 | clientRequestBody = interactionManipulations.changeBodyForRequestToRealService((String) clientRequestBody); 92 | } 93 | 94 | super.setClientRequestBodyAndContentType(clientRequestBody, clientRequestContentType); 95 | 96 | return Arrays.asList(headersToRecord); 97 | } 98 | 99 | } 100 | 101 | @Override 102 | public Interaction newInteraction(int interactionNum, String context, String method, String path, String url) { 103 | return new NonRecordingInteraction(interactionNum, context); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/ServiceInteropViaOkHttp.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium; 33 | 34 | import okhttp3.Headers; 35 | import okhttp3.MediaType; 36 | import okhttp3.OkHttpClient; 37 | import okhttp3.Request; 38 | import okhttp3.RequestBody; 39 | import okhttp3.Response; 40 | import okhttp3.ResponseBody; 41 | 42 | import java.io.IOException; 43 | import java.net.SocketTimeoutException; 44 | import java.util.ArrayList; 45 | import java.util.List; 46 | import java.util.concurrent.TimeUnit; 47 | 48 | import static com.paulhammant.servirtium.ServirtiumServer.isText; 49 | 50 | /** 51 | * Invoke remote HTTP services using Square's OkHttp library. 52 | */ 53 | public class ServiceInteropViaOkHttp implements ServiceInteroperation { 54 | 55 | private OkHttpClient okHttpClient; 56 | private int readTimeout = 10; // secs 57 | private int writeTimeout = 10; // secs 58 | private int connectionTimeout = 10; // secs 59 | 60 | /** 61 | * Change client to have a write timeout that's no the default for OkHttp 62 | * @param writeTimeout in milliseconds 63 | * @return this 64 | */ 65 | public ServiceInteropViaOkHttp withWriteTimeout(int writeTimeout) { 66 | this.writeTimeout = writeTimeout; 67 | return this; 68 | } 69 | 70 | /** 71 | * Change client to have a read timeout that's no the default for OkHttp 72 | * @param readTimeout in milliseconds 73 | * @return this 74 | */ 75 | public ServiceInteropViaOkHttp withReadTimeout(int readTimeout) { 76 | this.readTimeout = readTimeout; 77 | return this; 78 | } 79 | 80 | /** 81 | * Change client to have a connection timeout that's no the default for OkHttp 82 | * @param connectionTimeout in milliseconds 83 | * @return this 84 | */ 85 | public ServiceInteropViaOkHttp withConnectionTimeout(int connectionTimeout) { 86 | this.connectionTimeout = connectionTimeout; 87 | return this; 88 | } 89 | 90 | private OkHttpClient makeOkHttpClient() { 91 | return new OkHttpClient.Builder() 92 | .readTimeout(readTimeout, TimeUnit.SECONDS) 93 | .writeTimeout(writeTimeout, TimeUnit.SECONDS) 94 | .connectTimeout(connectionTimeout, TimeUnit.SECONDS) 95 | .build(); 96 | } 97 | 98 | @Override 99 | public ServiceResponse invokeServiceEndpoint(String method, 100 | Object clientRequestBody, 101 | String clientRequestContentType, 102 | String url, List clientRequestHeaders, 103 | InteractionManipulations interactionManipulations, 104 | boolean forceHeadersToLowerCase) throws ServiceInteroperationFailed { 105 | 106 | if (okHttpClient == null) { 107 | okHttpClient = makeOkHttpClient(); 108 | } 109 | 110 | RequestBody nonGetBody = null; 111 | if (!method.equals("GET")) { 112 | MediaType mediaType = MediaType.parse(clientRequestContentType); 113 | if (clientRequestBody != null) { 114 | if (clientRequestBody instanceof String) { 115 | nonGetBody = RequestBody.create(mediaType, (String) clientRequestBody); 116 | } else { 117 | nonGetBody = RequestBody.create(mediaType, (byte[]) clientRequestBody); 118 | } 119 | } 120 | } 121 | 122 | Response response = null; 123 | try { 124 | Request.Builder reqBuilder = null; 125 | 126 | Headers.Builder hb = new Headers.Builder(); 127 | 128 | for (String h : clientRequestHeaders) { 129 | hb.add(h); 130 | } 131 | 132 | final Headers headerForOkHttp = hb.build(); 133 | 134 | if (method.equalsIgnoreCase("POST")) { 135 | reqBuilder = new Request.Builder().url(url).post(nonGetBody).headers(headerForOkHttp); 136 | } else { 137 | reqBuilder = new Request.Builder().url(url).method(method, nonGetBody).headers(headerForOkHttp); 138 | } 139 | 140 | try { 141 | response = okHttpClient.newCall(reqBuilder.build()).execute(); 142 | } catch (SocketTimeoutException e) { 143 | throw new ServiceInteroperationFailed("OkHttp " + method + " to " + url + " timed out. See ServiceInteropViaOkHttp.withReadTimeout(), withWriteTimeout(), and withConnectionTimeout()", e); 144 | } 145 | 146 | ResponseBody body = response.body(); 147 | Object responseBody = null; 148 | String contentType = null; 149 | if (body.contentType() == null) { 150 | contentType = ""; 151 | } else { 152 | contentType = body.contentType().toString(); 153 | if (contentType == null) { 154 | contentType = ""; 155 | } 156 | } 157 | if (isText(contentType)) { 158 | responseBody = body.string(); 159 | } else { 160 | responseBody = body.bytes(); 161 | } 162 | String responseContentType = response.header("Content-Type"); 163 | int statusCode = response.code(); 164 | String[] responseHeaders = response.headers().toString().split("\n"); 165 | ArrayList responseHeaders2 = new ArrayList<>(); 166 | for (String hdrLine : responseHeaders) { 167 | int ix = hdrLine.indexOf(": "); 168 | String hdrName = hdrLine.substring(0, ix); 169 | String hdrVal = hdrLine.substring(ix + 2); 170 | String hdrKey = forceHeadersToLowerCase ? hdrName.toLowerCase() : hdrName; // HTTP 2.0 says lower-case header keys. 171 | responseHeaders2.add(hdrKey + ": " + interactionManipulations.headerValueManipulation(hdrKey, hdrVal)); 172 | } 173 | final String[] headers = responseHeaders2.toArray(new String[responseHeaders.length]); 174 | return new ServiceResponse(responseBody, responseContentType, statusCode, headers); 175 | 176 | } catch (IOException e) { 177 | throw new ServiceInteroperationFailed("OkHttp " + method + " to " + url + " failed with an IOException", e); 178 | } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/ServiceInteroperation.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * HTTP client to invoke endpoints against remote services 7 | */ 8 | public interface ServiceInteroperation { 9 | 10 | /** 11 | * Invoke a specific endpoint on a remote service 12 | * 13 | * @param method GET, POST etc 14 | * @param clientRequestBody The body of the client request being sent to the remote service 15 | * @param clientRequestContentType 16 | * @param url The fully qualified URL for the request 17 | * @param clientRequestHeaders Headers to use with the request 18 | * @param interactionManipulations For changing the headers back from the remote service 19 | * @param forceHeadersToLowerCase Response headers should be forced to be lower case 20 | * @return the service response 21 | * @throws ServiceInteroperationFailed 22 | */ 23 | ServiceResponse invokeServiceEndpoint(String method, 24 | Object clientRequestBody, 25 | String clientRequestContentType, 26 | String url, List clientRequestHeaders, 27 | InteractionManipulations interactionManipulations, 28 | boolean forceHeadersToLowerCase) throws ServiceInteroperationFailed; 29 | 30 | class ServiceInteroperationFailed extends RuntimeException { 31 | public ServiceInteroperationFailed(String message, Throwable cause) { 32 | super(message, cause); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/ServiceMonitor.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.PrintStream; 6 | 7 | public interface ServiceMonitor { 8 | 9 | default void interactionStarted(int interactionNum, InteractionMonitor.Interaction interactionl){} 10 | 11 | default void interactionFinished(int interactionNum, String method, String url, String context) {} 12 | 13 | default void interactionFailed(int interactionNum, String method, String url, AssertionError assertionError, String context) {} 14 | 15 | default void unexpectedRequestError(Throwable throwable, String context) {} 16 | 17 | class Default implements ServiceMonitor { 18 | } 19 | 20 | class Console implements ServiceMonitor { 21 | 22 | @Override 23 | public void unexpectedRequestError(Throwable throwable, String context) { 24 | printShevrons(); 25 | System.out.println(">> Servirtium >> (context: " + context + ") unexpected request error: " + throwable.getMessage() + "\nStackTrace:\n" + stackTrace(throwable)); 26 | printShevrons(); } 27 | 28 | private void printShevrons() { 29 | System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); 30 | } 31 | 32 | @Override 33 | public void interactionFinished(int interactionNum, String method, String url, String context) { 34 | printShevrons(); 35 | System.out.println(">> Servirtium >> (context: " + context + ") interaction " + interactionNum + " " + method + " " + url + " FINISHED"); 36 | printShevrons(); 37 | } 38 | 39 | 40 | @Override 41 | public void interactionFailed(int interactionNum, String method, String url, AssertionError assertionError, String context) { 42 | printShevrons(); 43 | System.out.println(">> Servirtium >> (context: " + context + ") interaction " + interactionNum + " " + method + " " + url + " FAILED"); 44 | System.out.println(">> " + assertionError.getMessage()); 45 | printShevrons(); 46 | } 47 | 48 | } 49 | 50 | static String stackTrace(Throwable throwable) { 51 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 52 | 53 | String stack = ""; 54 | throwable.printStackTrace(new PrintStream(baos)); 55 | try { 56 | baos.close(); 57 | stack = baos.toString(); 58 | } catch (IOException e) { 59 | } 60 | return stack; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/ServiceResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium; 33 | 34 | public class ServiceResponse { 35 | 36 | public final String[] headers; 37 | public final Object body; 38 | public final String contentType; 39 | public final int statusCode; 40 | 41 | public ServiceResponse(Object body, String contentType, int statusCode, String... headers) { 42 | this.headers = headers; 43 | this.body = body; 44 | this.contentType = contentType; 45 | this.statusCode = statusCode; 46 | } 47 | 48 | public ServiceResponse withRevisedHeaders(String[] headers) { 49 | return new ServiceResponse(this.body, this.contentType, this.statusCode, headers); 50 | } 51 | public ServiceResponse withRevisedBody(String body) { 52 | for (int i = 0; i < headers.length; i++) { 53 | String header = headers[i]; 54 | if (header.startsWith("Content-Length")) { 55 | headers[i] = "Content-Length: " + body.length(); 56 | break; 57 | } 58 | if (header.startsWith("content-length")) { 59 | headers[i] = "content-length: " + body.length(); 60 | break; 61 | } 62 | } 63 | return new ServiceResponse(body, this.contentType, this.statusCode, this.headers); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/ServirtiumServer.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.FileOutputStream; 5 | 6 | public abstract class ServirtiumServer { 7 | 8 | protected final InteractionManipulations interactionManipulations; 9 | protected final InteractionMonitor interactionMonitor; 10 | 11 | private String context = "no context"; 12 | private boolean pretty; 13 | private int interactionNum = -1; 14 | private boolean lowerCaseHeaders; 15 | 16 | public ServirtiumServer(InteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor) { 17 | 18 | this.interactionManipulations = interactionManipulations; 19 | this.interactionMonitor = interactionMonitor; 20 | } 21 | 22 | public abstract ServirtiumServer start() throws Exception; 23 | public abstract void stop(); 24 | public abstract void finishedScript(); 25 | 26 | public abstract Throwable getLastException(); 27 | 28 | public static boolean isText(String contentType) { 29 | return contentType.startsWith("text/") || 30 | contentType.startsWith("image/svg") || 31 | contentType.startsWith("multipart/form-data") || 32 | contentType.startsWith("application/json") || 33 | contentType.startsWith("application/xml") || 34 | (contentType.startsWith("application/") && contentType.contains("script")) || 35 | contentType.startsWith("application/xhtml+xml"); 36 | } 37 | 38 | public void setContext(String context) { 39 | this.context = context; 40 | } 41 | 42 | public String getContext() { 43 | return context; 44 | } 45 | 46 | public final ServirtiumServer withPrettyPrintedTextBodies() { 47 | pretty = true; 48 | return this; 49 | } 50 | 51 | public final ServirtiumServer withLowerCaseHeaders() { 52 | lowerCaseHeaders = true; 53 | return this; 54 | } 55 | 56 | protected boolean useLowerCaseHeaders() { 57 | return lowerCaseHeaders; 58 | } 59 | 60 | public boolean shouldHavePrettyPrintedTextBodies() { 61 | return pretty; 62 | } 63 | 64 | protected void bumpInteractionNum() { 65 | interactionNum++; 66 | } 67 | 68 | protected int getInteractionNum() { 69 | return interactionNum; 70 | } 71 | 72 | protected void resetInteractionNumber() { 73 | interactionNum = -1; 74 | } 75 | 76 | // protected ArrayList changeContentLength(List newHeaders, String body) { 77 | // int len = -1; 78 | // ArrayList tmp = new ArrayList<>(); 79 | // for (String header : newHeaders) { 80 | // if (header.startsWith("Content-Type")) { 81 | // int csIx = header.indexOf("charset="); 82 | // if (csIx == -1) { 83 | // csIx = header.indexOf("CHARSET="); 84 | // } 85 | // if (csIx > -1) { 86 | // try { 87 | // final String substring = header.substring(csIx + 8); 88 | // len = new String(body.getBytes(substring)).length(); 89 | // } catch (UnsupportedEncodingException e) { 90 | // throw new UnsupportedOperationException(e); 91 | // } 92 | // } 93 | // } 94 | // } 95 | // if (len == -1) { 96 | // len = body.length(); 97 | // } 98 | // for (String header : newHeaders) { 99 | // if (header.startsWith("Content-Length")) { 100 | // tmp.add("Content-Length: " + len); 101 | // } else { 102 | // tmp.add(header); 103 | // } 104 | // } 105 | // return tmp; 106 | // } 107 | 108 | public static class NullObject extends ServirtiumServer { 109 | 110 | public NullObject() { 111 | super(new InteractionManipulations.NullObject(), new InteractionMonitor.NullObject()); 112 | } 113 | 114 | @Override 115 | public ServirtiumServer start() { 116 | return this; 117 | } 118 | 119 | 120 | @Override 121 | public void stop() { 122 | } 123 | 124 | @Override 125 | public void finishedScript() { 126 | } 127 | 128 | @Override 129 | public Throwable getLastException() { 130 | return null; 131 | } 132 | } 133 | 134 | public static String classAndTestName() { 135 | return classAndTestName(0); 136 | } 137 | 138 | public static String classAndTestName(int numRemovedFromCaller) { 139 | StackTraceElement[] stes = Thread.currentThread().getStackTrace(); 140 | int ix = 0; 141 | for (int j = 0; j < stes.length; j++) { 142 | StackTraceElement ste = stes[j]; 143 | if (!ste.getClassName().startsWith("sun.") 144 | && !ste.getClassName().startsWith("java") 145 | && !ste.getMethodName().equals("classAndTestName")) { 146 | if (ix++ == numRemovedFromCaller) { 147 | return ste.getClassName() + "." + ste.getMethodName(); 148 | } 149 | } 150 | } 151 | throw new UnsupportedOperationException("could net get method name"); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/SimpleInteractionManipulations.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import java.util.List; 4 | 5 | public class SimpleInteractionManipulations implements InteractionManipulations { 6 | 7 | protected final String fromUrl; 8 | protected final String toUrl; 9 | protected final String fromHost; 10 | protected final String toHost; 11 | private String[] headerPrefixesToRemoveFromRequest = new String[0]; 12 | private String[] headerPrefixesToRemoveFromResponse = new String[0]; 13 | 14 | public SimpleInteractionManipulations() { 15 | this("xx8suf98su98sf98sjxjcvlkxjcv" , "s89s8798s7df98sdf98sdf98sdf9"); 16 | } 17 | public SimpleInteractionManipulations(String fromUrl, String toUrl) { 18 | this.fromUrl = fromUrl; 19 | this.toUrl = toUrl; 20 | this.fromHost = fromUrl.replaceAll("https://","").replaceAll("http://",""); 21 | this.toHost = toUrl.replaceAll("https://","").replaceAll("http://",""); 22 | } 23 | 24 | public SimpleInteractionManipulations withHeaderPrefixesToRemoveFromServiceResponse(String... headerPrefixesToRemove) { 25 | this.headerPrefixesToRemoveFromResponse = headerPrefixesToRemove; 26 | return this; 27 | } 28 | 29 | public SimpleInteractionManipulations withHeaderPrefixesToRemoveFromClientRequest(String... headerPrefixesToRemove) { 30 | this.headerPrefixesToRemoveFromRequest = headerPrefixesToRemove; 31 | return this; 32 | } 33 | 34 | @Override 35 | public String changeUrlForRequestToRealService(String url) { 36 | return url.replace(fromUrl, toUrl); 37 | } 38 | 39 | @Override 40 | public void changeSingleHeaderForRequestToRealService(String currentHeader, List clientRequestHeaders) { 41 | String currentHeaderKey = null; 42 | String currentHeaderVal = null; 43 | currentHeaderKey = currentHeader.substring(0, currentHeader.indexOf(": ")); 44 | currentHeaderVal = currentHeader.substring(currentHeader.indexOf(": ") +2); 45 | 46 | for (String pfx : headerPrefixesToRemoveFromRequest) { 47 | if (currentHeader.startsWith(pfx)) { 48 | clientRequestHeaders.remove(currentHeader); 49 | } 50 | } 51 | 52 | if (currentHeader.startsWith("Host: ") || currentHeader.startsWith("host: ")) { 53 | for (int i = 0; i < clientRequestHeaders.size(); i++) { 54 | String h = clientRequestHeaders.get(i); 55 | if (h.startsWith("Host: ") || h.startsWith("host: ")) { 56 | clientRequestHeaders.remove(h); 57 | final String replace = h.replace(fromHost, toHost); 58 | clientRequestHeaders.add(i, replace); 59 | break; 60 | } 61 | } 62 | } 63 | } 64 | 65 | @Override 66 | public void changeAnyHeadersReturnedBackFromRealServiceForRecording(List serviceResponseHeaders) { 67 | String[] hdrs = serviceResponseHeaders.toArray(new String[0]); 68 | for (String hdr : hdrs) { 69 | for (String pfx : headerPrefixesToRemoveFromResponse) { 70 | if (hdr.startsWith(pfx)) { 71 | serviceResponseHeaders.remove(hdr); 72 | } 73 | } 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/logging/Log4JServiceMonitor.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.logging; 2 | 3 | import com.paulhammant.servirtium.ServiceMonitor; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | public class Log4JServiceMonitor implements ServiceMonitor { 8 | 9 | private static final Logger log = LogManager.getLogger(); 10 | 11 | @Override 12 | public void unexpectedRequestError(Throwable throwable, String context) { 13 | log.debug("(context: " + context + ") unexpected request error", throwable); 14 | } 15 | 16 | @Override 17 | public void interactionFinished(int interactionNum, String method, String url, String context) { 18 | log.debug("(context: " + context + ") interaction " + interactionNum + " " + method + " " + url + " FINISHED"); 19 | } 20 | 21 | @Override 22 | public void interactionFailed(int interactionNum, String method, String url, AssertionError assertionError, String context) { 23 | log.debug("(context: " + context + ") interaction " + interactionNum + " " + method + " " + url + " FAILED; assertionError:" + assertionError.getMessage()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/com/paulhammant/servirtium/svn/SubversionInteractionManipulations.java: -------------------------------------------------------------------------------- 1 | /* 2 | Servirtium: Service Virtualized HTTP 3 | 4 | Copyright (c) 2018, Paul Hammant 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of the Servirtium project. 30 | */ 31 | 32 | package com.paulhammant.servirtium.svn; 33 | 34 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 35 | 36 | import java.util.List; 37 | 38 | public class SubversionInteractionManipulations extends SimpleInteractionManipulations { 39 | 40 | public SubversionInteractionManipulations(String fromUrl, String toUrl) { 41 | super(fromUrl, toUrl); 42 | } 43 | 44 | @Override 45 | public void changeSingleHeaderForRequestToRealService(String currentHeader, List clientRequestHeaders) { 46 | if (currentHeader.startsWith("User-Agent:")) { 47 | for (int i = 0; i < clientRequestHeaders.size(); i++) { 48 | String s = clientRequestHeaders.get(i); 49 | if (s.startsWith("User-Agent:")) { 50 | clientRequestHeaders.remove(s); 51 | clientRequestHeaders.add(i, "User-Agent: " + getUserAgentString()); 52 | break; 53 | } 54 | } 55 | } 56 | 57 | super.changeSingleHeaderForRequestToRealService(currentHeader, clientRequestHeaders); 58 | } 59 | 60 | protected String getUserAgentString() { 61 | return "SVN/1.10.0 (x86_64-apple-darwin17.0.0) serf/1.3.9"; 62 | } 63 | 64 | @Override 65 | public String headerValueManipulation(String hdrKey, String serviceResponseHeaders) { 66 | return serviceResponseHeaders 67 | //.replace(from, to) 68 | .replaceAll("SVN-Repository-UUID: ([a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12})", "SVN-Repository-UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") 69 | .replaceAll("Date: ((Mon|Tue|Wed|Thu|Fri|Sat|Sun), [0-9]{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} GMT)", "Date: Tue, 01 Jan 2018 01:02:03 GMT"); 70 | } 71 | 72 | @Override 73 | public String changeSingleHeaderReturnedBackFromRealServiceForRecording(int ix, String headerBackFromService) { 74 | if (headerBackFromService.startsWith("DAV:")) { 75 | headerBackFromService = "DAV:" + spaces(ix) + headerBackFromService.substring(4); 76 | } 77 | return headerBackFromService; 78 | } 79 | 80 | static String spaces(int i) { 81 | String rv = ""; 82 | for (int ix = 0; ix < i; ix++) { 83 | rv = rv + " "; 84 | } 85 | return rv; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /core/src/test/java/com/paulhammant/servirtium/IsJsonEqualTest.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import org.junit.Test; 4 | 5 | import static com.paulhammant.servirtium.JsonAndXmlUtilities.jsonEqualTo; 6 | import static org.hamcrest.CoreMatchers.not; 7 | import static org.junit.Assert.*; 8 | 9 | public class IsJsonEqualTest { 10 | 11 | @Test 12 | public void testJsonEqualTo() { 13 | 14 | String json = "{ \"a\": 123}"; 15 | 16 | assertThat(json, jsonEqualTo("{\"a\":\n\n 123 } ")); 17 | assertThat(json, not(jsonEqualTo("{\"a\":\n\n 2384728374927349827349 } "))); 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /core/src/test/java/com/paulhammant/servirtium/MarkdownRecorderTest.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.util.List; 7 | 8 | import static java.util.Arrays.asList; 9 | import static junit.framework.TestCase.assertEquals; 10 | import static org.mockito.Mockito.any; 11 | import static org.mockito.Mockito.eq; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.spy; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.verifyNoMoreInteractions; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class MarkdownRecorderTest { 19 | 20 | @Test 21 | public void canRecordASimpleScript() { 22 | final InteractionManipulations im = mock(InteractionManipulations.class); 23 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 24 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 25 | when(im.headerValueManipulation("ZZZZ", "ZZ")).thenReturn("Z-Z"); 26 | when(im.headerValueManipulation("REQ_HEADER_KEY", "VAL")).thenReturn("V-A-L"); 27 | when(im.changeBodyForRequestToRealService("REQ_BODY")).thenReturn("R-E-Q__B-O-D-Y"); 28 | when(im.headerValueManipulation("RSP_HEADER_KEY", "RSP_VAL")).thenReturn("R-S-P__V-A-L"); 29 | 30 | MarkdownRecorder mr = new MarkdownRecorder(si, im); 31 | mr.setOutputStream("foo", out); 32 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 33 | i.noteClientRequestHeadersAndBody(im, asList("ZZZZ: ZZ", "REQ_HEADER_KEY: VAL"), 34 | "REQ_BODY", "text/plain", "FOO", true); 35 | i.noteServiceResponseHeaders("RSP_HEADER_KEY: RSP_VAL"); 36 | i.noteServiceResponseBody("RSP_BODY", 200, "text/plain"); 37 | i.complete(); 38 | mr.finishedScript(0, false); 39 | 40 | verify(im).headerValueManipulation("ZZZZ", "ZZ"); 41 | verify(im).headerValueManipulation("REQ_HEADER_KEY", "VAL"); 42 | verify(im).changeSingleHeaderForRequestToRealService(eq("req_header_key: V-A-L"), any(List.class)); 43 | verify(im).changeSingleHeaderForRequestToRealService(eq("zzzz: Z-Z"), any(List.class)); 44 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 45 | verify(im).changeBodyForRequestToRealService("REQ_BODY"); 46 | verify(im).headerValueManipulation("RSP_HEADER_KEY", "RSP_VAL"); 47 | verifyNoMoreInteractions(im, si); 48 | 49 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 50 | "\n" + 51 | "### Request headers recorded for playback:\n" + 52 | "\n" + 53 | "```\n" + 54 | "zzzz: z-z\n" + 55 | "req_header_key: v-a-l\n" + 56 | "```\n" + 57 | "\n" + 58 | "### Request body recorded for playback (text/plain):\n" + 59 | "\n" + 60 | "```\n" + 61 | "R-E-Q__B-O-D-Y\n" + 62 | "```\n" + 63 | "\n" + 64 | "### Response headers recorded for playback:\n" + 65 | "\n" + 66 | "```\n" + 67 | "RSP_HEADER_KEY: R-S-P__V-A-L\n" + 68 | "```\n" + 69 | "\n" + 70 | "### Response body recorded for playback (200: text/plain):\n" + 71 | "\n" + 72 | "```\n" + 73 | "RSP_BODY\n" + 74 | "```\n\n", out.toString()); 75 | } 76 | 77 | @Test 78 | public void canRecordASimpleScriptWithQueryString() { 79 | final InteractionManipulations im = mock(InteractionManipulations.class); 80 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 81 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 82 | 83 | when(im.changeBodyForRequestToRealService("REQ_BODY")).thenReturn("REQ_BODY"); 84 | 85 | MarkdownRecorder mr = new MarkdownRecorder(si, im); 86 | mr.setOutputStream("foo", out); 87 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c?password=hardyHarHar", "http://foo.com/bar?password=hardyHarHar"); 88 | i.noteClientRequestHeadersAndBody(im, asList(), 89 | "REQ_BODY", "text/plain", "FOO", true); 90 | i.noteServiceResponseHeaders(); 91 | i.noteServiceResponseBody("RSP_BODY", 200, "text/plain"); 92 | i.complete(); 93 | mr.finishedScript(0, false); 94 | 95 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 96 | verify(im).changeBodyForRequestToRealService("REQ_BODY"); 97 | verifyNoMoreInteractions(im, si); 98 | 99 | assertEquals("## Interaction 0: FOO /a/b/c?password=hardyHarHar\n" + 100 | "\n" + 101 | "### Request headers recorded for playback:\n" + 102 | "\n" + 103 | "```\n" + 104 | "```\n" + 105 | "\n" + 106 | "### Request body recorded for playback (text/plain):\n" + 107 | "\n" + 108 | "```\n" + 109 | "REQ_BODY\n" + 110 | "```\n" + 111 | "\n" + 112 | "### Response headers recorded for playback:\n" + 113 | "\n" + 114 | "```\n" + 115 | "```\n" + 116 | "\n" + 117 | "### Response body recorded for playback (200: text/plain):\n" + 118 | "\n" + 119 | "```\n" + 120 | "RSP_BODY\n" + 121 | "```\n\n", out.toString()); 122 | } 123 | 124 | @Test 125 | public void canRecordASimpleScriptWithQueryStringAndRedactPartOfTheURL() { 126 | final SimpleInteractionManipulations im = new SimpleInteractionManipulations(); 127 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 128 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 129 | 130 | MarkdownRecorder mr = new MarkdownRecorder(si, im) 131 | .withReplacementsInRecording("hardyHarHar", "pAsSwOrD-rEdAcTeD"); 132 | mr.setOutputStream("foo", out); 133 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c?password=hardyHarHar", "http://foo.com/bar?password=hardyHarHar"); 134 | i.noteClientRequestHeadersAndBody(im, asList(), 135 | "REQ_BODY", "text/plain", "FOO", true); 136 | i.noteServiceResponseHeaders(); 137 | i.noteServiceResponseBody("RSP_BODY", 200, "text/plain"); 138 | i.complete(); 139 | mr.finishedScript(0, false); 140 | 141 | verifyNoMoreInteractions(si); 142 | 143 | assertEquals("## Interaction 0: FOO /a/b/c?password=pAsSwOrD-rEdAcTeD\n" + 144 | "\n" + 145 | "### Request headers recorded for playback:\n" + 146 | "\n" + 147 | "```\n" + 148 | "```\n" + 149 | "\n" + 150 | "### Request body recorded for playback (text/plain):\n" + 151 | "\n" + 152 | "```\n" + 153 | "REQ_BODY\n" + 154 | "```\n" + 155 | "\n" + 156 | "### Response headers recorded for playback:\n" + 157 | "\n" + 158 | "```\n" + 159 | "```\n" + 160 | "\n" + 161 | "### Response body recorded for playback (200: text/plain):\n" + 162 | "\n" + 163 | "```\n" + 164 | "RSP_BODY\n" + 165 | "```\n\n", out.toString()); 166 | } 167 | 168 | 169 | @Test 170 | public void canRecordASimpleScriptWithDebugging() { 171 | final InteractionManipulations im = mock(InteractionManipulations.class); 172 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 173 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 174 | when(im.headerValueManipulation("ZZZZ", "ZZ")).thenReturn("Z-Z"); 175 | when(im.headerValueManipulation("REQ_HEADER_KEY", "VAL")).thenReturn("V-A-L"); 176 | when(im.changeBodyForRequestToRealService("REQ_BODY")).thenReturn("R-E-Q__B-O-D-Y"); 177 | when(im.headerValueManipulation("RSP_HEADER_KEY", "RSP_VAL")).thenReturn("R-S-P__V-A-L"); 178 | 179 | MarkdownRecorder mr = new MarkdownRecorder(si, im) 180 | .withExtraDebugOutput(); 181 | mr.setOutputStream("foo", out); 182 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 183 | i.noteClientRequestHeadersAndBody(im, asList("ZZZZ: ZZ", "REQ_HEADER_KEY: VAL"), 184 | "REQ_BODY", "text/plain", "FOO", true); 185 | i.noteServiceResponseHeaders("RSP_HEADER_KEY: RSP_VAL"); 186 | i.noteServiceResponseBody("RSP_BODY", 200, "text/plain"); 187 | i.complete(); 188 | mr.finishedScript(0, false); 189 | 190 | verify(im).headerValueManipulation("ZZZZ", "ZZ"); 191 | verify(im).headerValueManipulation("REQ_HEADER_KEY", "VAL"); 192 | verify(im).changeSingleHeaderForRequestToRealService(eq("req_header_key: V-A-L"), any(List.class)); 193 | verify(im).changeSingleHeaderForRequestToRealService(eq("zzzz: Z-Z"), any(List.class)); 194 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 195 | verify(im).changeBodyForRequestToRealService("REQ_BODY"); 196 | verify(im).headerValueManipulation("RSP_HEADER_KEY", "RSP_VAL"); 197 | verifyNoMoreInteractions(im, si); 198 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 199 | "\n" + 200 | "### DEBUG: Request headers as received from client, WITHOUT ALPHA-SORT, REDACTIONS, ETC:\n" + 201 | "\n" + 202 | "```\n" + 203 | "ZZZZ: ZZ\n" + 204 | "REQ_HEADER_KEY: VAL\n" + 205 | "```\n" + 206 | "\n" + 207 | "### Request headers recorded for playback:\n" + 208 | "\n" + 209 | "```\n" + 210 | "zzzz: z-z\n" + 211 | "req_header_key: v-a-l\n" + 212 | "```\n" + 213 | "\n" + 214 | "### DEBUG: Request body as received from client (text/plain), WITHOUT REDACTIONS, ETC:\n" + 215 | "\n" + 216 | "```\n" + 217 | "REQ_BODY\n" + 218 | "```\n" + 219 | "\n" + 220 | "### Request body recorded for playback (text/plain):\n" + 221 | "\n" + 222 | "```\n" + 223 | "R-E-Q__B-O-D-Y\n" + 224 | "```\n" + 225 | "\n" + 226 | "### Response headers recorded for playback:\n" + 227 | "\n" + 228 | "```\n" + 229 | "RSP_HEADER_KEY: R-S-P__V-A-L\n" + 230 | "```\n" + 231 | "\n" + 232 | "### Response body recorded for playback (200: text/plain):\n" + 233 | "\n" + 234 | "```\n" + 235 | "RSP_BODY\n" + 236 | "```\n" + 237 | "\n", out.toString()); 238 | } 239 | 240 | @Test 241 | public void canRecordASimpleScriptAndAlphaSortHeaders() { 242 | final InteractionManipulations im = mock(InteractionManipulations.class); 243 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 244 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 245 | when(im.headerValueManipulation("ZZZZ", "ZZ")).thenReturn("Z-Z"); 246 | when(im.headerValueManipulation("REQ_HEADER_KEY", "VAL")).thenReturn("V-A-L"); 247 | when(im.changeBodyForRequestToRealService("REQ_BODY")).thenReturn("R-E-Q__B-O-D-Y"); 248 | when(im.headerValueManipulation("RSP_HEADER_KEY", "RSP_VAL")).thenReturn("R-S-P__V-A-L"); 249 | 250 | MarkdownRecorder mr = new MarkdownRecorder(si, im) 251 | .withAlphaSortingOfHeaders(); 252 | mr.setOutputStream("foo", out); 253 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 254 | i.noteClientRequestHeadersAndBody(im, asList("ZZZZ: ZZ", "REQ_HEADER_KEY: VAL"), 255 | "REQ_BODY", "text/plain", "FOO", true); 256 | i.noteServiceResponseHeaders("RSP_HEADER_KEY: RSP_VAL"); 257 | i.noteServiceResponseBody("RSP_BODY", 200, "text/plain"); 258 | i.complete(); 259 | mr.finishedScript(0, false); 260 | 261 | verify(im).headerValueManipulation("ZZZZ", "ZZ"); 262 | verify(im).headerValueManipulation("REQ_HEADER_KEY", "VAL"); 263 | verify(im).changeSingleHeaderForRequestToRealService(eq("req_header_key: V-A-L"), any(List.class)); 264 | verify(im).changeSingleHeaderForRequestToRealService(eq("zzzz: Z-Z"), any(List.class)); 265 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 266 | verify(im).changeBodyForRequestToRealService("REQ_BODY"); 267 | verify(im).headerValueManipulation("RSP_HEADER_KEY", "RSP_VAL"); 268 | verifyNoMoreInteractions(im, si); 269 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 270 | "\n" + 271 | "### Request headers recorded for playback:\n" + 272 | "\n" + 273 | "```\n" + 274 | "req_header_key: v-a-l\n" + 275 | "zzzz: z-z\n" + 276 | "```\n" + 277 | "\n" + 278 | "### Request body recorded for playback (text/plain):\n" + 279 | "\n" + 280 | "```\n" + 281 | "R-E-Q__B-O-D-Y\n" + 282 | "```\n" + 283 | "\n" + 284 | "### Response headers recorded for playback:\n" + 285 | "\n" + 286 | "```\n" + 287 | "RSP_HEADER_KEY: R-S-P__V-A-L\n" + 288 | "```\n" + 289 | "\n" + 290 | "### Response body recorded for playback (200: text/plain):\n" + 291 | "\n" + 292 | "```\n" + 293 | "RSP_BODY\n" + 294 | "```\n" + 295 | "\n", out.toString()); 296 | } 297 | 298 | @Test 299 | public void canRecordASimpleScriptWithNotes() { 300 | final InteractionManipulations im = mock(InteractionManipulations.class); 301 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 302 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 303 | when(im.changeBodyForRequestToRealService("REQ_BODY")).thenReturn("R-E-Q__B-O-D-Y"); 304 | 305 | MarkdownRecorder mr = new MarkdownRecorder(si, im); 306 | mr.setOutputStream("foo", out); 307 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 308 | mr.noteForNextInteraction("Mary", "... Had a Little Lamb"); 309 | i.noteClientRequestHeadersAndBody(im, asList(), 310 | "REQ_BODY", "text/plain", "FOO", true); 311 | i.complete(); 312 | mr.finishedScript(0, false); 313 | 314 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 315 | verify(im).changeBodyForRequestToRealService("REQ_BODY"); 316 | verifyNoMoreInteractions(im, si); 317 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 318 | "\n" + 319 | "## [Note] Mary:\n" + 320 | "\n" + 321 | "... Had a Little Lamb\n" + 322 | "\n" + 323 | "### Request headers recorded for playback:\n" + 324 | "\n" + 325 | "```\n" + 326 | "```\n" + 327 | "\n" + 328 | "### Request body recorded for playback (text/plain):\n" + 329 | "\n" + 330 | "```\n" + 331 | "R-E-Q__B-O-D-Y\n" + 332 | "```\n" + 333 | "\n", out.toString()); 334 | } 335 | 336 | @Test 337 | public void canRecordASimpleScriptWithCodeNotes() { 338 | final InteractionManipulations im = mock(InteractionManipulations.class); 339 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 340 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 341 | when(im.changeBodyForRequestToRealService("REQ_BODY")).thenReturn("R-E-Q__B-O-D-Y"); 342 | 343 | MarkdownRecorder mr = new MarkdownRecorder(si, im); 344 | mr.setOutputStream("foo", out); 345 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 346 | mr.codeNoteForNextInteraction("CodeNotes", "111\n222"); 347 | i.noteClientRequestHeadersAndBody(im, asList(), 348 | "REQ_BODY", "text/plain", "FOO", true); 349 | i.complete(); 350 | mr.finishedScript(0, false); 351 | 352 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 353 | verify(im).changeBodyForRequestToRealService("REQ_BODY"); 354 | verifyNoMoreInteractions(im, si); 355 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 356 | "\n" + 357 | "## [Note] CodeNotes:\n" + 358 | "\n" + 359 | "```\n" + 360 | "111\n" + 361 | "222\n" + 362 | "```\n" + 363 | "\n" + 364 | "### Request headers recorded for playback:\n" + 365 | "\n" + 366 | "```\n" + 367 | "```\n" + 368 | "\n" + 369 | "### Request body recorded for playback (text/plain):\n" + 370 | "\n" + 371 | "```\n" + 372 | "R-E-Q__B-O-D-Y\n" + 373 | "```\n" + 374 | "\n", out.toString()); 375 | } 376 | 377 | @Test 378 | public void canPerformBodyReplacementsInRecording() { 379 | final InteractionManipulations im = mock(InteractionManipulations.class); 380 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 381 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 382 | when(im.changeBodyForRequestToRealService("Mary had a little lamb")).thenReturn("Mary had a little lamb"); 383 | 384 | MarkdownRecorder mr = new MarkdownRecorder(si, im).withReplacementsInRecording("little", "tiny", "lamb", "piglet"); 385 | mr.setOutputStream("foo", out); 386 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 387 | i.noteClientRequestHeadersAndBody(im, asList(), 388 | "Mary had a little lamb", "text/plain", "FOO", true); 389 | i.noteServiceResponseHeaders(); 390 | i.noteServiceResponseBody("A little lamb had Mary", 200, "text/plain"); 391 | i.complete(); 392 | mr.finishedScript(0, false); 393 | 394 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 395 | verify(im).changeBodyForRequestToRealService("Mary had a little lamb"); 396 | verifyNoMoreInteractions(im, si); 397 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 398 | "\n" + 399 | "### Request headers recorded for playback:\n" + 400 | "\n" + 401 | "```\n" + 402 | "```\n" + 403 | "\n" + 404 | "### Request body recorded for playback (text/plain):\n" + 405 | "\n" + 406 | "```\n" + 407 | "Mary had a tiny piglet\n" + 408 | "```\n" + 409 | "\n" + 410 | "### Response headers recorded for playback:\n" + 411 | "\n" + 412 | "```\n" + 413 | "```\n" + 414 | "\n" + 415 | "### Response body recorded for playback (200: text/plain):\n" + 416 | "\n" + 417 | "```\n" + 418 | "A tiny piglet had Mary\n" + 419 | "```\n\n", out.toString()); 420 | } 421 | 422 | @Test 423 | public void canPerformHeaderReplacementsInRecording() { 424 | final InteractionManipulations im = mock(InteractionManipulations.class); 425 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 426 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 427 | 428 | when(im.headerValueManipulation("Mary", "had a little lamb")).thenReturn("had a little lamb"); 429 | when(im.headerValueManipulation("A", "tiny piglet had Mary")).thenReturn("tiny piglet had Mary"); 430 | 431 | 432 | MarkdownRecorder mr = new MarkdownRecorder(si, im).withReplacementsInRecording("little", "tiny", "lamb", "piglet"); 433 | mr.setOutputStream("foo", out); 434 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 435 | i.noteClientRequestHeadersAndBody(im, asList("Mary: had a little lamb"), 436 | "", "text/plain", "FOO", true); 437 | i.noteServiceResponseHeaders("A: little lamb had Mary"); 438 | i.noteServiceResponseBody("", 200, "text/plain"); 439 | i.complete(); 440 | mr.finishedScript(0, false); 441 | 442 | verify(im).changeAnyHeadersForRequestToRealService(any(List.class)); 443 | verify(im).changeSingleHeaderForRequestToRealService(eq("mary: had a little lamb"), any(List.class)); 444 | 445 | verify(im).headerValueManipulation("Mary", "had a little lamb"); 446 | verify(im).headerValueManipulation("A", "tiny piglet had Mary"); 447 | verify(im).changeBodyForRequestToRealService(""); 448 | verifyNoMoreInteractions(im, si); 449 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 450 | "\n" + 451 | "### Request headers recorded for playback:\n" + 452 | "\n" + 453 | "```\n" + 454 | "mary: had a tiny piglet\n" + 455 | "```\n" + 456 | "\n" + 457 | "### Request body recorded for playback (text/plain):\n" + 458 | "\n" + 459 | "```\n" + 460 | "\n" + 461 | "```\n" + 462 | "\n" + 463 | "### Response headers recorded for playback:\n" + 464 | "\n" + 465 | "```\n" + 466 | "A: tiny piglet had Mary\n" + 467 | "```\n" + 468 | "\n" + 469 | "### Response body recorded for playback (200: text/plain):\n" + 470 | "\n" + 471 | "```\n" + 472 | "\n" + 473 | "```\n\n", out.toString()); 474 | } 475 | 476 | @Test 477 | public void debugChunksCanBeRecorded() { 478 | final InteractionManipulations im = mock(InteractionManipulations.class); 479 | final ServiceInteroperation si = mock(ServiceInteroperation.class); 480 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 481 | when(im.headerValueManipulation("A", "a")).thenReturn("a"); 482 | when(im.headerValueManipulation("B", "b")).thenReturn("b"); 483 | when(im.headerValueManipulation("C", "c")).thenReturn("c"); 484 | when(im.headerValueManipulation("D", "d")).thenReturn("d"); 485 | when(im.headerValueManipulation("XX", "xx")).thenReturn("xx"); 486 | 487 | MarkdownRecorder mr = new MarkdownRecorder(si, im).withExtraDebugOutput(); 488 | mr.setOutputStream("foo", out); 489 | InteractionMonitor.Interaction i = mr.newInteraction(0, "ctx", "FOO", "/a/b/c", "http://foo.com/bar"); 490 | 491 | i.noteServiceResponseHeaders("XX: xx"); 492 | i.noteServiceResponseBody("", 200, "text/plain"); 493 | i.debugOriginalServiceResponseHeaders("A: a", "B: b"); 494 | i.debugClientsServiceResponseHeaders("C: c", "D: d"); 495 | i.debugOriginalServiceResponseBody("BBBB", 999, "foo/bar"); 496 | i.debugClientsServiceResponseBody("ZZZZ", 888, "bar/foo"); 497 | 498 | i.complete(); 499 | mr.finishedScript(0, false); 500 | 501 | //verifyNoMoreInteractions(im, si); 502 | assertEquals("## Interaction 0: FOO /a/b/c\n" + 503 | "\n" + 504 | "### Response headers recorded for playback:\n" + 505 | "\n" + 506 | "```\n" + 507 | "XX: xx\n" + 508 | "```\n" + 509 | "\n" + 510 | "### Response body recorded for playback (200: text/plain):\n" + 511 | "\n" + 512 | "```\n" + 513 | "\n" + 514 | "```\n" + 515 | "\n" + 516 | "### DEBUG: Response headers from real service, unchanged:\n" + 517 | "\n" + 518 | "```\n" + 519 | "A: a\n" + 520 | "B: b\n" + 521 | "```\n" + 522 | "\n" + 523 | "### DEBUG: Response Headers for client, possibly changed after recording:\n" + 524 | "\n" + 525 | "```\n" + 526 | "C: c\n" + 527 | "D: d\n" + 528 | "```\n" + 529 | "\n" + 530 | "### DEBUG: Response body from real service, unchanged (999: foo/bar):\n" + 531 | "\n" + 532 | "```\n" + 533 | "BBBB\n" + 534 | "```\n" + 535 | "\n" + 536 | "### DEBUG: Response body for client, possibly changed after recording (888: bar/foo):\n" + 537 | "\n" + 538 | "```\n" + 539 | "ZZZZ\n" + 540 | "```\n" + 541 | "\n", out.toString()); 542 | } 543 | 544 | 545 | 546 | } 547 | -------------------------------------------------------------------------------- /core/src/test/java/com/paulhammant/servirtium/MarkdownReplayerTest.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import static org.hamcrest.Matchers.equalTo; 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertThat; 11 | import static org.junit.Assert.fail; 12 | 13 | public class MarkdownReplayerTest { 14 | 15 | private static final InteractionManipulations.NullObject NO_MANIPULATIONS = new InteractionManipulations.NullObject(); 16 | 17 | @Test 18 | public void replayerShouldNotWorkWithoutAPreviouslyRecordedScript() { 19 | MarkdownReplayer m = new MarkdownReplayer(); 20 | try { 21 | m.setPlaybackConversation("oh noes"); 22 | fail("should have barfed"); 23 | } catch (UnsupportedOperationException e) { 24 | assertEquals("No '## Interaction' found in conversation 'oh noes'. Wrong/empty script file?", e.getMessage()); 25 | } 26 | } 27 | 28 | @Test 29 | public void replayerShouldWorkWithAPreviouslyRecordedScript() { 30 | MarkdownReplayer m = new MarkdownReplayer(); 31 | m.setPlaybackConversation("## Interaction 0: GET /hello/how/are/you.json\n" + 32 | "\n" + 33 | "### Request headers recorded for playback:\n" + 34 | "\n" + 35 | "```\n" + 36 | "foo: aaa\n" + 37 | "bar: bbb\n" + 38 | "```\n" + 39 | "\n" + 40 | "### Request body recorded for playback ():\n" + 41 | "\n" + 42 | "```\n" + 43 | "\n" + 44 | "```\n" + 45 | "\n" + 46 | "### Response headers recorded for playback:\n" + 47 | "\n" + 48 | "```\n" + 49 | "h1: one\n" + 50 | "h2: two\n" + 51 | "```\n" + 52 | "\n" + 53 | "### Response body recorded for playback (200: text/plain; charset=utf-8):\n" + 54 | "\n" + 55 | "```\n" + 56 | "{\n" + 57 | " \"hello\": \"how-are-you\"\n" + 58 | "}\n" + 59 | "```\n" + 60 | "\n"); 61 | final MarkdownReplayer.ReplayingInteraction interaction = m.newInteraction(0, "hello", "not used in playback", 62 | "not used in playback", "not used in playback"); 63 | interaction.noteClientRequestHeadersAndBody(NO_MANIPULATIONS, Arrays.asList("foo: aaa", "bar: bbb"), "", "", "GET", false); 64 | ServiceResponse x = m.getServiceResponseForRequest("GET", "http://example.com/hello/how/are/you.json", interaction, false); 65 | assertEquals(2, x.headers.length); 66 | assertEquals("h1: one", x.headers[0]); 67 | assertEquals("h2: two", x.headers[1]); 68 | assertEquals("{\n \"hello\": \"how-are-you\"\n}", x.body); 69 | } 70 | 71 | @Test 72 | public void unexpectedHeaders() { 73 | MarkdownReplayer m = new MarkdownReplayer(); 74 | m.setPlaybackConversation("## Interaction 0: GET /hello/how/are/you.json\n" + 75 | "\n" + 76 | "### Request headers recorded for playback:\n" + 77 | "\n" + 78 | "```\n" + 79 | "foo: aaa\n" + 80 | "bar: bbb\n" + 81 | "```\n" + 82 | "\n" + 83 | "### Request body recorded for playback ():\n" + 84 | "\n" + 85 | "```\n" + 86 | "\n" + 87 | "```\n" + 88 | "\n" + 89 | "### Response headers recorded for playback:\n" + 90 | "\n" + 91 | "```\n" + 92 | "h1: one\n" + 93 | "h2: two\n" + 94 | "```\n" + 95 | "\n" + 96 | "### Response body recorded for playback (200: text/plain; charset=utf-8):\n" + 97 | "\n" + 98 | "```\n" + 99 | "{\n" + 100 | " \"hello\": \"how-are-you\"\n" + 101 | "}\n" + 102 | "```\n" + 103 | "\n"); 104 | final MarkdownReplayer.ReplayingInteraction interaction = m.newInteraction(0, "hello", "not used in playback", 105 | "not used in playback", "not used in playback"); 106 | interaction.noteClientRequestHeadersAndBody(NO_MANIPULATIONS, Arrays.asList("foo: aaaaaaaa", "bar: bbbbbbbbb"), "", "", "GET", false); 107 | 108 | try { 109 | ServiceResponse x = m.getServiceResponseForRequest("GET", "http://example.com/hello/how/are/you.json", interaction, false); 110 | } catch (AssertionError e) { 111 | 112 | assertThat(e.getMessage(), equalTo("Interaction 0 (method: GET) in file 'no filename set' (context: hello), headers from " + 113 | "the client that should be sent to real server are not the same as those previously recorded")); 114 | 115 | assertThat(e.getCause().getMessage(), equalTo("\n" + 116 | "Expected: [\"foo: aaa\", \"bar: bbb\"] in any order\n" + 117 | " but: Not matched: \"foo: aaaaaaaa\"")); 118 | } 119 | } 120 | 121 | @Test 122 | public void unexpectedHttpMethod() { 123 | MarkdownReplayer m = new MarkdownReplayer(); 124 | m.setPlaybackConversation("## Interaction 0: GET /hello/how/are/you.json\n" + 125 | "\n" + 126 | "### Request headers recorded for playback:\n" + 127 | "\n" + 128 | "```\n" + 129 | "foo: aaa\n" + 130 | "bar: bbb\n" + 131 | "```\n" + 132 | "\n" + 133 | "### Request body recorded for playback ():\n" + 134 | "\n" + 135 | "```\n" + 136 | "\n" + 137 | "```\n" + 138 | "\n" + 139 | "### Response headers recorded for playback:\n" + 140 | "\n" + 141 | "```\n" + 142 | "h1: one\n" + 143 | "h2: two\n" + 144 | "```\n" + 145 | "\n" + 146 | "### Response body recorded for playback (200: text/plain; charset=utf-8):\n" + 147 | "\n" + 148 | "```\n" + 149 | "{\n" + 150 | " \"hello\": \"how-are-you\"\n" + 151 | "}\n" + 152 | "```\n" + 153 | "\n"); 154 | final MarkdownReplayer.ReplayingInteraction interaction = m.newInteraction(0, "hello", "not used in playback", 155 | "not used in playback", "not used in playback"); 156 | interaction.noteClientRequestHeadersAndBody(NO_MANIPULATIONS, Arrays.asList("foo: aaaaaaaa", "bar: bbbbbbbbb"), "", "", "GET", false); 157 | 158 | try { 159 | ServiceResponse x = m.getServiceResponseForRequest("BLORT", "http://example.com/hello/how/are/you.json", interaction, false); 160 | } catch (AssertionError e) { 161 | 162 | assertThat(e.getMessage(), equalTo("Interaction 0 (method: GET) in file 'no filename set' (context: hello), " + 163 | "method from the client that should be sent to real server are not the same as " + 164 | "expected: BLORT (URL=http://example.com/hello/how/are/you.json, script=no filename set)")); 165 | 166 | assertThat(e.getCause().getMessage(), equalTo("\n" + 167 | "Expected: \"GET\"\n" + 168 | " but: was \"BLORT\"")); 169 | } 170 | } 171 | 172 | @Test 173 | public void replayerShouldWorkWithAPreviouslyRecordedScriptAndRequestHeaderReplacements() { 174 | MarkdownReplayer m = new MarkdownReplayer(); 175 | m.setPlaybackConversation("## Interaction 0: GET /hello/how/are/you.json\n" + 176 | "\n" + 177 | "### Request headers recorded for playback:\n" + 178 | "\n" + 179 | "```\n" + 180 | "foo: aaa\n" + 181 | "bar: bbb\n" + 182 | "```\n" + 183 | "\n" + 184 | "### Request body recorded for playback ():\n" + 185 | "\n" + 186 | "```\n" + 187 | "\n" + 188 | "```\n" + 189 | "\n" + 190 | "### Response headers recorded for playback:\n" + 191 | "\n" + 192 | "```\n" + 193 | "h1: one\n" + 194 | "h2: two\n" + 195 | "```\n" + 196 | "\n" + 197 | "### Response body recorded for playback (200: text/plain; charset=utf-8):\n" + 198 | "\n" + 199 | "```\n" + 200 | "{\n" + 201 | " \"hello\": \"how-are-you\"\n" + 202 | "}\n" + 203 | "```\n" + 204 | "\n"); 205 | m.withReplacementInPlayback("abc", "foo"); 206 | MarkdownReplayer.ReplayingInteraction interaction = m.newInteraction(0, "hello", "not used in playback", 207 | "not used in playback", "not used in playback"); 208 | interaction.noteClientRequestHeadersAndBody(NO_MANIPULATIONS, Arrays.asList("abc: aaa", "bar: bbb"), "", "", "GET", false); 209 | ServiceResponse x = m.getServiceResponseForRequest("GET", "http://example.com/hello/how/are/you.json", interaction, false); 210 | assertEquals(2, x.headers.length); 211 | assertEquals("h1: one", x.headers[0]); 212 | assertEquals("h2: two", x.headers[1]); 213 | assertEquals("{\n \"hello\": \"how-are-you\"\n}", x.body); 214 | } 215 | 216 | @Test 217 | public void replayerShouldWorkWithAPreviouslyRecordedScriptAndRequestBodyReplacements() { 218 | MarkdownReplayer m = new MarkdownReplayer(); 219 | m.setPlaybackConversation("## Interaction 0: POST /hello/how/are/you.json\n" + 220 | "\n" + 221 | "### Request headers recorded for playback:\n" + 222 | "\n" + 223 | "```\n" + 224 | "foo: aaa\n" + 225 | "bar: bbb\n" + 226 | "```\n" + 227 | "\n" + 228 | "### Request body recorded for playback (text/plain):\n" + 229 | "\n" + 230 | "```\n" + 231 | "kapow\n" + 232 | "```\n" + 233 | "\n" + 234 | "### Response headers recorded for playback:\n" + 235 | "\n" + 236 | "```\n" + 237 | "h1: one\n" + 238 | "h2: two\n" + 239 | "```\n" + 240 | "\n" + 241 | "### Response body recorded for playback (200: text/plain; charset=utf-8):\n" + 242 | "\n" + 243 | "```\n" + 244 | "{\n" + 245 | " \"hello\": \"how-are-you\"\n" + 246 | "}\n" + 247 | "```\n" + 248 | "\n"); 249 | m.withReplacementInPlayback("smack", "kapow"); 250 | final MarkdownReplayer.ReplayingInteraction interaction = m.newInteraction(0, "hello", "not used in playback", 251 | "not used in playback", "not used in playback"); 252 | final List clientRequestHeaders = Arrays.asList("foo: aaa", "bar: bbb"); 253 | interaction.noteClientRequestHeadersAndBody(NO_MANIPULATIONS, clientRequestHeaders, "smack", "text/plain", "GET", false); 254 | ServiceResponse x = m.getServiceResponseForRequest("POST", "http://example.com/hello/how/are/you.json", interaction, false); 255 | assertEquals(2, x.headers.length); 256 | assertEquals("h1: one", x.headers[0]); 257 | assertEquals("h2: two", x.headers[1]); 258 | assertEquals("{\n \"hello\": \"how-are-you\"\n}", x.body); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /core/src/test/java/com/paulhammant/servirtium/UtilityTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static com.paulhammant.servirtium.ServirtiumServer.classAndTestName; 8 | import static org.junit.Assert.assertEquals; 9 | 10 | public class UtilityTests { 11 | 12 | @Test 13 | public void testNameCanBeCalculated() throws IOException { 14 | assertEquals("com.paulhammant.servirtium.UtilityTests.testNameCanBeCalculated", classAndTestName()); 15 | } 16 | 17 | @Test 18 | public void testNameCanBeCalculated_2() throws IOException { 19 | assertEquals("com.paulhammant.servirtium.UtilityTests.testNameCanBeCalculated_2", delegateToTestNameForDepthTestingPurposes()); 20 | } 21 | 22 | private String delegateToTestNameForDepthTestingPurposes() { 23 | return classAndTestName(1); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/resources/TodobackendDotComServiceRecording.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/core/src/test/resources/TodobackendDotComServiceRecording.md -------------------------------------------------------------------------------- /core/src/test/resources/png-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/servirtium/servirtium-java/e24f4c79396fa57e0f40f2b17c678be151c71baf/core/src/test/resources/png-transparent.png -------------------------------------------------------------------------------- /core/src/test/resources/test.json: -------------------------------------------------------------------------------- 1 | {"Accept-Language": "en-US,en;q=0.8", "Host": "headers.jsontest.com", "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } 2 | -------------------------------------------------------------------------------- /docs/SvnMerkleizer_More_Info.md: -------------------------------------------------------------------------------- 1 | # SvnMerkleizer project - emulation of Subversion 2 | 3 | SvnMerkleizer is very I/O heavy to a coupled Subversion server/repository. In a mode of operation 4 | for suite of service tests that use RestAssured to hit SvnMerkleizer which in turn hits Subversion many 5 | times `DirectServiceTests` has no use of Servirtium and takes 1m 40s. 6 | 7 | A second mode of operation in this test-suite 8 | uses Servirtium to 9 | record Subversion HTTP requests/responses in `RecordingSubversionServiceTests` takes 2m 13s. A mode of operation that 10 | plays back the same recording in `PlayingBackSvnMerkleizerServiceTests` takes 24s. 11 | These tests (whether direct, recording or playing back) are non standard in that they perform (or emulate) 13,500 HTTP 12 | operations to Subversion, and each of these three modes of operation give 69% code 13 | coverage to the SvnMerkleizer codebase for RecordingSubversionServiceTests or 14 | PlayingBackSvnMerkleizerServiceTests test classes. 15 | 16 | This suite is ridiculous overkill really, as 13,500 HTTP operations recorded into Markdown is too big to be human comprehensible. 17 | For correct usage of Servirtium, you'd have a test that did a handful of HTTP operations at most, and finished 18 | (playback mode) in less than half a second. 19 | 20 | * `RecordingSubversionServiceTests` - 69% code coverage - 2m 21s 21 | * `PlayingBackSvnMerkleizerServiceTests` - 69% code coverage - 51s 22 | 23 | Markdown recordings [here](https://github.com/paul-hammant/SvnMerkleizer/tree/master/src/test/mocks/subversion). 24 | 25 | ## Architecture diagrams: 26 | 27 | (ASCII box art) 28 | 29 | [For recording of Subversion in a test class](https://github.com/paul-hammant/SvnMerkleizer/blob/master/src/test/java/com/paulhammant/svnmerkleizer/hiddengetroutes/recorded/subversion/RecordingSubversionServiceTests.java#L59) 30 | [For playback of Subversion in a test class](https://github.com/paul-hammant/SvnMerkleizer/blob/master/src/test/java/com/paulhammant/svnmerkleizer/hiddengetroutes/recorded/subversion/PlayingBackSubversionServiceTests.java#L57) 31 | 32 | The playback box art shows two fewer boxes in that mode of operation. 33 | 34 | ## Differences between record and playback test for recorded Subversion 35 | 36 | ![](https://user-images.githubusercontent.com/82182/59253263-b7fcfe00-8c25-11e9-81c3-62111bfe197b.png) 37 | 38 | # SvnMerkleizer project - testbase emulation of SvnMerkleizer itself 39 | 40 | This is nonsensical as testing mocks is not really legitmate - tests should be of "prod code" with mocks removing dependencies 41 | on collaborators). However, here is the breakdown: 42 | 43 | * `RecordingSvnMerkleizerServiceTests` - 69% code coverage - 1m 36s 44 | * `PlayingBackSvnMerkleizerServiceTests` - 0% code coverage - 31s 45 | 46 | The playback shows the lack of coverage of SvnMerkleizer itself. The mocking using Servirtium of SvnMerkleizer is only 47 | appropriate for **another library/app** that does HTTP calls to a SvnMerkleizer extended Subversion server. For that 48 | eventuality, these two tests would be copyable to another project. Well, maybe the setup/teardown is. Either way, you'd 49 | be getting your coverage up to 70% or more again, and not observe it at 10% or below. 50 | 51 | Markdown recordings [here](https://github.com/paul-hammant/SvnMerkleizer/tree/master/src/test/mocks/svnmerkleizer). 52 | 53 | ## Architecture diagrams: 54 | 55 | (ASCII box art) 56 | 57 | [For recording of SvnMerkleizer in a test class](https://github.com/paul-hammant/SvnMerkleizer/blob/master/src/test/java/com/paulhammant/svnmerkleizer/hiddengetroutes/recorded/svnmerkleizer/RecordingSvnMerkleizerServiceTests.java#L59) 58 | [For playback of SvnMerkleizer in a test class](https://github.com/paul-hammant/SvnMerkleizer/blob/master/src/test/java/com/paulhammant/svnmerkleizer/hiddengetroutes/recorded/svnmerkleizer/PlayingBackSvnMerkleizerServiceTests.java#L55) 59 | 60 | The playback box art shows two fewer boxes in that mode of operation. Coverage is 0% for the 61 | playback which highlights the folly of this test being in SvnMerkleizer's own codebase - it proves 62 | nothing at all. 63 | 64 | -------------------------------------------------------------------------------- /jetty/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | 7 | com.paulhammant.servirtium 8 | servirtium-pom 9 | 0.9.10-SNAPSHOT 10 | 11 | 12 | servirtium-jetty 13 | jar 14 | 15 | 16 | 17 | 18 | com.paulhammant 19 | servirtium-core 20 | ${project.version} 21 | 22 | 23 | 24 | org.eclipse.jetty 25 | jetty-server 26 | 11.0.12 27 | 28 | 29 | 30 | com.paulhammant 31 | servirtium-core 32 | tests 33 | test-jar 34 | 0.9.10-SNAPSHOT 35 | test 36 | 37 | 38 | 39 | junit 40 | junit 41 | 4.13.2 42 | test 43 | 44 | 45 | 46 | io.rest-assured 47 | rest-assured 48 | 5.2.0 49 | test 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-compiler-plugin 59 | 3.10.1 60 | 61 | 1.8 62 | 1.8 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | tests 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-surefire-plugin 76 | 2.22.2 77 | 78 | 79 | all-tests 80 | 81 | test 82 | 83 | 84 | 85 | **/*Tests.java 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /jetty/src/main/java/com/paulhammant/servirtium/jetty/JettyServirtiumServer.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.jetty; 2 | 3 | import com.paulhammant.servirtium.InteractionManipulations; 4 | import com.paulhammant.servirtium.InteractionMonitor; 5 | import com.paulhammant.servirtium.ServiceMonitor; 6 | import com.paulhammant.servirtium.ServiceResponse; 7 | import com.paulhammant.servirtium.ServirtiumServer; 8 | import org.eclipse.jetty.server.Request; 9 | import org.eclipse.jetty.server.Server; 10 | import org.eclipse.jetty.server.handler.AbstractHandler; 11 | import org.eclipse.jetty.util.log.Logger; 12 | 13 | import jakarta.servlet.ServletInputStream; 14 | import jakarta.servlet.http.HttpServletRequest; 15 | import jakarta.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.Enumeration; 20 | import java.util.List; 21 | import java.util.Scanner; 22 | 23 | import static com.paulhammant.servirtium.JsonAndXmlUtilities.prettifyDocOrNot; 24 | 25 | public class JettyServirtiumServer extends ServirtiumServer { 26 | 27 | private Server jettyServer; 28 | boolean failed = false; 29 | Throwable lastFailure; 30 | 31 | public JettyServirtiumServer(ServiceMonitor monitor, int port, 32 | InteractionManipulations interactionManipulations, 33 | InteractionMonitor interactionMonitor) { 34 | super(interactionManipulations, interactionMonitor); 35 | 36 | jettyServer = new Server(port); 37 | // How the f*** do you turn off Embedded Jetty's logging??? 38 | // Everything I tried (mostly static operations on Log) didn't work. 39 | 40 | jettyServer.setHandler(new AbstractHandler() { 41 | 42 | @Override 43 | public void handle(String target, org.eclipse.jetty.server.Request baseRequest, 44 | HttpServletRequest request, HttpServletResponse response) throws IOException { 45 | handleExchange(baseRequest, request, response, monitor); 46 | } 47 | }); 48 | } 49 | 50 | private void handleExchange(Request baseRequest, HttpServletRequest request, 51 | HttpServletResponse response, ServiceMonitor monitor) throws IOException { 52 | 53 | bumpInteractionNum(); 54 | 55 | String method = request.getMethod(); 56 | 57 | String url = request.getRequestURL().toString(); 58 | String uri = request.getRequestURI(); 59 | 60 | String qs = request.getQueryString(); 61 | if (qs != null) { 62 | uri = uri + "?" + qs; 63 | url = url + "?" + qs; 64 | } 65 | 66 | if (uri.contains("://")) { 67 | uri = uri.substring(uri.indexOf("/", 8)); 68 | } 69 | 70 | url = (url.startsWith("http://") || url.startsWith("https://")) 71 | ? url : "http://" + request.getRemoteHost() + ":" + request.getRemotePort() + uri; 72 | 73 | // List clientRequestHeaders = new ArrayList<>(); 74 | 75 | try { 76 | 77 | if (method.equals("CONNECT")) { 78 | response.getWriter().write("Servirtium does not support CONNECT yet"); 79 | response.setContentType("text/plain"); 80 | response.setStatus(500); 81 | return; 82 | } 83 | 84 | InteractionMonitor.Interaction interaction = interactionMonitor.newInteraction(getInteractionNum(), getContext(), method, uri, url); 85 | 86 | monitor.interactionStarted(getInteractionNum(), interaction); 87 | 88 | String clientRequestContentType = request.getContentType(); 89 | if (clientRequestContentType == null) { 90 | clientRequestContentType = ""; 91 | } 92 | 93 | // if (isText(contentType)) { 94 | // BufferedReader reader = baseRequest.getReader(); 95 | // clientRequestBody = reader.lines().collect(Collectors.joining("\n")); 96 | // } else { 97 | // ServletInputStream is = baseRequest.getInputStream(); 98 | // clientRequestBody = new byte[is.available()]; 99 | // 100 | // } 101 | // 102 | 103 | final UrlAndHeaders urlAndHeaders = prepareHeadersAndBodyForService(request, method, url, 104 | interaction, clientRequestContentType, interactionManipulations); 105 | 106 | // INTERACTION 107 | ServiceResponse serviceResponse = interactionMonitor.getServiceResponseForRequest(method, urlAndHeaders.url, 108 | interaction, useLowerCaseHeaders()); 109 | 110 | serviceResponse = processHeadersAndBodyBackFromRealService(interaction, serviceResponse); 111 | 112 | interaction.complete(); 113 | 114 | response.setStatus(serviceResponse.statusCode); 115 | 116 | for (String header : serviceResponse.headers) { 117 | int ix = header.indexOf(": "); 118 | String hdrKey = header.substring(0, ix); 119 | String hdrVal = header.substring(ix + 2); 120 | if (!header.contains("Content-Length")) { 121 | response.setHeader(hdrKey, hdrVal); 122 | } 123 | } 124 | 125 | if (serviceResponse.contentType != null) { 126 | response.setContentType(serviceResponse.contentType); 127 | } 128 | 129 | if (serviceResponse.body instanceof String) { 130 | response.getWriter().write((String) serviceResponse.body); 131 | } else { 132 | response.getOutputStream().write((byte[]) serviceResponse.body); 133 | } 134 | 135 | monitor.interactionFinished(getInteractionNum(), method, url, getContext()); 136 | } catch (AssertionError assertionError) { 137 | failed = true; 138 | lastFailure = assertionError; 139 | monitor.interactionFailed(getInteractionNum(), method, url, assertionError, getContext()); 140 | return; 141 | } catch (Throwable throwable) { 142 | failed = true; 143 | lastFailure = throwable; 144 | monitor.unexpectedRequestError(throwable, getContext()); 145 | return; 146 | } finally { 147 | // Inform jetty that this request has now been handled 148 | baseRequest.setHandled(true); 149 | } 150 | } 151 | 152 | private ServiceResponse processHeadersAndBodyBackFromRealService(InteractionMonitor.Interaction interaction, ServiceResponse serviceResponse) { 153 | 154 | interaction.debugOriginalServiceResponseHeaders(serviceResponse.headers); 155 | 156 | ServiceResponse originalResponse = serviceResponse; 157 | 158 | List newHeaders = new ArrayList<>(); 159 | Collections.addAll(newHeaders, serviceResponse.headers); 160 | 161 | // Change of headers back from service 162 | 163 | ArrayList newHeadersTmp = new ArrayList<>(); 164 | for (int i = 0; i < newHeaders.size(); i++) { 165 | String headerBackFromService = newHeaders.get(i); 166 | String potentiallyChangedHeader = interactionManipulations.changeSingleHeaderReturnedBackFromRealServiceForRecording(i, headerBackFromService); 167 | if (potentiallyChangedHeader != null) { 168 | newHeadersTmp.add(potentiallyChangedHeader); 169 | } 170 | } 171 | 172 | newHeaders = newHeadersTmp; 173 | 174 | interactionManipulations.changeAnyHeadersReturnedBackFromRealServiceForRecording(newHeaders); 175 | 176 | if (serviceResponse.body instanceof String) { 177 | serviceResponse = serviceResponse.withRevisedBody( 178 | interactionManipulations.changeBodyReturnedBackFromRealServiceForRecording((String) serviceResponse.body)); 179 | // recreate response 180 | 181 | if (shouldHavePrettyPrintedTextBodies()) { 182 | String body = prettifyDocOrNot((String) serviceResponse.body); 183 | if (!body.equals(serviceResponse.body)) { 184 | // realResponse.headers 185 | serviceResponse = serviceResponse.withRevisedBody(body); 186 | } 187 | } 188 | } 189 | 190 | serviceResponse = serviceResponse.withRevisedHeaders(newHeaders.toArray(new String[0])); 191 | 192 | interaction.noteServiceResponseHeaders(serviceResponse.headers); 193 | 194 | serviceResponse = serviceResponse.withRevisedHeaders( 195 | interactionManipulations.changeHeadersForClientResponseAfterRecording(serviceResponse.headers)); 196 | 197 | interaction.debugClientsServiceResponseHeaders(serviceResponse.headers); 198 | 199 | interaction.debugOriginalServiceResponseBody(originalResponse.body, originalResponse.statusCode, originalResponse.contentType); 200 | 201 | interaction.noteServiceResponseBody(serviceResponse.body, serviceResponse.statusCode, serviceResponse.contentType); 202 | 203 | 204 | if (serviceResponse.body instanceof String) { 205 | final String b = (String) serviceResponse.body; 206 | serviceResponse = serviceResponse.withRevisedBody(interactionManipulations.changeBodyForClientResponseAfterRecording(b)); 207 | } 208 | 209 | interaction.debugClientsServiceResponseBody(originalResponse.body, originalResponse.statusCode, originalResponse.contentType); 210 | 211 | return serviceResponse; 212 | } 213 | 214 | private class UrlAndHeaders { 215 | String url; 216 | List clientRequestHeaders; 217 | 218 | public UrlAndHeaders(String url, List clientRequestHeaders) { 219 | this.url = url; 220 | this.clientRequestHeaders = clientRequestHeaders; 221 | } 222 | } 223 | 224 | private UrlAndHeaders prepareHeadersAndBodyForService(HttpServletRequest request, String method, String url, 225 | InteractionMonitor.Interaction interaction, 226 | String clientRequestContentType, 227 | InteractionManipulations interactionManipulations) throws IOException { 228 | Enumeration hdrs = request.getHeaderNames(); 229 | 230 | ServletInputStream is = request.getInputStream(); 231 | 232 | Object clientRequestBody = null; 233 | 234 | if (is.available() > 0) { 235 | 236 | if (isText(clientRequestContentType)) { 237 | clientRequestBody = null; 238 | String characterEncoding = request.getCharacterEncoding(); 239 | if (characterEncoding == null) { 240 | characterEncoding = "utf-8"; 241 | } 242 | try (Scanner scanner = new Scanner(is, characterEncoding)) { 243 | clientRequestBody = scanner.useDelimiter("\\A").next(); 244 | } 245 | if (shouldHavePrettyPrintedTextBodies() && clientRequestBody != null) { 246 | clientRequestBody = prettifyDocOrNot((String) clientRequestBody); 247 | } 248 | } else { 249 | byte[] targetArray = new byte[is.available()]; 250 | is.read(targetArray); 251 | clientRequestBody = targetArray; 252 | } 253 | } 254 | 255 | List clientRequestHeaders = new ArrayList<>(); 256 | while (hdrs.hasMoreElements()) { 257 | String hdrName = hdrs.nextElement(); 258 | Enumeration hdrVals = request.getHeaders(hdrName); 259 | while (hdrVals.hasMoreElements()) { 260 | String s = hdrVals.nextElement(); 261 | clientRequestHeaders.add(hdrName + ": " + s); 262 | } 263 | } 264 | 265 | List clientRequestHeaders2 = interaction.noteClientRequestHeadersAndBody(interactionManipulations, clientRequestHeaders, clientRequestBody, clientRequestContentType, method, useLowerCaseHeaders()); 266 | 267 | final String chgdURL = interactionManipulations.changeUrlForRequestToRealService(url); 268 | 269 | int ixU = url.indexOf("/", url.indexOf(":") + 3); 270 | int ixC = chgdURL.indexOf("/", chgdURL.indexOf(":") + 3); 271 | 272 | if (ixU != -1 && ixC != -1 && !url.substring(ixU).equals(chgdURL.substring(ixC))) { 273 | interaction.noteChangedResourceForRequestToClient(url.substring(ixU), chgdURL.substring(ixC)); 274 | } 275 | 276 | return new UrlAndHeaders(chgdURL, clientRequestHeaders2); 277 | } 278 | 279 | public ServirtiumServer start() throws Exception { 280 | lastFailure = null; 281 | jettyServer.start(); 282 | return this; 283 | } 284 | 285 | public void stop() { 286 | try { 287 | interactionMonitor.finishedScript(getInteractionNum(), failed); // just in case 288 | } finally { 289 | try { 290 | jettyServer.setStopTimeout(1); 291 | jettyServer.stop(); 292 | } catch (Exception e) { 293 | throw new RuntimeException(e); 294 | } 295 | } 296 | } 297 | 298 | public void finishedScript() { 299 | interactionMonitor.finishedScript(getInteractionNum(), failed); 300 | } 301 | 302 | @Override 303 | public Throwable getLastException() { 304 | return lastFailure; 305 | } 306 | 307 | public static void disableJettyLogging() { 308 | System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog"); 309 | System.setProperty("org.eclipse.jetty.LEVEL", "OFF"); 310 | org.eclipse.jetty.util.log.Log.setLog(new NoLogging()); 311 | 312 | } 313 | public static class NoLogging implements Logger { 314 | @Override public String getName() { return "no"; } 315 | @Override public void warn(String msg, Object... args) { } 316 | @Override public void warn(Throwable thrown) { } 317 | @Override public void warn(String msg, Throwable thrown) { } 318 | @Override public void info(String msg, Object... args) { } 319 | @Override public void info(Throwable thrown) { } 320 | @Override public void info(String msg, Throwable thrown) { } 321 | @Override public boolean isDebugEnabled() { return false; } 322 | @Override public void setDebugEnabled(boolean enabled) { } 323 | @Override public void debug(String msg, Object... args) { } 324 | @Override public void debug(Throwable thrown) { } 325 | @Override public void debug(String msg, Throwable thrown) { } 326 | @Override public Logger getLogger(String name) { return this; } 327 | @Override public void ignore(Throwable ignored) { } 328 | @Override public void debug(String s, long l) { } 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /jetty/src/test/java/com/paulhammant/servirtium/jetty/SimpleGetCentricBinaryWithJettyTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.jetty; 2 | 3 | import com.paulhammant.servirtium.InteractionMonitor; 4 | import com.paulhammant.servirtium.ServiceMonitor; 5 | import com.paulhammant.servirtium.ServirtiumServer; 6 | import com.paulhammant.servirtium.SimpleGetCentricBinaryTests; 7 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 8 | import org.junit.After; 9 | import org.junit.Test; 10 | 11 | public class SimpleGetCentricBinaryWithJettyTests extends SimpleGetCentricBinaryTests { 12 | 13 | protected ServirtiumServer makeServirtiumServer(SimpleInteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor) { 14 | return new JettyServirtiumServer(new ServiceMonitor.Console(), 15 | 8080, interactionManipulations, interactionMonitor); 16 | } 17 | 18 | @Override @After 19 | public void tearDown() { 20 | super.tearDown(); 21 | } 22 | 23 | @Override @Test 24 | public void canRecordABinaryGetFromApachesSubversionViaOkHttp() throws Exception { 25 | super.canRecordABinaryGetFromApachesSubversionViaOkHttp(); 26 | } 27 | 28 | @Override @Test 29 | public void canRecordAPngGetFromWikimedia() throws Exception { 30 | super.canRecordAPngGetFromWikimedia(); 31 | } 32 | 33 | @Override @Test 34 | public void canRecordASvgGetFromWikimedia() throws Exception { 35 | super.canRecordASvgGetFromWikimedia(); 36 | } 37 | 38 | @Override @Test 39 | public void canReplayABinaryGetFromApachesSubversion() throws Exception { 40 | super.canReplayABinaryGetFromApachesSubversion(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /jetty/src/test/java/com/paulhammant/servirtium/jetty/SimpleGetCentricTextWithJettyTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.jetty; 2 | 3 | import com.paulhammant.servirtium.InteractionMonitor; 4 | import com.paulhammant.servirtium.ServiceMonitor; 5 | import com.paulhammant.servirtium.ServirtiumServer; 6 | import com.paulhammant.servirtium.SimpleGetCentricTextTests; 7 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 8 | import org.junit.After; 9 | import org.junit.Ignore; 10 | import org.junit.Test; 11 | 12 | public class SimpleGetCentricTextWithJettyTests extends SimpleGetCentricTextTests { 13 | 14 | public ServirtiumServer makeServirtiumServer(ServiceMonitor.Console serverMonitor, SimpleInteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor, int port) { 15 | return new JettyServirtiumServer(serverMonitor, 16 | port, interactionManipulations, interactionMonitor); 17 | } 18 | 19 | @After 20 | public void tearDown() { 21 | super.tearDown(); 22 | } 23 | 24 | @Override @Test 25 | public void canRecordASimpleGetFromApachesSubversionViaOkHttp() throws Exception { 26 | super.canRecordASimpleGetFromApachesSubversionViaOkHttp(); 27 | } 28 | 29 | @Override @Test 30 | public void canRecordASequenceThenBarfInPlaybackWithClearMessagingIfUnplayedInteractions() throws Exception { 31 | super.canRecordASequenceThenBarfInPlaybackWithClearMessagingIfUnplayedInteractions(); 32 | } 33 | 34 | @Override @Test 35 | public void canRecordASimpleGetOfARedditJsonDocumentAndPrettify() throws Exception { 36 | super.canRecordASimpleGetOfARedditJsonDocumentAndPrettify(); 37 | } 38 | 39 | @Override @Test 40 | public void canRecordASimpleQueryStringGet() throws Exception { 41 | super.canRecordASimpleQueryStringGet(); 42 | } 43 | 44 | @Override @Test 45 | public void canPassThroughASimpleQueryStringGet() throws Exception { 46 | super.canPassThroughASimpleQueryStringGet(); 47 | } 48 | 49 | @Override @Test 50 | public void canPlaybackASimpleQueryStringGet() throws Exception { 51 | super.canPlaybackASimpleQueryStringGet(); 52 | } 53 | 54 | @Override @Test 55 | public void canSupplyDebugInformationOnRedditJsonGet() throws Exception { 56 | super.canSupplyDebugInformationOnRedditJsonGet(); 57 | } 58 | 59 | @Override @Test 60 | public void worksThroughAproxyServer() throws Exception { 61 | super.worksThroughAproxyServer(); 62 | } 63 | 64 | @Override @Test @Ignore 65 | public void worksThroughAproxyServer2() throws Exception { 66 | super.worksThroughAproxyServer2(); 67 | } 68 | 69 | @Override @Test 70 | public void canRecordASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly() throws Exception { 71 | super.canRecordASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly(); 72 | } 73 | 74 | @Override @Test 75 | public void canReplayASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly() throws Exception { 76 | super.canReplayASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly(); 77 | } 78 | 79 | @Override @Test 80 | public void canReplayWithAReplacementInTheURLtoo() throws Exception { 81 | super.canReplayWithAReplacementInTheURLtoo(); 82 | } 83 | 84 | @Override @Test 85 | public void canReplayASimpleGetFromApachesSubversion() throws Exception { 86 | super.canReplayASimpleGetFromApachesSubversion(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /jetty/src/test/java/com/paulhammant/servirtium/jetty/SimplePostCentricWithJettyTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.jetty; 2 | 3 | import com.paulhammant.servirtium.*; 4 | import org.junit.After; 5 | import org.junit.Ignore; 6 | import org.junit.Test; 7 | 8 | public class SimplePostCentricWithJettyTests extends SimplePostCentricTests { 9 | 10 | public ServirtiumServer makeServirtiumServer(SimpleInteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor) { 11 | return new JettyServirtiumServer(new ServiceMonitor.Console(), 12 | 8080, interactionManipulations, interactionMonitor); 13 | } 14 | 15 | @Override @After 16 | public void tearDown() { 17 | super.tearDown(); 18 | } 19 | 20 | @Override @Test 21 | public void canRecordASimplePostToPostmanEchoViaOkHttp() throws Exception { 22 | super.canRecordASimplePostToPostmanEchoViaOkHttp(); 23 | } 24 | 25 | @Override @Test @Ignore 26 | public void canRecordABase64PostToPostmanEchoViaOkHttp() throws Exception { 27 | super.canRecordABase64PostToPostmanEchoViaOkHttp(); 28 | } 29 | 30 | @Override @Test 31 | public void canReplayASimplePostToPostmanEcho() throws Exception { 32 | super.canReplayASimplePostToPostmanEcho(); 33 | } 34 | 35 | @Override @Test @Ignore 36 | public void canReplayABase64PostToPostmanEcho() throws Exception { 37 | super.canReplayABase64PostToPostmanEcho(); 38 | } 39 | 40 | @Override @Test 41 | public void canRecordABinaryPost() throws Exception { 42 | super.canRecordABinaryPost(); 43 | } 44 | 45 | @Override @Test 46 | public void canRecordABinaryPut() throws Exception { 47 | super.canRecordABinaryPut(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /jetty/src/test/java/com/paulhammant/servirtium/jetty/TodobackendDotComRecorderMain.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.jetty; 2 | 3 | import com.paulhammant.servirtium.InteractionMonitor; 4 | import com.paulhammant.servirtium.MarkdownRecorder; 5 | import com.paulhammant.servirtium.ServiceMonitor; 6 | import com.paulhammant.servirtium.ServiceInteropViaOkHttp; 7 | import com.paulhammant.servirtium.ServirtiumServer; 8 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 9 | 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.function.BiConsumer; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | public class TodobackendDotComRecorderMain { 18 | 19 | public static final Pattern UID_PATTERN = Pattern.compile("\"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\""); 20 | 21 | public static final String DOMAIN = "todo-backend-sinatra.herokuapp.com"; 22 | 23 | public static void main(String[] args) throws Exception { 24 | 25 | /* 26 | 27 | Run this main() method from within Intellij 28 | 29 | Then, in a browser go to: 30 | 31 | http://www.todobackend.com/specs/index.html?http://localhost:8099 32 | 33 | ... src/test/resources/TodobackendDotComServiceRecording.md should be overwritten 34 | 35 | Effectively, this is the same as pointing the browser to ... 36 | 37 | https://www.todobackend.com/specs/index.html?https://todobackend-phoenix.herokuapp.com 38 | or http://www.todobackend.com/specs/index.html?http://todobackend-phoenix.herokuapp.com 39 | 40 | */ 41 | 42 | Map guids = new HashMap<>(); 43 | 44 | final SimpleInteractionManipulations manipulations = makeInteractionManipulations(guids); 45 | 46 | MarkdownRecorder recorder = new MarkdownRecorder( 47 | new ServiceInteropViaOkHttp(), manipulations) 48 | .withAlphaSortingOfHeaders(); 49 | // .withExtraDebugOutput(); // - This adds extra debugging output 50 | 51 | ServirtiumServer servirtiumServer = makeServirtiumServer(manipulations, recorder); 52 | 53 | recorder.setScriptFilename("core/src/test/resources/TodobackendDotComServiceRecording.md"); 54 | servirtiumServer.start(); 55 | 56 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 57 | 58 | recorder.noteForNextInteraction("GUIDs and their mock names", 59 | guids.toString() 60 | .replace(" ","") 61 | .replace("{","") 62 | .replace("}","") 63 | .replace(",","\n") + "\n") ; 64 | servirtiumServer.stop(); 65 | 66 | })); 67 | } 68 | 69 | public static ServirtiumServer makeServirtiumServer(SimpleInteractionManipulations manipulations, InteractionMonitor interactionMonitor) { 70 | return new JettyServirtiumServer(new ServiceMonitor.Console(), 8099, 71 | manipulations, interactionMonitor) 72 | .withPrettyPrintedTextBodies(); 73 | } 74 | 75 | private static class ReplaceMockGuidForRealOnes implements BiConsumer { 76 | private String result; 77 | 78 | public ReplaceMockGuidForRealOnes(String str) { 79 | this.result = str; 80 | } 81 | 82 | @Override 83 | public void accept(String guid, Integer integer) { 84 | result = result.replaceAll("MOCK-GUID-" + integer, guid); 85 | } 86 | } 87 | 88 | 89 | public static SimpleInteractionManipulations makeInteractionManipulations(Map guids) { 90 | return new SimpleInteractionManipulations("localhost:8099", DOMAIN) { 91 | 92 | @Override 93 | public void changeAnyHeadersForRequestToRealService(List clientRequestHeaders) { 94 | String refer = ""; 95 | for (int i = 0; i < clientRequestHeaders.size(); i++) { 96 | String s = clientRequestHeaders.get(i); 97 | if (s.startsWith("Referer:")) { 98 | refer = s; 99 | } 100 | if (s.startsWith("Cache-Control:") || s.startsWith("Pragma:") || s.startsWith("Referer:")) { 101 | clientRequestHeaders.remove(s); 102 | } 103 | } 104 | clientRequestHeaders.add("Cache-Control: no-cache"); 105 | clientRequestHeaders.add("Pragma: no-cache"); 106 | clientRequestHeaders.add(refer.replace(super.fromUrl, super.toUrl)); 107 | } 108 | 109 | @Override 110 | public String changeBodyReturnedBackFromRealServiceForRecording(String bodyFromService) { 111 | return replaceRealGuidForMockOnes(bodyFromService); 112 | } 113 | 114 | @Override 115 | public String changeSingleHeaderReturnedBackFromRealServiceForRecording(int ix, String orig) { 116 | return replaceRealGuidForMockOnes(orig); 117 | } 118 | 119 | @Override 120 | public String changeUrlForRequestToRealService(String url) { 121 | return super.changeUrlForRequestToRealService(replaceRealGuidForMockOnes(url)); 122 | } 123 | 124 | @Override 125 | public String changeBodyForClientResponseAfterRecording(String body) { 126 | 127 | final ReplaceMockGuidForRealOnes replaceMockGuidForRealOnes = new ReplaceMockGuidForRealOnes(body); 128 | guids.forEach(replaceMockGuidForRealOnes); 129 | 130 | return replaceMockGuidForRealOnes.result.replaceAll(escapeDots(DOMAIN), "localhost:8099"); 131 | } 132 | 133 | private String replaceRealGuidForMockOnes(String result) { 134 | Matcher uidMatcher = UID_PATTERN.matcher(result); 135 | while (uidMatcher.find()) { 136 | final String uid1 = uidMatcher.group(1); 137 | if (!guids.containsKey(uid1)) { 138 | guids.put(uid1, guids.size() + 1); 139 | } 140 | result = result.replaceAll(uid1, "MOCK-GUID-" + guids.get(uid1)); 141 | uidMatcher = UID_PATTERN.matcher(result); 142 | } 143 | return result; 144 | } 145 | 146 | }; 147 | } 148 | 149 | private static String escapeDots(String domain) { 150 | return domain.replaceAll("\\.", "\\."); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /jetty/src/test/java/com/paulhammant/servirtium/jetty/TodobackendDotComReplayerMain.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.jetty; 2 | 3 | import com.paulhammant.servirtium.MarkdownReplayer; 4 | import com.paulhammant.servirtium.ServirtiumServer; 5 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import static com.paulhammant.servirtium.jetty.TodobackendDotComRecorderMain.makeInteractionManipulations; 11 | import static com.paulhammant.servirtium.jetty.TodobackendDotComRecorderMain.makeServirtiumServer; 12 | 13 | public class TodobackendDotComReplayerMain { 14 | 15 | public static void main(String[] args) throws Exception { 16 | 17 | // Run this main() method from within Intellij 18 | 19 | // Then, in a browser go to: 20 | // http://www.todobackend.com/specs/index.html?http://localhost:8099/todos 21 | 22 | // ... src/test/resources/TodobackendDotComServiceRecording.md will be read and 23 | // hopefully the Jasmine tests in the browser still pass. 24 | 25 | Map guids = new HashMap<>(); 26 | 27 | final SimpleInteractionManipulations manipulations = makeInteractionManipulations(guids); 28 | 29 | MarkdownReplayer replayer = new MarkdownReplayer().withAlphaSortingOfHeaders() 30 | .withAlphaSortingOfHeaders(); 31 | 32 | ServirtiumServer servirtiumServer = makeServirtiumServer(manipulations, replayer); 33 | 34 | replayer.setScriptFilename("core/src/test/resources/TodobackendDotComServiceRecording.md"); 35 | servirtiumServer.start(); 36 | 37 | Runtime.getRuntime().addShutdownHook(new Thread(servirtiumServer::stop)); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM 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.5/maven-wrapper-0.5.5.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.5/maven-wrapper-0.5.5.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 | 3 | 4 | 4.0.0 5 | 6 | 7 | org.sonatype.oss 8 | oss-parent 9 | 9 10 | 11 | 12 | com.paulhammant.servirtium 13 | servirtium-pom 14 | 0.9.10-SNAPSHOT 15 | pom 16 | 17 | Service Virtualized HTTP 18 | Service Virtualized HTTP that includes record, playback and a markdown storage format FOR TEST 19 | AUTOMATION 20 | 21 | 22 | http://github.com/paul-hammant/servirtium 23 | 24 | 25 | Two clause BSD license 26 | https://github.com/paul-hammant/servirtium/blob/master/LICENSE.txt 27 | 28 | 29 | 30 | scm:git:git@github.com:paul-hammant/servirtium.git 31 | scm:git:git@github.com:paul-hammant/servirtium.git 32 | git@github.com:paul-hammant/servirtium.git 33 | HEAD 34 | 35 | 36 | 37 | UTF-8 38 | 39 | 40 | 41 | core 42 | jetty 43 | undertow 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /undertow/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | 7 | com.paulhammant.servirtium 8 | servirtium-pom 9 | 0.9.10-SNAPSHOT 10 | 11 | 12 | servirtium-undertow 13 | jar 14 | 15 | 16 | 17 | 18 | com.paulhammant 19 | servirtium-core 20 | ${project.version} 21 | 22 | 23 | 24 | io.undertow 25 | undertow-core 26 | 2.3.0.Final 27 | 28 | 29 | 30 | org.eclipse.jetty 31 | jetty-server 32 | 11.0.12 33 | test 34 | 35 | 36 | 37 | com.paulhammant 38 | servirtium-core 39 | tests 40 | test-jar 41 | 0.9.10-SNAPSHOT 42 | test 43 | 44 | 45 | 46 | junit 47 | junit 48 | 4.13.2 49 | test 50 | 51 | 52 | 53 | io.rest-assured 54 | rest-assured 55 | 5.2.0 56 | test 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-compiler-plugin 68 | 3.10.1 69 | 70 | 1.8 71 | 1.8 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | tests 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-surefire-plugin 85 | 2.22.2 86 | 87 | 88 | all-tests 89 | 90 | test 91 | 92 | 93 | 94 | **/*Tests.java 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /undertow/src/main/java/com/paulhammant/servirtium/undertow/UndertowServirtiumServer.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.undertow; 2 | 3 | import com.paulhammant.servirtium.InteractionManipulations; 4 | import com.paulhammant.servirtium.InteractionMonitor; 5 | import com.paulhammant.servirtium.ServiceMonitor; 6 | import com.paulhammant.servirtium.ServiceResponse; 7 | import com.paulhammant.servirtium.ServirtiumServer; 8 | import io.undertow.Undertow; 9 | import io.undertow.server.HttpServerExchange; 10 | import io.undertow.server.handlers.BlockingHandler; 11 | import io.undertow.util.HeaderValues; 12 | import io.undertow.util.Headers; 13 | import io.undertow.util.HttpString; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.Scanner; 21 | 22 | import static com.paulhammant.servirtium.JsonAndXmlUtilities.prettifyDocOrNot; 23 | 24 | public class UndertowServirtiumServer extends ServirtiumServer { 25 | 26 | private Undertow undertowServer; 27 | private boolean failed = false; 28 | private Throwable lastFailure; 29 | 30 | public UndertowServirtiumServer(ServiceMonitor monitor, int port, 31 | InteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor) { 32 | super(interactionManipulations, interactionMonitor); 33 | 34 | undertowServer = Undertow.builder() 35 | .addHttpListener(port, "localhost") 36 | .setHandler(new BlockingHandler(exchange -> 37 | UndertowServirtiumServer.this.handleExchange(exchange, monitor))) 38 | .build(); 39 | } 40 | 41 | private void handleExchange(HttpServerExchange exchange, ServiceMonitor monitor) throws IOException { 42 | bumpInteractionNum(); 43 | 44 | String method = exchange.getRequestMethod().toString(); 45 | 46 | String uri = exchange.getRequestURI(); 47 | String url = exchange.getRequestURL(); 48 | 49 | String qs = exchange.getQueryString(); 50 | if (!qs.equals("")) { 51 | uri = uri + "?" + qs; 52 | url = url + "?" + qs; 53 | } 54 | 55 | // Fixes for Proxy server case - Jetty and Undertow are different here. 56 | if (uri.startsWith("https://") || uri.startsWith("http://")) { 57 | uri = uri.substring(url.indexOf("/",7)); 58 | } 59 | 60 | url = (url.startsWith("http://") || url.startsWith("https://")) ? url : "http://" + exchange.getHostAndPort() + uri; 61 | 62 | //String clientRequestBody = ""; 63 | List clientRequestHeaders = new ArrayList<>(); 64 | 65 | try { 66 | 67 | if (method.equals("CONNECT")) { 68 | exchange.getResponseSender().send("Servirtium does not support CONNECT yet"); 69 | exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, "text/plain"); 70 | exchange.setStatusCode(500); 71 | return; 72 | } 73 | 74 | InteractionMonitor.Interaction interaction = interactionMonitor.newInteraction(getInteractionNum(), getContext(), method, uri, url); 75 | 76 | monitor.interactionStarted(getInteractionNum(), interaction); 77 | 78 | final HeaderValues headerValues = exchange.getRequestHeaders().get(Headers.CONTENT_TYPE_STRING); 79 | String clientRequestContentType; 80 | if (headerValues == null) { 81 | clientRequestContentType = ""; 82 | } else { 83 | clientRequestContentType = headerValues.getFirst(); 84 | ; 85 | 86 | } 87 | 88 | // if (isText(contentType)) { 89 | // BufferedReader reader = baseRequest.getReader(); 90 | // clientRequestBody = reader.lines().collect(Collectors.joining("\n")); 91 | // } else { 92 | // ServletInputStream is = baseRequest.getInputStream(); 93 | // clientRequestBody = new byte[is.available()]; 94 | // 95 | // } 96 | // 97 | 98 | final String requestUrl = prepareHeadersAndBodyForService(exchange, method, url, clientRequestHeaders, 99 | interaction, clientRequestContentType, interactionManipulations); 100 | 101 | // INTERACTION 102 | ServiceResponse serviceResponse = interactionMonitor.getServiceResponseForRequest(method, requestUrl, 103 | interaction, useLowerCaseHeaders()); 104 | 105 | serviceResponse = processHeadersAndBodyBackFromService(interaction, serviceResponse, interactionManipulations); 106 | 107 | interaction.complete(); 108 | 109 | exchange.setStatusCode(serviceResponse.statusCode); 110 | 111 | for (String header : serviceResponse.headers) { 112 | int ix = header.indexOf(": "); 113 | String hdrKey = header.substring(0, ix); 114 | String hdrVal = header.substring(ix + 2); 115 | exchange.getResponseHeaders().add(new HttpString(hdrKey), hdrVal); 116 | } 117 | 118 | if (serviceResponse.contentType != null) { 119 | exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, serviceResponse.contentType); 120 | } 121 | 122 | if (serviceResponse.body instanceof String) { 123 | exchange.getResponseSender().send((String) serviceResponse.body); 124 | } else { 125 | exchange.getOutputStream().write((byte[]) serviceResponse.body); 126 | } 127 | 128 | monitor.interactionFinished(getInteractionNum(), method, url, getContext()); 129 | } catch (AssertionError assertionError) { 130 | failed = true; 131 | lastFailure = assertionError; 132 | exchange.setStatusCode(500); 133 | monitor.interactionFailed(getInteractionNum(), method, url, assertionError, getContext()); 134 | } catch (Throwable throwable) { 135 | failed = true; 136 | lastFailure = throwable; 137 | exchange.setStatusCode(500); 138 | monitor.unexpectedRequestError(throwable, getContext()); 139 | } finally { 140 | } 141 | } 142 | 143 | private ServiceResponse processHeadersAndBodyBackFromService(InteractionMonitor.Interaction interaction, 144 | ServiceResponse serviceResponse, 145 | InteractionManipulations interactionManipulations) { 146 | 147 | interaction.debugOriginalServiceResponseHeaders(serviceResponse.headers); 148 | 149 | ServiceResponse originalResponse = serviceResponse; 150 | 151 | List newHeaders = new ArrayList<>(); 152 | Collections.addAll(newHeaders, serviceResponse.headers); 153 | 154 | // Change of headers back from service 155 | 156 | ArrayList newHeadersTmp = new ArrayList<>(); 157 | for (int i = 0; i < newHeaders.size(); i++) { 158 | String headerBackFromService = newHeaders.get(i); 159 | String potentiallyChangedHeader = interactionManipulations.changeSingleHeaderReturnedBackFromRealServiceForRecording(i, headerBackFromService); 160 | if (potentiallyChangedHeader != null) { 161 | newHeadersTmp.add(potentiallyChangedHeader); 162 | } 163 | } 164 | 165 | newHeaders = newHeadersTmp; 166 | 167 | interactionManipulations.changeAnyHeadersReturnedBackFromRealServiceForRecording(newHeaders); 168 | 169 | if (serviceResponse.body instanceof String) { 170 | serviceResponse = serviceResponse.withRevisedBody( 171 | interactionManipulations.changeBodyReturnedBackFromRealServiceForRecording((String) serviceResponse.body)); 172 | // recreate response 173 | 174 | if (shouldHavePrettyPrintedTextBodies()) { 175 | String body = prettifyDocOrNot((String) serviceResponse.body); 176 | if (!body.equals(serviceResponse.body)) { 177 | // realResponse.headers 178 | serviceResponse = serviceResponse.withRevisedBody(body); 179 | } 180 | } 181 | } 182 | 183 | serviceResponse = serviceResponse.withRevisedHeaders(newHeaders.toArray(new String[0])); 184 | 185 | interaction.noteServiceResponseHeaders(serviceResponse.headers); 186 | 187 | serviceResponse = serviceResponse.withRevisedHeaders( 188 | interactionManipulations.changeHeadersForClientResponseAfterRecording(serviceResponse.headers)); 189 | 190 | interaction.debugClientsServiceResponseHeaders(serviceResponse.headers); 191 | 192 | interaction.debugOriginalServiceResponseBody(originalResponse.body, originalResponse.statusCode, originalResponse.contentType); 193 | 194 | interaction.noteServiceResponseBody(serviceResponse.body, serviceResponse.statusCode, serviceResponse.contentType); 195 | 196 | if (serviceResponse.body instanceof String) { 197 | final String b = (String) serviceResponse.body; 198 | 199 | serviceResponse = serviceResponse.withRevisedBody(interactionManipulations.changeBodyForClientResponseAfterRecording(b)); 200 | 201 | } 202 | 203 | interaction.debugClientsServiceResponseBody(originalResponse.body, originalResponse.statusCode, originalResponse.contentType); 204 | 205 | return serviceResponse; 206 | } 207 | 208 | private String prepareHeadersAndBodyForService(HttpServerExchange exchange, String method, String url, 209 | List clientRequestHeaders, InteractionMonitor.Interaction interaction, 210 | String clientRequestContentType, 211 | InteractionManipulations interactionManipulations) throws IOException { 212 | 213 | exchange.startBlocking(); 214 | InputStream is = exchange.getInputStream(); 215 | 216 | Object clientRequestBody = null; 217 | 218 | if (is.available() > 0) { 219 | 220 | if (isText(clientRequestContentType)) { 221 | clientRequestBody = null; 222 | String characterEncoding = exchange.getRequestCharset(); 223 | if (characterEncoding == null) { 224 | characterEncoding = "utf-8"; 225 | } 226 | try (Scanner scanner = new Scanner(is, characterEncoding)) { 227 | clientRequestBody = scanner.useDelimiter("\\A").next(); 228 | } 229 | if (shouldHavePrettyPrintedTextBodies() && clientRequestBody != null) { 230 | clientRequestBody = prettifyDocOrNot((String) clientRequestBody); 231 | } 232 | } else { 233 | byte[] targetArray = new byte[is.available()]; 234 | is.read(targetArray); 235 | clientRequestBody = targetArray; 236 | ; 237 | } 238 | } 239 | 240 | exchange.getRequestHeaders().forEach(header -> { 241 | String hdrName = header.getHeaderName().toString(); 242 | header.forEach(hdrVal -> { 243 | hdrVal = interactionManipulations.headerValueManipulation(hdrName, hdrVal); 244 | final String newHeader = (useLowerCaseHeaders() ? hdrName.toLowerCase() : hdrName) + ": " + hdrVal; 245 | clientRequestHeaders.add(newHeader); 246 | interactionManipulations.changeSingleHeaderForRequestToRealService(newHeader, clientRequestHeaders); 247 | }); 248 | }); 249 | 250 | 251 | if (clientRequestBody instanceof String) { 252 | clientRequestBody = interactionManipulations.changeBodyForRequestToRealService((String) clientRequestBody); 253 | } 254 | 255 | if (clientRequestBody == null) { 256 | clientRequestBody = ""; 257 | } 258 | 259 | interaction.noteClientRequestHeadersAndBody(interactionManipulations, clientRequestHeaders, clientRequestBody, clientRequestContentType, method, useLowerCaseHeaders()); 260 | 261 | return interactionManipulations.changeUrlForRequestToRealService(url); 262 | } 263 | 264 | public ServirtiumServer start() throws Exception { 265 | lastFailure = null; 266 | undertowServer.start(); 267 | return this; 268 | } 269 | 270 | public void stop() { 271 | try { 272 | interactionMonitor.finishedScript(getInteractionNum(), failed); // just in case 273 | } finally { 274 | undertowServer.stop(); 275 | } 276 | } 277 | 278 | public void finishedScript() { 279 | interactionMonitor.finishedScript(getInteractionNum(), failed); 280 | } 281 | 282 | @Override 283 | public Throwable getLastException() { 284 | return lastFailure; 285 | } 286 | 287 | 288 | } 289 | -------------------------------------------------------------------------------- /undertow/src/test/java/com/paulhammant/servirtium/undertow/SimpleGetCentricBinaryWithUndertowTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.undertow; 2 | 3 | import com.paulhammant.servirtium.InteractionMonitor; 4 | import com.paulhammant.servirtium.ServiceMonitor; 5 | import com.paulhammant.servirtium.ServirtiumServer; 6 | import com.paulhammant.servirtium.SimpleGetCentricBinaryTests; 7 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 8 | import org.junit.After; 9 | import org.junit.Test; 10 | 11 | public class SimpleGetCentricBinaryWithUndertowTests extends SimpleGetCentricBinaryTests { 12 | 13 | protected ServirtiumServer makeServirtiumServer(SimpleInteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor) { 14 | return new UndertowServirtiumServer(new ServiceMonitor.Console(), 15 | 8080, interactionManipulations, interactionMonitor); 16 | } 17 | 18 | @Override @After 19 | public void tearDown() { 20 | super.tearDown(); 21 | } 22 | 23 | @Override @Test 24 | public void canRecordABinaryGetFromApachesSubversionViaOkHttp() throws Exception { 25 | super.canRecordABinaryGetFromApachesSubversionViaOkHttp(); 26 | } 27 | 28 | @Override @Test 29 | public void canRecordAPngGetFromWikimedia() throws Exception { 30 | super.canRecordAPngGetFromWikimedia(); 31 | } 32 | 33 | @Override @Test 34 | public void canRecordASvgGetFromWikimedia() throws Exception { 35 | super.canRecordASvgGetFromWikimedia(); 36 | } 37 | 38 | @Override @Test 39 | public void canReplayABinaryGetFromApachesSubversion() throws Exception { 40 | super.canReplayABinaryGetFromApachesSubversion(); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /undertow/src/test/java/com/paulhammant/servirtium/undertow/SimpleGetCentricTextWithUndertowTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.undertow; 2 | 3 | import com.paulhammant.servirtium.InteractionMonitor; 4 | import com.paulhammant.servirtium.ServiceMonitor; 5 | import com.paulhammant.servirtium.ServirtiumServer; 6 | import com.paulhammant.servirtium.SimpleGetCentricTextTests; 7 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 8 | import org.junit.After; 9 | import org.junit.Ignore; 10 | import org.junit.Test; 11 | 12 | public class SimpleGetCentricTextWithUndertowTests extends SimpleGetCentricTextTests { 13 | 14 | public ServirtiumServer makeServirtiumServer(ServiceMonitor.Console serverMonitor, SimpleInteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor, int port) { 15 | return new UndertowServirtiumServer(serverMonitor, 16 | port, interactionManipulations, interactionMonitor); 17 | } 18 | 19 | @After 20 | public void tearDown() { 21 | super.tearDown(); 22 | } 23 | 24 | @Override @Test 25 | public void canRecordASimpleGetFromApachesSubversionViaOkHttp() throws Exception { 26 | super.canRecordASimpleGetFromApachesSubversionViaOkHttp(); 27 | } 28 | 29 | @Override @Test 30 | public void canRecordASequenceThenBarfInPlaybackWithClearMessagingIfUnplayedInteractions() throws Exception { 31 | super.canRecordASequenceThenBarfInPlaybackWithClearMessagingIfUnplayedInteractions(); 32 | } 33 | 34 | @Override @Test 35 | public void canRecordASimpleGetOfARedditJsonDocumentAndPrettify() throws Exception { 36 | super.canRecordASimpleGetOfARedditJsonDocumentAndPrettify(); 37 | } 38 | 39 | @Override @Test 40 | public void canRecordASimpleQueryStringGet() throws Exception { 41 | super.canRecordASimpleQueryStringGet(); 42 | } 43 | 44 | @Override @Test 45 | public void canPassThroughASimpleQueryStringGet() throws Exception { 46 | super.canPassThroughASimpleQueryStringGet(); 47 | } 48 | 49 | @Override @Test 50 | public void canPlaybackASimpleQueryStringGet() throws Exception { 51 | super.canPlaybackASimpleQueryStringGet(); 52 | } 53 | 54 | @Override @Test @Ignore 55 | public void canSupplyDebugInformationOnRedditJsonGet() throws Exception { 56 | super.canSupplyDebugInformationOnRedditJsonGet(); 57 | } 58 | 59 | @Override @Test 60 | public void worksThroughAproxyServer() throws Exception { 61 | super.worksThroughAproxyServer(); 62 | } 63 | 64 | @Override @Test @Ignore 65 | public void worksThroughAproxyServer2() throws Exception { 66 | super.worksThroughAproxyServer2(); 67 | } 68 | 69 | @Override @Test 70 | public void canRecordASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly() throws Exception { 71 | super.canRecordASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly(); 72 | } 73 | 74 | @Override @Test 75 | public void canReplayASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly() throws Exception { 76 | super.canReplayASimpleGetOfARedditJsonDocumentAndPrettifyAndRedactPartOfTheRecordingOnly(); 77 | } 78 | 79 | @Override @Test 80 | public void canReplayWithAReplacementInTheURLtoo() throws Exception { 81 | super.canReplayWithAReplacementInTheURLtoo(); 82 | } 83 | 84 | @Override @Test 85 | public void canReplayASimpleGetFromApachesSubversion() throws Exception { 86 | super.canReplayASimpleGetFromApachesSubversion(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /undertow/src/test/java/com/paulhammant/servirtium/undertow/SimplePostCentricWithUndertowTests.java: -------------------------------------------------------------------------------- 1 | package com.paulhammant.servirtium.undertow; 2 | 3 | import com.paulhammant.servirtium.InteractionMonitor; 4 | import com.paulhammant.servirtium.ServiceMonitor; 5 | import com.paulhammant.servirtium.ServirtiumServer; 6 | import com.paulhammant.servirtium.SimpleInteractionManipulations; 7 | import com.paulhammant.servirtium.SimplePostCentricTests; 8 | import org.junit.After; 9 | import org.junit.Test; 10 | 11 | public class SimplePostCentricWithUndertowTests extends SimplePostCentricTests { 12 | 13 | public ServirtiumServer makeServirtiumServer(SimpleInteractionManipulations interactionManipulations, InteractionMonitor interactionMonitor) { 14 | return new UndertowServirtiumServer(new ServiceMonitor.Console(), 15 | 8080, interactionManipulations, interactionMonitor); 16 | } 17 | 18 | @Override @After 19 | public void tearDown() { 20 | super.tearDown(); 21 | } 22 | 23 | @Override @Test 24 | public void canRecordASimplePostToPostmanEchoViaOkHttp() throws Exception { 25 | super.canRecordASimplePostToPostmanEchoViaOkHttp(); 26 | } 27 | 28 | @Override @Test 29 | public void canRecordABase64PostToPostmanEchoViaOkHttp() throws Exception { 30 | super.canRecordABase64PostToPostmanEchoViaOkHttp(); 31 | } 32 | 33 | @Override @Test 34 | public void canReplayASimplePostToPostmanEcho() throws Exception { 35 | super.canReplayASimplePostToPostmanEcho(); 36 | } 37 | 38 | @Override @Test 39 | public void canReplayABase64PostToPostmanEcho() throws Exception { 40 | super.canReplayABase64PostToPostmanEcho(); 41 | } 42 | 43 | @Override @Test 44 | public void canRecordABinaryPost() throws Exception { 45 | super.canRecordABinaryPost(); 46 | } 47 | 48 | @Override @Test 49 | public void canRecordABinaryPut() throws Exception { 50 | super.canRecordABinaryPut(); 51 | } 52 | } 53 | --------------------------------------------------------------------------------