├── .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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------