├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── HTTP_CLIENT.md ├── HTTP_CLIENT_MATCHERS.md ├── LICENSE ├── MARSHALLER.md ├── README.md ├── README.temp.md ├── WEBSERVER.md ├── WEBSERVER_MATCHERS.md ├── build.sbt ├── contract-tests ├── http-testkit-contract-tests │ └── src │ │ └── test │ │ ├── scala │ │ └── com │ │ │ └── wix │ │ │ └── e2e │ │ │ └── http │ │ │ ├── client │ │ │ ├── BlockingHttpClientContractTest.scala │ │ │ └── NonBlockingHttpClientContractTest.scala │ │ │ ├── drivers │ │ │ ├── HttpClientTestSupport.scala │ │ │ └── StubWebServerProvider.scala │ │ │ ├── info │ │ │ └── VersionConsistencyTest.scala │ │ │ └── server │ │ │ └── WebServerContractTest.scala │ │ ├── scala_2.13+ │ │ └── com │ │ │ └── wix │ │ │ └── e2e │ │ │ └── http │ │ │ └── info │ │ │ └── VersionConsistencyTestSupport.scala │ │ └── scala_2.13- │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── info │ │ └── VersionConsistencyTestSupport.scala └── marshaller-contract-tests │ ├── http-testkit-contract-tests-custom-marshaller │ └── src │ │ └── test │ │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ ├── drivers │ │ └── MarshallerTestSupport.scala │ │ └── marshaller │ │ └── HttpClientCustomMarshallerContractTest.scala │ ├── http-testkit-contract-tests-dual-marshallers │ └── src │ │ └── test │ │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── json │ │ └── DualMarshallersTest.scala │ ├── http-testkit-contract-tests-malformed-marshaller │ └── src │ │ └── test │ │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── marshaller │ │ └── HttpClientMalformedMarshallerContractTest.scala │ └── http-testkit-contract-tests-no-custom-marshaller │ └── src │ └── test │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ ├── drivers │ └── MarshallerTestSupport.scala │ └── marshaller │ └── HttpClientNoCustomMarshallerContractTest.scala ├── examples └── src │ └── main │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ └── examples │ └── MediaServer.scala ├── http-testkit-client └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── client │ │ ├── HttpClientSupport.scala │ │ ├── extractors │ │ ├── HttpMessageExtractors.scala │ │ └── package.scala │ │ ├── internals │ │ ├── RequestManager.scala │ │ └── package.scala │ │ └── transformers │ │ ├── HttpClientTransformers.scala │ │ ├── internals │ │ └── request.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ ├── client │ ├── HttpClientTransformersTest.scala │ ├── drivers │ │ └── PathBuilderTestSupport.scala │ ├── extractors │ │ └── HttpMessageExtractorsTest.scala │ └── internals │ │ └── PathBuilderTest.scala │ └── drivers │ └── HttpClientTransformersTestSupport.scala ├── http-testkit-core └── src │ └── main │ ├── scala │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ ├── BaseUri.scala │ │ ├── WixHttpTestkitResources.scala │ │ ├── api │ │ ├── Marshaller.scala │ │ └── api.scala │ │ ├── config │ │ └── Config.scala │ │ ├── exceptions │ │ └── exceptions.scala │ │ ├── info │ │ └── package.scala │ │ ├── package.scala │ │ └── utils │ │ └── package.scala │ ├── scala_2.13+ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── api │ │ └── ExternalMarshaller.scala │ └── scala_2.13- │ └── com │ └── wix │ └── e2e │ └── http │ └── api │ └── ExternalMarshaller.scala ├── http-testkit-marshaller-jackson └── src │ ├── main │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── json │ │ └── JsonJacksonMarshaller.scala │ └── test │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ └── json │ └── JsonJacksonMarshallerTest.scala ├── http-testkit-scala-test └── src │ ├── main │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── matchers │ │ ├── RequestMatchers.scala │ │ ├── ResponseMatchers.scala │ │ ├── internal │ │ ├── matchers.scala │ │ └── server.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ └── matchers │ ├── drivers │ ├── HttpMessageTestSupport.scala │ ├── MarshallerTestSupport.scala │ ├── MatchersTestSupport.scala │ └── RequestRecordTestSupport.scala │ └── internal │ ├── RequestBodyMatchersTest.scala │ ├── RequestContentTypeMatchersTest.scala │ ├── RequestCookiesMatchersTest.scala │ ├── RequestHeadersMatchersTest.scala │ ├── RequestMethodMatchersTest.scala │ ├── RequestRecorderMatchersTest.scala │ ├── RequestUrlMatchersTest.scala │ ├── ResponseBodyAndStatusMatchersTest.scala │ ├── ResponseBodyMatchersTest.scala │ ├── ResponseContentLengthMatchersTest.scala │ ├── ResponseContentTypeMatchersTest.scala │ ├── ResponseCookiesMatchersTest.scala │ ├── ResponseHeadersMatchersTest.scala │ ├── ResponseStatusAndHeaderMatchersTest.scala │ ├── ResponseStatusMatchersTest.scala │ └── ResponseTransferEncodingMatchersTest.scala ├── http-testkit-server └── src │ └── main │ ├── resources │ └── reference.conf │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ └── server │ ├── builders │ └── builders.scala │ └── internals │ ├── AkkaHttpMockWebServer.scala │ └── StubAkkaHttpMockWebServer.scala ├── http-testkit-specs2 └── src │ ├── main │ └── scala │ │ └── com │ │ └── wix │ │ └── e2e │ │ └── http │ │ └── matchers │ │ ├── RequestMatchers.scala │ │ ├── ResponseMatchers.scala │ │ ├── internal │ │ ├── HeaderMatching.scala │ │ ├── matchers.scala │ │ └── server.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ └── matchers │ ├── drivers │ ├── HttpMessageTestSupport.scala │ ├── MarshallerTestSupport.scala │ ├── MatchersTestSupport.scala │ └── RequestRecorderTestSupport.scala │ └── internal │ ├── RequestBodyMatchersTest.scala │ ├── RequestContentTypeMatchersTest.scala │ ├── RequestCookiesMatchersTest.scala │ ├── RequestHeadersMatchersTest.scala │ ├── RequestMethodMatchersTest.scala │ ├── RequestRecorderMatchersTest.scala │ ├── RequestUrlMatchersTest.scala │ ├── ResponseBodyAndStatusMatchersTest.scala │ ├── ResponseBodyMatchersTest.scala │ ├── ResponseContentLengthMatchersTest.scala │ ├── ResponseContentTypeMatchersTest.scala │ ├── ResponseCookiesMatchersTest.scala │ ├── ResponseHeadersMatchersTest.scala │ ├── ResponseStatusAndHeaderMatchersTest.scala │ ├── ResponseStatusMatchersTest.scala │ └── ResponseTransferEncodingMatchersTest.scala ├── http-testkit-test-commons └── src │ └── main │ └── scala │ └── com │ └── wix │ └── test │ └── random │ └── package.scala ├── http-testkit └── src │ └── main │ └── scala │ └── com │ └── wix │ └── e2e │ └── http │ ├── client │ ├── async │ │ └── package.scala │ └── sync │ │ └── package.scala │ └── server │ └── WebServerFactory.scala ├── project ├── build.properties ├── compiler.scala ├── depends.scala ├── pgp.sbt ├── plugins.sbt ├── release.sbt └── sonatype.sbt └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | ########################### 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .idea/ 7 | cache 8 | 9 | 10 | 11 | # Linux 12 | ##################### 13 | #.* 14 | !.gitignore 15 | *~ 16 | 17 | 18 | # Java 19 | ###################### 20 | 21 | *.class 22 | 23 | # Package Files # 24 | *.jar 25 | *.war 26 | *.ear 27 | 28 | target/ 29 | 30 | # Scala 31 | ###################### 32 | 33 | *.sc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - "2.12.15" 5 | - "2.13.7" 6 | 7 | sudo: false 8 | 9 | script: 10 | - sbt "++$TRAVIS_SCALA_VERSION clean" "++$TRAVIS_SCALA_VERSION test" 11 | 12 | after_success: 13 | - "[[ \"$TRAVIS_REPO_SLUG\" == \"wix/wix-http-testkit\" ]] && [[ \"$TRAVIS_BRANCH\" == \"master\" ]] && { sbt \"++$TRAVIS_SCALA_VERSION publish\"; };" 14 | 15 | jdk: 16 | - oraclejdk8 17 | 18 | env: 19 | global: 20 | - secure: naUSlGUX60zmIJv3+Vevk0+2UppS091KywKn1Dzdc9+cWoX1D2hqbFclRgDaekKrN79UsT/PxUOYCK87Xm9MHAd+jwlAnRxdaSpVKYjButKbZOa+SnLyrv/kKt50rJouOPJ1V1aQ0c4NxCYQrmevQDG/bqOoPucoqUpo/MTizbeQ1mvxz0441RR3it1tBaL6JY4XtH9iJylGqhVVlguH8JLssJrGhUqjQo6yg2Jjg8m17oxbZ5kRlnrxWJKe8X0k5w9QgZIgOLBGZcppSTqE1BmR0U18Flpm+y8NeTpfziEZ39SQv0Yg9fu0mJDi0Q2327X+2fqXIPC2GHVelpLz7d5w1QgPQQMeqJcuMEqe9hO4HeFP+B9Nk6gBfXcWqXklgAc9ipmm/4sGvYMMW1oemmt5pHAoIpVv5R2ubW1coOrKRv3Vacl2uPU1tLp0NL2oI68YM2UYSAmTI2zgpiyJ191QDGMXAqocTBOuNDA68F3CJylZg1emyxcrNzjSdXF7jQqUmJjCbcyyv0OEYE9ORyw1nO++vGBbRs9jUC8ZxDKrfYxJyIk1J8a+idjLAvuNWqshD+QYT/J2tTVjv8XRDwi/OoQDXFCwg0CK7sVYqtQeSn/eN+5oNVlqrhq+0Chl01y+CDKCzGSKSOXmAf9I1nnGA8usr3rUfBrC/EwJGWw= 21 | - secure: NLKwnydKd/aXoJAwxvKGS0Jp+4LhZubV7clrqnakEdzYBXox+d8GiEp1ev5za7weo4Z2hH+CZcbJY7/JcPpNNIsLayBDyaTQso5HrUxBj5GEc1GNF0BTS547boDMGqzrtayxFQ1sztnYbdh0mk+sM7Dba28r/ZDOMDiWKyM+PUbT8mXmOWmN2nNyw88bpGSB3DWKj5Pv4t3tBEKgWfpgN8365xNfvbQfu8t8KEGvb0IsMdlUwuUyAqK7S+WQLWxjk2VeI9sfbsCF9ICs3nbN7OZnQ3VKqnHFK1BDnNag+eBGSqSpGwxoN4M9kTCkofVXixC7ymtbEQSzM03xSNQIWa2JHq3qUZxUwEe+3nm9Pyi4s1PkZKhyal2+DFH6H3byQQOYuArYee8N9KeJKeHLe9to539XYI/HLA/rGdD41PwLnXV8IytT3Lt3AFqQ66W2UtN2fikpN99PbmcIciAlvOxEAI+cY6jZtDI+QbAwDBez+5IidDsSdvKwW3PNLSLDNv1WYdVDvyBJA/c4R9rM8BsWG77JA0dCMntO7pt3jN/j7qXFzAyU74BzNrVlpZsPoMTxgYqQRPhW+XapN0XFjM0bVwn2G4J0WDBY103vzAUY0pxebrrzPu85k+JsVHvoOkTHdrocmYHRTut2YA+m611Kv1u/8ArnhMHfOnyE9ME= 22 | 23 | cache: 24 | directories: 25 | - "$HOME/.ivy2/cache" 26 | - "$HOME/.sbt/boot/" 27 | - "$HOME/.sbt/launchers/" 28 | 29 | before_cache: 30 | - find $HOME/.sbt -name "*.lock" | xargs rm 31 | - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm 32 | 33 | dist: trusty -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at noamal@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /HTTP_CLIENT.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | A sane DSL to test REST API's. 5 | 6 | There are two variations of the client that can be used: 7 | * __Blocking__ 8 | * __Non-Blocking__ 9 | 10 | 11 | ## Create HTTP Client 12 | 13 | #### Import the DSL: 14 | 15 | package import: 16 | ```scala 17 | 18 | // blocking implementation 19 | import com.wix.e2e.http.client.sync._ 20 | 21 | // or non blocking implementation 22 | import com.wix.e2e.http.client.async._ 23 | ``` 24 | 25 | Other Options 26 | ```scala 27 | //Import Object 28 | // blocking implementation 29 | import com.wix.e2e.http.client.BlockingHttpClientSupport 30 | 31 | // or non blocking implementation 32 | import com.wix.e2e.http.client.NonBlockingHttpClientSupport 33 | 34 | // Or add mixin trait to call site 35 | // blocking implementation 36 | class MyClass extends com.wix.e2e.http.client.BlockingHttpClientSupport 37 | 38 | // or non blocking implementation 39 | class MyClass extends com.wix.e2e.http.client.NonBlockingHttpClientSupport 40 | 41 | ``` 42 | 43 | #### Issuing New Request 44 | ```scala 45 | 46 | val somePort = 99123 /// any port 47 | implicit val baseUri = BaseUri(port = somePort) 48 | 49 | get("/somePath") 50 | post("/anotherPath") 51 | // suported method: get, post, put, patch, delete, options, head, trace 52 | ``` 53 | 54 | ### Customizing Request 55 | 56 | Each request can be easily customized with a set of basic transformers allowing all basic functionality (add parameters, headers, cookies and request body) 57 | ```scala 58 | 59 | get("/somePath", 60 | but = withParam("param1" -> "value") 61 | and header("header" -> "value") 62 | and withCookie("cookie" -> "cookieValue")) 63 | 64 | // post plain text data to api 65 | post("/somePath", 66 | but = withPayload("Hi There !!!")) 67 | 68 | // or post entity that would be marshalled using testkit marshaller (or custom user marshaller) 69 | case class SomeCaseClass(str: String) 70 | 71 | // request will automatically be marshalled to json 72 | put("/somePath", but = withPayload(SomeCaseClass("Hi There !!!"))) 73 | ``` 74 | 75 | Handlers can be also be defined by developer, it can use existing transformers or to implement transformers from scratch 76 | ```scala 77 | 78 | def withSiteId(id: String): RequestTransformer = withParam("site-id" -> id) and withHeader("x-user-custom" -> "whatever") 79 | 80 | get("/path", but = withSiteId("someId")) 81 | 82 | ``` 83 | 84 | ### Validate Responses 85 | To validate HTTP response use the included [Specs2 Matcher Suite](./HTTP_CLIENT_MATCHERS.md). 86 | 87 | ### Json Marshaller 88 | 89 | Testkit comes out of the box with a default [Jackson](https://github.com/FasterXML/jackson) json marshaller preloaded with several commonly used modules, to define your own marshaller see [Custom Marshaller](./MARSHALLER.md). 90 | -------------------------------------------------------------------------------- /HTTP_CLIENT_MATCHERS.md: -------------------------------------------------------------------------------- 1 | # Response Matchers 2 | 3 | 4 | 5 | Import the matcher suite 6 | 7 | ```scala 8 | import com.wix.e2e.http.matchers.ResponseMatchers._ 9 | 10 | ``` 11 | 12 | You can also use trait mixin 13 | 14 | ```scala 15 | class MyTestClass extends SpecWithJUnit with ResponseMatchers 16 | ``` 17 | 18 | 19 | ### Status Matchers 20 | 21 | All Http response statuses can be matched 22 | 23 | ```scala 24 | val response = get("/somePath") 25 | 26 | response must beSuccessful 27 | response must beNotFound 28 | // more statuses are available 29 | ``` 30 | 31 | ### Body Matchers 32 | 33 | It is possible to match response body in several ways 34 | ```scala 35 | //Match exact content 36 | response must haveBodyWith("someBody") 37 | 38 | // compose matchers 39 | response must haveBodyThat(must = contain("someBody")) 40 | ``` 41 | 42 | Unmarshal and match 43 | 44 | ```scala 45 | case class SomeCaseClass(s: String) 46 | 47 | response must haveBodyWith(SomeCaseClass("some string")) 48 | 49 | // or compose matchers 50 | response must haveBodyThat(must = be_===( SomeCaseClass("some string") )) 51 | ``` 52 | 53 | All responses are unmarshalled with default or custom marshaller, for more info see [Marshaller Documentation](./MARSHALLER.md) 54 | 55 | 56 | ### Headers Matchers 57 | 58 | Check if response contain headers 59 | 60 | ```scala 61 | response must haveAnyHeadersOf("h1" -> "v1", "h2" -> "v2") // at least one is found 62 | response must haveAllHeadersOf("h1" -> "v1", "h2" -> "v2") // all exists on response 63 | response must haveTheSameHeadersAs("h1" -> "v1", "h2" -> "v2") // same list of headers (no more, no less) 64 | 65 | // compose 66 | response must haveAnyHeaderThat(must = contain("value"), withHeaderName = "header" ) 67 | 68 | ``` 69 | 70 | Check if response contain headers cookies 71 | ```scala 72 | response must receivedCookieWith(name = "cookie name") 73 | 74 | response must receivedCookieThat(must = be_===( HttpCookie("cookie name", "cookie value") )) 75 | 76 | ``` 77 | 78 | ### Common Matchers 79 | ```scala 80 | 81 | // successful response with body 82 | response must beSuccessfulWith( "some content" ) 83 | response must beSuccessfulWithEntityThat(must = be_===( SomeCaseClass("some content" ) ) ) 84 | 85 | // more matchers exists 86 | 87 | ``` 88 | 89 | ## Create Your Own 90 | 91 | You can mix and match and create your own [Specs2 matchers](http://etorreborre.github.io/specs2/), if there are more commonly used matchers you are using and think that should be included do not hasitate to open an issue or create a PR. 92 | 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Wix.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MARSHALLER.md: -------------------------------------------------------------------------------- 1 | # Json Marshaller 2 | 3 | Testkit comes out of the box with a default [Jackson](https://github.com/FasterXML/jackson) json marshaller preloaded with ([Scala Module](https://github.com/FasterXML/jackson-module-scala), [JDK8](https://github.com/FasterXML/jackson-datatype-jdk8), [Java Time](https://github.com/FasterXML/jackson-datatype-jsr310), [JodaTime](https://github.com/FasterXML/jackson-datatype-joda)) 4 | 5 | It can also allow you to create your own custom marshaller: 6 | 7 | ```scala 8 | 9 | val myMarshaller = new com.wix.e2e.http.api.Marshaller { 10 | def unmarshall[T : Manifest](jsonStr: String): T = { /*your code here*/ } 11 | def marshall[T](t: T): String = { /*your code here*/ } 12 | } 13 | 14 | 15 | // on call site, define implicit marshaller 16 | implicit val customMarshaller = myMarshaller 17 | 18 | put("/somePath", but = withPayload(SomeCaseClass("Hi There !!!"))) 19 | 20 | ``` 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wix/wix-http-testkit.svg?branch=master)](https://travis-ci.org/wix/wix-http-testkit) 2 | 3 | # HTTP Testkit 4 | 5 | Overview 6 | ======== 7 | 8 | Wix Http Testkit is a library that will address many of the End-to-end testing concerns you might encounter. 9 | 10 | Wix HTTP Testkit aims to be: 11 | * __Simple__ Testing REST services or starting mock/stub servers is very simple and requires very few lines of code. 12 | * __Fast__ Leveraging [Akka-Http](https://github.com/akka/akka-http) infrastructure, starting servers takes milliseconds. 13 | * __Integrated__: Other than providing a set of DSLs to support composing and executing REST HTTP calls and creating and configuring web servers, it also contains out of the box matcher libraries for [Specs2](http://wix.github.io/accord/specs2.html) to easily validate each aspect of the tested flow. 14 | 15 | 16 | Getting Started 17 | =============== 18 | ### Testing Client 19 | 20 | Import DSL 21 | ```scala 22 | import com.wix.e2e.http.client.sync._ 23 | ``` 24 | 25 | Issue Call 26 | ```scala 27 | val somePort = 99123 /// any port 28 | implicit val baseUri = BaseUri(port = somePort) 29 | 30 | 31 | get("/somePath", 32 | but = withParam("param1" -> "value") 33 | and header("header" -> "value") 34 | and withCookie("cookie" -> "cookieValue")) 35 | ``` 36 | 37 | Use Specs2 Matcher suite to match response 38 | ```scala 39 | import com.wix.e2e.http.matchers.ResponseMatchers._ 40 | 41 | put("/anotherPath") must haveBodyWith("someBody") 42 | ``` 43 | 44 | For more info see [Http Client Documentation](./HTTP_CLIENT.md) and [Response Matchers Suite](./HTTP_CLIENT_MATCHERS.md). 45 | 46 | 47 | ### Web Servers 48 | 49 | Import Factory 50 | ```scala 51 | import com.wix.e2e.http.server.WebServerFactory._ 52 | ``` 53 | 54 | Run an easily programmable web server 55 | 56 | ```scala 57 | val handler: RequestHandler = { case r: HttpRequest => HttpResponse() } 58 | val server = aMockWebServerWith(handler).build 59 | .start() 60 | ``` 61 | 62 | Or run a programmable that will record all incoming messages 63 | 64 | ```scala 65 | val server = aStubWebServer.build 66 | .start() 67 | 68 | ``` 69 | 70 | Match against recorded requests 71 | 72 | ```scala 73 | 74 | import com.wix.e2e.http.matchers.RequestMatchers._ 75 | 76 | 77 | server must receivedAnyRequestThat(must = beGet) 78 | ``` 79 | 80 | For more info see [Web Server Documentation](./WEBSERVER.md) and [Request Matchers Suite](./WEBSERVER_MATCHERS.md). 81 | 82 | 83 | 84 | ## Usage 85 | 86 | HTTP-testkit version '0.1.25' is available on Maven Central Repository. Scala versions 2.11.x, 2.12.x and 2.13.x are supported. 87 | 88 | ### SBT 89 | Simply add the *wix-http-testkit* module to your build settings: 90 | 91 | ```sbt 92 | libraryDependencies += "com.wix" %% "http-testkit" % "0.1.25" 93 | ``` 94 | ### Maven 95 | 96 | ```xml 97 | 98 | 99 | com.wix 100 | http-testkit_${scala.dependencies.version} 101 | 0.1.25 102 | 103 | 104 | 105 | ``` 106 | 107 | # Documentation 108 | 109 | * __Rest Client__: a declarative REST client [Documentation](./HTTP_CLIENT.md). 110 | * __Simplicator Web Servers__: Easily configurable web servers [Documentation](./WEBSERVER.md). 111 | * __Specs2 Matchers Suite__: Comprehensive matcher suites [Response Matchers](./HTTP_CLIENT_MATCHERS.md) and [Request Matchers](./WEBSERVER_MATCHERS.md). 112 | 113 | # Contribute 114 | 115 | Ideas and feature requests welcome! Report an [issue](https://github.com/wix/wix-http-testkit/issues/) or contact the [maintainer](https://github.com/noam-almog) directly. 116 | 117 | 118 | ## License 119 | 120 | This project is licensed under [MIT License](./LICENSE.md). 121 | -------------------------------------------------------------------------------- /README.temp.md: -------------------------------------------------------------------------------- 1 | # Wix HTTP Testkit 2 | 3 | ##Mock Web Server 4 | Server created in order to simulate a simple web server with specific behavior. 5 | The default behavior is to answer requests, if behavior is not defined the server will return 404 Not found by default. 6 | 7 | Create server 8 | ``` 9 | // you must define at least one handler 10 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 11 | val someHandler: RequestHandler = { case r: HttpRequest => HttpResponse(entity = "Hello!") } 12 | 13 | 14 | import com.wix.e2e.http.server.WebServerFactory._ 15 | val server = aMockWebServerWith(someHandler).build 16 | 17 | // server will start on an open port 18 | server.start() 19 | ``` 20 | 21 | Optionally specify port if you want a server on a specific port 22 | ``` 23 | val somePort = 6667 24 | aMockWebServerWith(someHandler).onPort(somePort) 25 | .build 26 | ``` 27 | 28 | ##Stub Web Server 29 | Server created in order to respond to requests and record all incoming traffic. 30 | The default behavior would be to respond with 200 ok. 31 | You should use this server if the tested system you are simulating does not return a valid output that will be a part of the server flow and can be validated later on. 32 | 33 | Create server 34 | ``` 35 | // you must define at least one handler 36 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 37 | val someHandler: RequestHandler = { case r: HttpRequest => HttpResponse(entity = "Hello!") } 38 | 39 | 40 | import com.wix.e2e.http.server.WebServerFactory._ 41 | val server = aStubWebServer.build 42 | 43 | // server will start on an open port 44 | server.start() 45 | ``` 46 | 47 | On test flow you can check that requests were recieved on the tested system. 48 | ``` 49 | val server = aStubWebServer.build 50 | 51 | get("/somePath")(server.baseUri) 52 | 53 | server.recordedRequests must contain( beGetRequestWith(path = somePath) ) 54 | ``` 55 | 56 | Optional customizations 57 | ``` 58 | val server = aStubWebServer 59 | 60 | // set explicit port 61 | server.onPort(somePort) 62 | 63 | // optinally add handlers 64 | server.addHandler(someHandler) 65 | server.addHandlers(anotherHandler, yetAnotherHandler) 66 | 67 | server.start() 68 | ``` 69 | -------------------------------------------------------------------------------- /WEBSERVER.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | A simple DSL to configure and define a Web Server 4 | 5 | There are two variations of the server: 6 | * __Mock Server__: A Programmable REST server, allows to define custom behavior on each REST API, responds with *404 Not Found* on undefinded APIs. 7 | * __Stub Server__: Responds *200 OK* on all REST APIs and records all incoming requests. 8 | 9 | 10 | ## Create Web Server 11 | 12 | Import Factory 13 | ```scala 14 | import com.wix.e2e.http.server.WebServerFactory._ 15 | ``` 16 | 17 | ### Mock Server 18 | 19 | The mock server is useful for cases in which the server is a part of an end-to-end transaction in which it is expected to get some inputs and reply with a specific output that can later on be validated from the outside. 20 | 21 | ```scala 22 | // start a server on a dynamic open port 23 | val handler: RequestHandler = { case r: HttpRequest => HttpResponse() } 24 | val server = aMockWebServerWith(handler).build 25 | .start() 26 | 27 | // start on a custom port 28 | val somePort = 11111 29 | val serverOnCustomPort = aMockWebServerWith(handler).onPort(somePort) 30 | .build 31 | .start() 32 | 33 | ``` 34 | 35 | To program our mock server we will need to define handlers. A Handler is a function that receives a request and returns some response. 36 | 37 | For example, a server that listens to requests on `/somePath` and responds with `OK!!!`. 38 | A server can handle one or more handlers and it will use the first handler that is defined for the incoming request. 39 | 40 | ```scala 41 | val okHandler = { case r: HttpRequest if r.uri.path.toString.endsWith("somePath") => HttpResponse(entity = "OK!!!") } 42 | ``` 43 | 44 | ### Stub Server 45 | 46 | The stub server will record all incoming requests and respond with a 200OK to all requests. 47 | You will probably need this simple implementation in case you have an external server being called from your service while the output from this service is not being used in the transaction or simply not accessible from the outside. 48 | For example: you are triggering a REST API that sends a mail. 49 | 50 | Create the server 51 | ```scala 52 | // start a server on a dynamic open port 53 | val server = aStubWebServer.build 54 | .start() 55 | 56 | // use custom port 57 | val somePort = 11111 58 | val serverOnCustomPort = aStubWebServer.onPort(somePort) 59 | .build 60 | .start() 61 | ``` 62 | 63 | A Stub server can, but is not required to, have custom handlers (the same as the mock server) 64 | 65 | ```scala 66 | val someHandler = // create your own 67 | val anotherHandler = // create your own 68 | 69 | val server = aStubWebServer.addHandler(someHandler) // add one 70 | .addHandlers(someHandler, anotherHandler) // add more than one handler 71 | .build 72 | .start() 73 | 74 | 75 | ``` 76 | #### Editing handlers in test 77 | You can update the handlers by calling : 78 | ```scala 79 | val newHandler = // create your own 80 | mockWebServer.replaceWith(newHandler) 81 | ``` 82 | This will reset the handlers and set it to the new one 83 | Or you can add handlers to the existing ones : 84 | ```scala 85 | val newHandler = // create your own 86 | mockWebServer.appendAll(newHandler) 87 | ``` 88 | 89 | #### Recorded Requests 90 | 91 | To view the recorded requests just access the `recordedRequests` member: 92 | ```scala 93 | 94 | val server = // start server 95 | 96 | val requests = server.recordedRequests 97 | 98 | // you can also reset the recorded requests between tests 99 | server.clearRecordedRequests() 100 | 101 | ``` 102 | 103 | To validate incoming requests use the included [Specs2 Matcher Suite](./WEBSERVER_MATCHERS.md). 104 | -------------------------------------------------------------------------------- /WEBSERVER_MATCHERS.md: -------------------------------------------------------------------------------- 1 | # Stub Servers Request Matchers 2 | 3 | 4 | Import the matcher suite 5 | 6 | ```scala 7 | import com.wix.e2e.http.matchers.RequestMatchers._ 8 | 9 | ``` 10 | 11 | You can also use trait mixin 12 | 13 | ```scala 14 | class MyTestClass extends SpecWithJUnit with RequestMatchers 15 | ``` 16 | 17 | ### Validate Recorded Requests 18 | 19 | ```scala 20 | val server = aStubWebServer.build 21 | .start() 22 | 23 | // match concrete requests 24 | val request = HttpRequest(HttpMethods.GET) 25 | server must receivedAnyOf(request) 26 | 27 | // compose matchers 28 | 29 | server must receivedAnyRequestThat(must = beGet) 30 | ``` 31 | 32 | # Request Matchers 33 | 34 | ### Method Matchers 35 | 36 | All Http request statuses can be matched 37 | 38 | ```scala 39 | val request = // server.recordedRequests.head 40 | 41 | request must beGet 42 | request must bePost 43 | // more method matchers are available 44 | ``` 45 | 46 | ### Request URL Matchers 47 | 48 | Match against request path or parameters 49 | ```scala 50 | // request path 51 | request must havePath("/somePath") 52 | request must havePathThat(must = contain("/somePath")) 53 | 54 | // request parameters 55 | request must haveAnyParamOf("param1" -> "value1", "param2" -> "value2") 56 | request must haveAnyParamThat(must = be_===( "value1" ), withParamName = "param1") 57 | ``` 58 | 59 | 60 | 61 | ### Body Matchers 62 | 63 | It is possible to match request body in several ways 64 | ```scala 65 | //Match exact content 66 | request must haveBodyWith(bodyContent = "someBody") 67 | 68 | // compose matchers 69 | request must haveBodyThat(must = contain("someBody")) 70 | ``` 71 | 72 | Unmarshal and match 73 | 74 | ```scala 75 | case class SomeCaseClass(s: String) 76 | 77 | request must haveBodyWith(entity = SomeCaseClass("some string")) 78 | 79 | // or compose matchers 80 | request must haveBodyEntityThat(must = be_===( SomeCaseClass("some string") )) 81 | ``` 82 | 83 | All requests are unmarshalled with default or custom marshaller, for more info see [Marshaller Documentation](./MARSHALLER.md) 84 | 85 | 86 | ### Headers Matchers 87 | 88 | Check if request contain headers 89 | 90 | ```scala 91 | request must haveAnyHeadersOf("h1" -> "v1", "h2" -> "v2") // at least one is found 92 | request must haveAllHeadersOf("h1" -> "v1", "h2" -> "v2") // all exists on request 93 | request must haveTheSameHeadersAs("h1" -> "v1", "h2" -> "v2") // same list of headers (no more, no less) 94 | 95 | // compose 96 | request must haveAnyHeaderThat(must = contain("value"), withHeaderName = "header" ) 97 | 98 | ``` 99 | 100 | Check if request contain headers cookies 101 | ```scala 102 | request must receivedCookieWith(name = "cookie name") 103 | 104 | request must receivedCookieThat(must = be_===( HttpCookie("cookie name", "cookie value") )) 105 | 106 | ``` 107 | 108 | ## Create Your Own 109 | 110 | You can mix and match and create your own [Specs2 matchers](http://etorreborre.github.io/specs2/), if there are more commonly used matchers you are using and think that should be included do not hasitate to open an issue or create a PR. 111 | 112 | -------------------------------------------------------------------------------- /contract-tests/http-testkit-contract-tests/src/test/scala/com/wix/e2e/http/drivers/HttpClientTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.drivers 2 | 3 | import java.io.DataOutputStream 4 | import java.net.{HttpURLConnection, URL} 5 | 6 | import akka.http.scaladsl.model.HttpMethods.GET 7 | import akka.http.scaladsl.model._ 8 | import akka.http.scaladsl.model.headers.`Transfer-Encoding` 9 | import akka.stream.scaladsl.Source 10 | import com.wix.e2e.http.client.extractors._ 11 | import com.wix.e2e.http.info.HttpTestkitVersion 12 | import com.wix.e2e.http.matchers.{RequestMatcher, ResponseMatcher} 13 | import com.wix.e2e.http.{BaseUri, HttpRequest, RequestHandler} 14 | import com.wix.test.random._ 15 | 16 | import scala.collection.immutable 17 | import scala.collection.mutable.ListBuffer 18 | 19 | trait HttpClientTestSupport { 20 | val parameter = randomStrPair 21 | val header = randomStrPair 22 | val formData = randomStrPair 23 | val userAgent = randomStr 24 | val cookie = randomStrPair 25 | val path = s"$randomStr/$randomStr" 26 | val anotherPath = s"$randomStr/$randomStr" 27 | val someObject = SomeCaseClass(randomStr, randomInt) 28 | 29 | val somePort = randomPort 30 | val content = randomStr 31 | val anotherContent = randomStr 32 | 33 | val requestData = ListBuffer.empty[String] 34 | 35 | val thirtyTwoKHeader = "h" -> Seq.fill(32 * 1024)('a').mkString 36 | val thirtyTwoKHeaderPlus = "h" -> Seq.fill(32 * 1024 + 1)('a').mkString 37 | 38 | 39 | val bigResponse = 1024 * 1024 40 | 41 | def issueChunkedPostRequestWith(content: String, toPath: String)(implicit baseUri: BaseUri) = { 42 | val serverUrl = new URL(s"http://localhost:${baseUri.port}/$toPath") 43 | val conn = serverUrl.openConnection.asInstanceOf[HttpURLConnection] 44 | conn.setRequestMethod("POST") 45 | conn.setRequestProperty("Content-Type", "text/plain") 46 | conn.setChunkedStreamingMode(0) 47 | conn.setDoOutput(true) 48 | conn.setDoInput(true) 49 | conn.setUseCaches(false) 50 | conn.connect() 51 | 52 | val out = new DataOutputStream(conn.getOutputStream) 53 | out.writeBytes(content) 54 | out.flush() 55 | out.close() 56 | conn.disconnect() 57 | } 58 | } 59 | 60 | object HttpClientTestResponseHandlers { 61 | def handlerFor(path: String, returnsBody: String): RequestHandler = { 62 | case r: HttpRequest if r.uri.path.toString.endsWith(path) => HttpResponse(entity = returnsBody) 63 | } 64 | 65 | def unmarshallingAndStoringHandlerFor(path: String, storeTo: ListBuffer[String]): RequestHandler = { 66 | case r: HttpRequest if r.uri.path.toString.endsWith(path) => 67 | storeTo.append( r.extractAsString ) 68 | HttpResponse() 69 | } 70 | 71 | def bigResponseWith(size: Int): RequestHandler = { 72 | case HttpRequest(GET, uri, _, _, _) if uri.path.toString().contains("big-response") => 73 | HttpResponse(entity = HttpEntity(randomStrWith(size))) 74 | } 75 | 76 | def chunkedResponseFor(path: String): RequestHandler = { 77 | case r: HttpRequest if r.uri.path.toString.endsWith(path) => 78 | HttpResponse(entity = HttpEntity.Chunked(ContentTypes.`text/plain(UTF-8)`, Source.single(randomStr))) 79 | } 80 | 81 | def alwaysRespondWith(transferEncoding: TransferEncoding, toPath: String): RequestHandler = { 82 | case r: HttpRequest if r.uri.path.toString.endsWith(toPath) => 83 | HttpResponse().withHeaders(immutable.Seq(`Transfer-Encoding`(transferEncoding))) 84 | } 85 | 86 | val slowRespondingServer: RequestHandler = { case _ => Thread.sleep(500); HttpResponse() } 87 | } 88 | 89 | case class SomeCaseClass(s: String, i: Int) 90 | 91 | object HttpClientMatchers { 92 | import com.wix.e2e.http.matchers.RequestMatchers._ 93 | 94 | def haveClientHttpTestkitUserAgentWithLibraryVersion: RequestMatcher = 95 | haveAnyHeadersOf("User-Agent" -> s"client-http-testkit/$HttpTestkitVersion") 96 | } 97 | 98 | object HttpServerMatchers { 99 | import com.wix.e2e.http.matchers.ResponseMatchers._ 100 | 101 | def haveServerHttpTestkitHeaderWithLibraryVersion: ResponseMatcher = 102 | haveAnyHeadersOf("Server" -> s"server-http-testkit/$HttpTestkitVersion") 103 | } -------------------------------------------------------------------------------- /contract-tests/http-testkit-contract-tests/src/test/scala/com/wix/e2e/http/drivers/StubWebServerProvider.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.drivers 2 | 3 | import com.wix.e2e.http.BaseUri 4 | import com.wix.e2e.http.server.WebServerFactory.aStubWebServer 5 | import org.specs2.mutable.After 6 | 7 | trait StubWebServerProvider extends After { 8 | val server = aStubWebServer.build 9 | .start() 10 | 11 | def after = server.stop() 12 | 13 | val ClosedPort = BaseUri(port = 11111) 14 | 15 | lazy implicit val baseUri: BaseUri = server.baseUri 16 | } 17 | 18 | 19 | 20 | //object StubWebServerMatchers { 21 | // import org.specs2.matcher.Matchers._ 22 | // 23 | // def httpRequestWith(method: String, toPath: String): Matcher[HttpRequest] = 24 | // be_===( toPath ) ^^ { (_: HttpRequest).uri.path.toString().stripPrefix("/") aka "request path"} and 25 | // be_==[String](method).ignoreCase ^^ { (_: HttpRequest).method.name /*aka "method"*/ } 26 | // 27 | // def httpRequestWith(header: (String, String)): Matcher[HttpRequest] = 28 | // havePair( header ) ^^ { (_: HttpRequest).headers.map( h => h.name -> h.value) aka "request headers" } 29 | // 30 | // def receivedRequestWith(method: String, toPath: String): Matcher[StubWebServer] = { 31 | // contain(httpRequestWith(method, toPath)).eventually ^^ { (_: StubWebServer).recordedRequests aka "requests" } 32 | // } 33 | // 34 | // def receivedRequestWith(header: (String, String)): Matcher[StubWebServer] = { 35 | // contain(httpRequestWith(header)).eventually ^^ { (_: StubWebServer).recordedRequests aka "requests" } 36 | // } 37 | //} 38 | -------------------------------------------------------------------------------- /contract-tests/http-testkit-contract-tests/src/test/scala/com/wix/e2e/http/info/VersionConsistencyTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.info 2 | 3 | import org.specs2.mutable.Spec 4 | 5 | class VersionConsistencyTest extends Spec with VersionConsistencyTestSupport { 6 | "Version Constant" should { 7 | "be consistent with version.sbt" in { 8 | readVersionFromSbtFile must beSome( HttpTestkitVersion ) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contract-tests/http-testkit-contract-tests/src/test/scala_2.13+/com/wix/e2e/http/info/VersionConsistencyTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.info 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | 6 | import scala.jdk.CollectionConverters._ 7 | 8 | trait VersionConsistencyTestSupport { 9 | 10 | def readVersionFromSbtFile = 11 | Files.readAllLines(new File("./version.sbt").toPath).asScala 12 | .find( findLineContainingVersion ) 13 | .map( parseVersionFromSbt ) 14 | 15 | def parseVersionFromSbt(line: String) = 16 | line.substring( line.indexOf('"') + 1, line.lastIndexOf('"')) 17 | 18 | def findLineContainingVersion(line: String) = 19 | line.contains("ThisBuild / version") 20 | } 21 | -------------------------------------------------------------------------------- /contract-tests/http-testkit-contract-tests/src/test/scala_2.13-/com/wix/e2e/http/info/VersionConsistencyTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.info 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | 6 | import scala.collection.JavaConverters._ 7 | 8 | trait VersionConsistencyTestSupport { 9 | 10 | def readVersionFromSbtFile = 11 | Files.readAllLines(new File("./version.sbt").toPath).asScala 12 | .find( findLineContainingVersion ) 13 | .map( parseVersionFromSbt ) 14 | 15 | def parseVersionFromSbt(line: String) = 16 | line.substring( line.indexOf('"') + 1, line.lastIndexOf('"')) 17 | 18 | def findLineContainingVersion(line: String) = 19 | line.contains("ThisBuild / version") 20 | } -------------------------------------------------------------------------------- /contract-tests/marshaller-contract-tests/http-testkit-contract-tests-custom-marshaller/src/test/scala/com/wix/e2e/http/drivers/MarshallerTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.drivers 2 | 3 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 4 | import com.wix.e2e.http.api.Marshaller 5 | import com.wix.e2e.http.drivers.MarshallingTestObjects.SomeCaseClass 6 | import com.wix.test.random.{randomInt, randomStr} 7 | 8 | import scala.collection.concurrent.TrieMap 9 | 10 | trait MarshallerTestSupport { 11 | val someObject = SomeCaseClass(randomStr, randomInt) 12 | val content = randomStr 13 | 14 | def givenMarshallerThatUnmarshalWith(unmarshal: SomeCaseClass, forContent: String): Unit = 15 | MarshallingTestObjects.unmarshallResult.put(forContent, unmarshal) 16 | 17 | def givenMarshallerThatMarshal(content: String, to: SomeCaseClass): Unit = 18 | MarshallingTestObjects.marshallResult.put(to, content) 19 | 20 | def aResponseWith(body: String) = HttpResponse(entity = body) 21 | def aRequestWith(body: String) = HttpRequest(entity = body) 22 | val request = HttpRequest() 23 | } 24 | 25 | object MarshallingTestObjects { 26 | case class SomeCaseClass(s: String, i: Int) 27 | 28 | val marshallResult = TrieMap.empty[SomeCaseClass, String] 29 | val unmarshallResult = TrieMap.empty[String, SomeCaseClass] 30 | 31 | class MarshallerForTest extends Marshaller { 32 | 33 | def unmarshall[T: Manifest](jsonStr: String) = 34 | MarshallingTestObjects.unmarshallResult 35 | .getOrElse(jsonStr, throw new UnsupportedOperationException) 36 | .asInstanceOf[T] 37 | 38 | def marshall[T](t: T) = 39 | MarshallingTestObjects.marshallResult 40 | .getOrElse(t.asInstanceOf[SomeCaseClass], throw new UnsupportedOperationException) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /contract-tests/marshaller-contract-tests/http-testkit-contract-tests-custom-marshaller/src/test/scala/com/wix/e2e/http/marshaller/HttpClientCustomMarshallerContractTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.marshaller 2 | 3 | import com.wix.e2e.http.api.Marshaller.Implicits._ 4 | import com.wix.e2e.http.client.extractors.HttpMessageExtractors 5 | import com.wix.e2e.http.client.transformers.HttpClientTransformers 6 | import com.wix.e2e.http.drivers.MarshallerTestSupport 7 | import com.wix.e2e.http.drivers.MarshallingTestObjects.SomeCaseClass 8 | import com.wix.e2e.http.matchers.{RequestMatchers, ResponseMatchers} 9 | import org.specs2.mutable.Spec 10 | import org.specs2.specification.Scope 11 | 12 | class HttpClientCustomMarshallerContractTest extends Spec with HttpClientTransformers with HttpMessageExtractors { 13 | 14 | trait ctx extends Scope with MarshallerTestSupport 15 | 16 | "RequestTransformers with custom marshaller" should { 17 | 18 | "detect custom marshaller and use it to marshall body payload" in new ctx { 19 | givenMarshallerThatUnmarshalWith(someObject, content) 20 | givenMarshallerThatMarshal(content, to = someObject) 21 | 22 | withPayload(someObject).apply(request) must RequestMatchers.haveBodyWith(someObject) 23 | } 24 | 25 | "detect custom marshaller and use it to extract response" in new ctx { 26 | givenMarshallerThatUnmarshalWith(someObject, content) 27 | 28 | aResponseWith(content).extractAs[SomeCaseClass] must_=== someObject 29 | } 30 | } 31 | 32 | "RequestBodyMatchers with custom marshaller" should { 33 | 34 | "in haveBodyWith, support unmarshalling body content with user custom unmarshaller" in new ctx { 35 | givenMarshallerThatUnmarshalWith(someObject, forContent = content) 36 | 37 | aRequestWith(content) must RequestMatchers.haveBodyWith(entity = someObject) 38 | } 39 | 40 | "in haveBodyEntityThat, support unmarshalling body content with user custom unmarshaller" in new ctx { 41 | givenMarshallerThatUnmarshalWith(someObject, forContent = content) 42 | 43 | aRequestWith(content) must RequestMatchers.haveBodyEntityThat(must = be_===( someObject )) 44 | } 45 | } 46 | 47 | "ResponseBodyMatchers with custom marshaller" should { 48 | 49 | "in haveBodyWith matcher, detect custom marshaller from classpath and use it to unmarshal request" in new ctx { 50 | givenMarshallerThatUnmarshalWith(someObject, forContent = content) 51 | 52 | aResponseWith(content) must ResponseMatchers.haveBodyWith(someObject) 53 | } 54 | 55 | "in haveBodyThat matcher, detect custom marshaller from classpath and use it to unmarshal request" in new ctx { 56 | givenMarshallerThatUnmarshalWith(someObject, forContent = content) 57 | 58 | aResponseWith(content) must ResponseMatchers.haveBodyWithEntityThat(must = be_===(someObject)) 59 | } 60 | 61 | "in beSuccessfulWith matcher, detect custom marshaller from classpath and use it to unmarshal request" in new ctx { 62 | givenMarshallerThatUnmarshalWith(someObject, forContent = content) 63 | 64 | aResponseWith(content) must ResponseMatchers.beSuccessfulWith(someObject) 65 | } 66 | 67 | "in beSuccessfulWithEntityThat matcher, detect custom marshaller from classpath and use it to unmarshal request" in new ctx { 68 | givenMarshallerThatUnmarshalWith(someObject, forContent = content) 69 | 70 | aResponseWith(content) must ResponseMatchers.beSuccessfulWithEntityThat(must = be_===(someObject)) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /contract-tests/marshaller-contract-tests/http-testkit-contract-tests-dual-marshallers/src/test/scala/com/wix/e2e/http/json/DualMarshallersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.json 2 | 3 | 4 | import com.wix.e2e.http.api.Marshaller 5 | import org.specs2.mutable.Spec 6 | 7 | 8 | class DualMarshallersTest extends Spec { 9 | 10 | "Dual Marshallers" should { 11 | "pick the custom one over the testkit provided marshaller" in { 12 | Marshaller.Implicits.marshaller must beAnInstanceOf[DummyCustomMarshaller] 13 | } 14 | } 15 | } 16 | 17 | class DummyCustomMarshaller extends Marshaller { 18 | def unmarshall[T : Manifest](jsonStr: String): T = ??? 19 | def marshall[T](t: T): String = ??? 20 | } 21 | -------------------------------------------------------------------------------- /contract-tests/marshaller-contract-tests/http-testkit-contract-tests-malformed-marshaller/src/test/scala/com/wix/e2e/http/marshaller/HttpClientMalformedMarshallerContractTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.marshaller 2 | 3 | import com.wix.e2e.http.api.{Marshaller, NopMarshaller} 4 | import org.specs2.mutable.Spec 5 | 6 | import scala.collection.mutable.ListBuffer 7 | 8 | class HttpClientMalformedMarshallerContractTest extends Spec { 9 | 10 | "RequestTransformers with malformed marshaller" should { 11 | "try to create all malformed marshallers and fallback to NopMarshaller when all is failing" in { 12 | Marshaller.Implicits.marshaller must beAnInstanceOf[NopMarshaller] 13 | 14 | MarshallerCalled.contractorsCalled must containTheSameElementsAs(Seq(classOf[MalformedCustomMarshaller], classOf[MalformedCustomMarshaller2])) 15 | } 16 | } 17 | } 18 | 19 | class MalformedCustomMarshaller(dummy: Int) extends BaseMalformedCustomMarshaller { 20 | def this() = { 21 | this(5) 22 | markConstractorCalledAndExplode 23 | } 24 | } 25 | 26 | class MalformedCustomMarshaller2(dummy: Int) extends BaseMalformedCustomMarshaller { 27 | def this() = { 28 | this(5) 29 | markConstractorCalledAndExplode 30 | } 31 | } 32 | 33 | abstract class BaseMalformedCustomMarshaller extends Marshaller { 34 | def markConstractorCalledAndExplode = { 35 | MarshallerCalled.markConstructorCalled(getClass) 36 | throw new RuntimeException("whatever") 37 | } 38 | 39 | def unmarshall[T : Manifest](jsonStr: String): T = ??? 40 | def marshall[T](t: T): String = ??? 41 | } 42 | 43 | object MarshallerCalled { 44 | private val called = ListBuffer.empty[Class[_]] 45 | 46 | def markConstructorCalled(clazz: Class[_]) = this.synchronized { 47 | called.append(clazz) 48 | } 49 | 50 | def contractorsCalled: Seq[Class[_]] = this.synchronized { 51 | called 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /contract-tests/marshaller-contract-tests/http-testkit-contract-tests-no-custom-marshaller/src/test/scala/com/wix/e2e/http/drivers/MarshallerTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.drivers 2 | 3 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 4 | import com.wix.e2e.http.drivers.MarshallingTestObjects.SomeCaseClass 5 | import com.wix.e2e.http.exceptions.MissingMarshallerException 6 | import com.wix.test.random.{randomInt, randomStr} 7 | import org.specs2.execute.AsResult 8 | import org.specs2.matcher.Matcher 9 | import org.specs2.matcher.ResultMatchers.beError 10 | 11 | trait MarshallerTestSupport { 12 | val someObject = SomeCaseClass(randomStr, randomInt) 13 | val content = randomStr 14 | 15 | def aResponseWith(body: String) = HttpResponse(entity = body) 16 | def aRequestWith(body: String) = HttpRequest(entity = body) 17 | val request = HttpRequest() 18 | } 19 | 20 | 21 | object MarshallingTestObjects { 22 | case class SomeCaseClass(s: String, i: Int) 23 | } 24 | 25 | object MarshallerMatchers { 26 | def beMissingMarshallerMatcherError[T : AsResult]: Matcher[T] = beError[T](new MissingMarshallerException().getMessage) 27 | } 28 | -------------------------------------------------------------------------------- /contract-tests/marshaller-contract-tests/http-testkit-contract-tests-no-custom-marshaller/src/test/scala/com/wix/e2e/http/marshaller/HttpClientNoCustomMarshallerContractTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.marshaller 2 | 3 | import com.wix.e2e.http.api.Marshaller.Implicits.marshaller 4 | import com.wix.e2e.http.client.extractors.HttpMessageExtractors 5 | import com.wix.e2e.http.client.transformers.HttpClientTransformers 6 | import com.wix.e2e.http.drivers.MarshallerMatchers._ 7 | import com.wix.e2e.http.drivers.MarshallerTestSupport 8 | import com.wix.e2e.http.drivers.MarshallingTestObjects.SomeCaseClass 9 | import com.wix.e2e.http.exceptions.MissingMarshallerException 10 | import com.wix.e2e.http.matchers.{RequestMatchers, ResponseMatchers} 11 | import org.specs2.mutable.Spec 12 | import org.specs2.specification.Scope 13 | 14 | class HttpClientNoCustomMarshallerContractTest extends Spec with HttpClientTransformers with HttpMessageExtractors { 15 | 16 | trait ctx extends Scope with MarshallerTestSupport 17 | 18 | "Response Transformers without custom marshaller" should { 19 | 20 | "detect custom marshaller and use it to marshall body payload" in new ctx { 21 | withPayload(someObject).apply(request) must throwA[MissingMarshallerException] 22 | } 23 | 24 | 25 | "print informative error message when marshaller is not included" in new ctx { 26 | aResponseWith(content).extractAs[SomeCaseClass] must throwA[MissingMarshallerException] 27 | } 28 | } 29 | 30 | "RequestBodyMatchers without custom marshaller" should { 31 | 32 | "print informative error message when marshaller is not included" in new ctx { 33 | RequestMatchers.haveBodyWith(entity = someObject).apply(aRequestWith(content)) must beMissingMarshallerMatcherError 34 | } 35 | 36 | "print informative error message when marshaller is not included2" in new ctx { 37 | RequestMatchers.haveBodyEntityThat(must = be_===(someObject)).apply(aRequestWith(content)) must beMissingMarshallerMatcherError 38 | } 39 | } 40 | 41 | "ResponseBodyMatchers without custom marshaller" should { 42 | "in haveBodyWith matcher, print informative error message when marshaller is not included" in new ctx { 43 | ResponseMatchers.haveBodyWith(entity = someObject).apply(aResponseWith(content)) must beMissingMarshallerMatcherError 44 | } 45 | 46 | "in haveBodyThat matcher, print informative error message when marshaller is not included2" in new ctx { 47 | ResponseMatchers.haveBodyWithEntityThat(must = be_===(someObject)).apply(aResponseWith(content)) must beMissingMarshallerMatcherError 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/src/main/scala/com/wix/e2e/http/examples/MediaServer.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.examples 2 | 3 | import akka.http.scaladsl.model.HttpMethods.{GET, PUT} 4 | import akka.http.scaladsl.model.MediaTypes.`image/png` 5 | import akka.http.scaladsl.model.StatusCodes.NotFound 6 | import akka.http.scaladsl.model.Uri.Path 7 | import akka.http.scaladsl.model._ 8 | import com.wix.e2e.http.RequestHandler 9 | import com.wix.e2e.http.client.extractors._ 10 | import com.wix.e2e.http.server.WebServerFactory.aMockWebServerWith 11 | 12 | import scala.collection.concurrent.TrieMap 13 | 14 | class MediaServer(port: Int, uploadPath: String, downloadPath: String) { 15 | 16 | private val mockWebServer = aMockWebServerWith( { 17 | case HttpRequest(PUT, u, headers, entity, _) if u.path.tail == Path(uploadPath) => 18 | handleMediaPost(u, headers.toList, entity) 19 | 20 | case HttpRequest(GET, u, headers, _, _) if u.path.tail.toString().startsWith(downloadPath) => 21 | handleMediaGet(u, headers.toList) 22 | 23 | } : RequestHandler).onPort(port) 24 | .build.start() 25 | 26 | def stop() = mockWebServer.stop() 27 | 28 | private def handleMediaPost(uri: Uri, headers: List[HttpHeader], entity: HttpEntity): HttpResponse = { 29 | val fileName = headers.find( _.name == "filename").map( _.value ).orElse( uri.query().toMap.get("f") ).get 30 | val media = entity.extractAsBytes 31 | files.put(fileName, media) 32 | HttpResponse() 33 | } 34 | 35 | private def handleMediaGet(uri: Uri, headers: List[HttpHeader]): HttpResponse = { 36 | val fileName = uri.path.reverse 37 | .head.toString 38 | .stripPrefix("/") 39 | files.get(fileName) 40 | .map( i => HttpResponse(entity = HttpEntity(`image/png`, i)) ) 41 | .getOrElse( HttpResponse(status = NotFound) ) 42 | } 43 | 44 | private val files = TrieMap.empty[String, Array[Byte]] 45 | } 46 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = "ERROR" 3 | } -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/HttpClientSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client 2 | 3 | import akka.http.scaladsl.client.RequestBuilding.{Delete, Get, Head, Options, Patch, Post, Put, RequestBuilder} 4 | import akka.http.scaladsl.model.HttpMethods.TRACE 5 | import com.wix.e2e.http.client.extractors.HttpMessageExtractors 6 | import com.wix.e2e.http.client.internals.{BlockingRequestManager, NonBlockingRequestManager} 7 | import com.wix.e2e.http.client.transformers.HttpClientTransformers 8 | 9 | trait BlockingHttpClientSupport extends HttpClientTransformers with HttpMessageExtractors { 10 | val get = new BlockingRequestManager(Get()) 11 | val post = new BlockingRequestManager(Post()) 12 | val put = new BlockingRequestManager(Put()) 13 | val patch = new BlockingRequestManager(Patch()) 14 | val delete = new BlockingRequestManager(Delete()) 15 | val options = new BlockingRequestManager(Options()) 16 | val head = new BlockingRequestManager(Head()) 17 | val trace = new BlockingRequestManager(new RequestBuilder(TRACE).apply()) 18 | } 19 | 20 | trait NonBlockingHttpClientSupport extends HttpClientTransformers with HttpMessageExtractors { 21 | val get = new NonBlockingRequestManager(Get()) 22 | val post = new NonBlockingRequestManager(Post()) 23 | val put = new NonBlockingRequestManager(Put()) 24 | val patch = new NonBlockingRequestManager(Patch()) 25 | val delete = new NonBlockingRequestManager(Delete()) 26 | val options = new NonBlockingRequestManager(Options()) 27 | val head = new NonBlockingRequestManager(Head()) 28 | val trace = new NonBlockingRequestManager(new RequestBuilder(TRACE).apply()) 29 | } 30 | 31 | object NonBlockingHttpClientSupport extends NonBlockingHttpClientSupport 32 | object BlockingHttpClientSupport extends BlockingHttpClientSupport 33 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/extractors/HttpMessageExtractors.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.extractors 2 | 3 | import akka.http.scaladsl.model.{HttpEntity, HttpMessage} 4 | import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} 5 | import com.wix.e2e.http.WixHttpTestkitResources 6 | import com.wix.e2e.http.api.Marshaller 7 | import com.wix.e2e.http.config.Config.DefaultTimeout 8 | import com.wix.e2e.http.utils._ 9 | 10 | import scala.concurrent.duration._ 11 | 12 | trait HttpMessageExtractors { 13 | implicit class HttpMessageExtractorsOps[M <: HttpMessage](message: M) { 14 | def extractAs[T : Manifest](implicit marshaller: Marshaller = Marshaller.Implicits.marshaller, atMost: FiniteDuration = DefaultTimeout): T = message.entity.extractAs[T] 15 | def extractAsString(implicit atMost: FiniteDuration = DefaultTimeout): String = message.entity.extractAsString 16 | def extractAsBytes(implicit atMost: FiniteDuration = DefaultTimeout): Array[Byte] = message.entity.extractAsBytes 17 | } 18 | 19 | implicit class HttpEntityExtractorsOps[E <: HttpEntity](entity: E) { 20 | def extractAs[T : Manifest](implicit marshaller: Marshaller = Marshaller.Implicits.marshaller, atMost: FiniteDuration = DefaultTimeout): T = marshaller.unmarshall[T](extract[String]) 21 | def extractAsString(implicit atMost: FiniteDuration = DefaultTimeout): String = extract[String] 22 | def extractAsBytes(implicit atMost: FiniteDuration = DefaultTimeout): Array[Byte] = extract[Array[Byte]] 23 | 24 | import WixHttpTestkitResources.materializer 25 | private def extract[T](implicit um: Unmarshaller[E, T], atMost: FiniteDuration) = waitFor(Unmarshal(entity).to[T])(atMost) 26 | } 27 | } 28 | 29 | object HttpMessageExtractors extends HttpMessageExtractors 30 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/extractors/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client 2 | 3 | package object extractors extends HttpMessageExtractors 4 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/internals/RequestManager.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.internals 2 | 3 | import akka.http.scaladsl.Http 4 | import akka.http.scaladsl.model.TransferEncodings.chunked 5 | import akka.http.scaladsl.model.headers.{ProductVersion, `Transfer-Encoding`, `User-Agent`} 6 | import akka.http.scaladsl.settings.ConnectionPoolSettings 7 | import akka.stream.StreamTcpException 8 | import com.wix.e2e.http._ 9 | import com.wix.e2e.http.config.Config.DefaultTimeout 10 | import com.wix.e2e.http.exceptions.ConnectionRefusedException 11 | import com.wix.e2e.http.info.HttpTestkitVersion 12 | import com.wix.e2e.http.utils._ 13 | 14 | import scala.concurrent.Future 15 | import scala.concurrent.duration._ 16 | 17 | 18 | trait RequestManager[R] { 19 | def apply(path: String, but: RequestTransformer = identity, withTimeout: FiniteDuration = DefaultTimeout)(implicit baseUri: BaseUri): R 20 | } 21 | 22 | class NonBlockingRequestManager(request: HttpRequest) extends RequestManager[Future[HttpResponse]] { 23 | 24 | 25 | def apply(path: String, but: RequestTransformer, withTimeout: FiniteDuration)(implicit baseUri: BaseUri): Future[HttpResponse] = { 26 | val transformed = Seq(composeUrlFor(baseUri, path), but) 27 | .foldLeft(request) { case (r, tr) => tr(r) } 28 | import WixHttpTestkitResources.{executionContext, materializer, system} 29 | Http().singleRequest(request = transformed, 30 | settings = settingsWith(withTimeout)) 31 | .map( recreateTransferEncodingHeader ) 32 | .flatMap( _.toStrict(withTimeout) ) 33 | .recoverWith( { case _: StreamTcpException => Future.failed(new ConnectionRefusedException(baseUri)) } ) 34 | } 35 | 36 | private def composeUrlFor(baseUri: BaseUri, path: String): RequestTransformer = 37 | _.withUri(uri = baseUri.asUriWith(path) ) 38 | 39 | private def recreateTransferEncodingHeader(r: HttpResponse) = 40 | if ( !r.entity.isChunked ) r 41 | else { 42 | val encodings = r.header[`Transfer-Encoding`] 43 | .map( _.encodings ) 44 | .getOrElse( Seq.empty ) 45 | r.removeHeader("Transfer-Encoding") 46 | .addHeader(`Transfer-Encoding`(chunked, encodings:_*)) 47 | } 48 | 49 | private def settingsWith(timeout: FiniteDuration) = { 50 | val settings = ConnectionPoolSettings(WixHttpTestkitResources.system) 51 | settings.withConnectionSettings( settings.connectionSettings 52 | .withIdleTimeout(timeout) 53 | .withUserAgentHeader(Some(`User-Agent`(ProductVersion("client-http-testkit", HttpTestkitVersion)))) 54 | .withConnectingTimeout(timeout) 55 | .withParserSettings( settings.connectionSettings 56 | .parserSettings //maxHeaderValueLength 57 | .withMaxHeaderValueLength(32 * 1024) ) ) 58 | .withMaxConnections(32) 59 | .withPipeliningLimit(4) 60 | .withMaxRetries(0) 61 | } 62 | } 63 | 64 | class BlockingRequestManager(request: HttpRequest) extends RequestManager[HttpResponse] { 65 | 66 | def apply(path: String, but: RequestTransformer, withTimeout: FiniteDuration)(implicit baseUri: BaseUri): HttpResponse = 67 | waitFor(nonBlockingRequestManager(path, but, withTimeout))(withTimeout + 1.second) 68 | 69 | private val nonBlockingRequestManager = new NonBlockingRequestManager(request) 70 | } 71 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/internals/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client 2 | 3 | import java.net.URLEncoder 4 | 5 | import akka.http.scaladsl.model.Uri 6 | import akka.http.scaladsl.model.Uri.Path 7 | import com.wix.e2e.http.BaseUri 8 | 9 | package object internals { 10 | 11 | implicit class `BaseUri --> akka.Uri`(private val u: BaseUri) extends AnyVal { 12 | def asUri: Uri = asUriWith("") 13 | def asUriWith(relativeUrl: String): Uri = 14 | if (relativeUrl.contains('?')) 15 | urlWithoutParams(relativeUrl).withRawQueryString( extractParamsFrom(relativeUrl) ) 16 | else urlWithoutParams(relativeUrl) 17 | 18 | 19 | private def fixPath(url: Option[String]) = { 20 | url.map( _.trim ) 21 | .map( u => s"/${u.stripPrefix("/")}" ) 22 | .filterNot( _.equals("/") ) 23 | .map( Path(_) ) 24 | .getOrElse( Path.Empty ) 25 | } 26 | 27 | private def buildPath(context: Option[String], relativePath: Option[String]) = { 28 | val c = fixPath(context) 29 | val r = fixPath(relativePath) 30 | c ++ r 31 | } 32 | 33 | private def urlWithoutParams(relativeUrl: String) = 34 | Uri(scheme = "http").withHost(u.host) 35 | .withPort(u.port) 36 | .withPath( buildPath(u.contextRoot, Option(extractPathFrom(relativeUrl))) ) 37 | 38 | private def extractPathFrom(relativeUrl: String) = relativeUrl.split('?').head 39 | 40 | private def extractParamsFrom(relativeUrl: String) = 41 | rebuildAndEscapeParams(relativeUrl.substring(relativeUrl.indexOf('?') + 1)) 42 | 43 | private def rebuildAndEscapeParams(p: String) = 44 | p.split("&") 45 | .map( _.split("=") ) 46 | .map( { case Array(k, v) => s"$k=${URLEncoder.encode(v, "UTF-8")}" 47 | case Array(k) => k 48 | } ) 49 | .mkString("&") 50 | } 51 | } -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/transformers/HttpClientTransformers.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.transformers 2 | 3 | import java.io.File 4 | 5 | import akka.http.scaladsl.model.{ContentType, ContentTypes, HttpCharsets, MediaTypes} 6 | import com.wix.e2e.http.client.transformers.internals._ 7 | 8 | trait HttpClientTransformers extends HttpClientRequestUrlTransformers 9 | with HttpClientRequestHeadersTransformers 10 | with HttpClientRequestBodyTransformers 11 | with HttpClientRequestTransformersOps 12 | 13 | object HttpClientTransformers extends HttpClientTransformers 14 | 15 | trait HttpClientContentTypes { 16 | val TextPlain = ContentTypes.`text/plain(UTF-8)` 17 | val JsonContent = ContentTypes.`application/json` 18 | val XmlContent = ContentType(MediaTypes.`application/xml`, HttpCharsets.`UTF-8`) 19 | val BinaryStream = ContentTypes.`application/octet-stream` 20 | val FormUrlEncoded = ContentTypes.`application/x-www-form-urlencoded` 21 | } 22 | 23 | object HttpClientContentTypes extends HttpClientContentTypes 24 | 25 | sealed trait RequestPart 26 | case class PlainRequestPart(body: String, contentType: ContentType = TextPlain) extends RequestPart 27 | case class BinaryRequestPart(body: Array[Byte], contentType: ContentType = BinaryStream, filename: Option[String] = None) extends RequestPart 28 | case class FileRequestPart(file: File, contentType: ContentType = BinaryStream, filename: Option[String] = None) extends RequestPart 29 | case class FileNameRequestPart(filepath: String, contentType: ContentType = BinaryStream, filename: Option[String] = None) extends RequestPart 30 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/transformers/internals/request.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.transformers.internals 2 | 3 | import java.io.File 4 | import akka.http.scaladsl.model.Uri.Query 5 | import akka.http.scaladsl.model._ 6 | import akka.http.scaladsl.model.headers.{Cookie, RawHeader, `User-Agent`} 7 | import akka.util.ByteString 8 | import com.wix.e2e.http.api.Marshaller 9 | import com.wix.e2e.http.client.transformers._ 10 | import com.wix.e2e.http.client.transformers.internals.RequestPartOps._ 11 | import com.wix.e2e.http.exceptions.UserAgentModificationNotSupportedException 12 | import com.wix.e2e.http.{RequestTransformer, WixHttpTestkitResources} 13 | 14 | import java.nio.file.Path 15 | import scala.xml.Node 16 | 17 | trait HttpClientRequestUrlTransformers { 18 | def withParam(param: (String, String)): RequestTransformer = withParams(param) 19 | def withParams(params: (String, String)*): RequestTransformer = r => 20 | r.withUri(uri = r.uri 21 | .withQuery( Query(currentParams(r) ++ params: _*)) ) 22 | 23 | private def currentParams(r: HttpRequest): Seq[(String, String)] = 24 | r.uri.rawQueryString 25 | .map( Query(_).toSeq ) 26 | .getOrElse( Seq.empty ) 27 | } 28 | 29 | trait HttpClientRequestHeadersTransformers { 30 | def withHeader(header: (String, String)): RequestTransformer = withHeaders(header) 31 | def withHeaders(headers: (String, String)*): RequestTransformer = 32 | appendHeaders( headers.map { 33 | case (h, _) if h.toLowerCase == "user-agent" => throw new UserAgentModificationNotSupportedException 34 | case (h, v) => RawHeader(h, v) 35 | } ) 36 | 37 | def withUserAgent(value: String): RequestTransformer = appendHeaders(Seq(`User-Agent`(value))) 38 | 39 | def withCookie(cookie: (String, String)): RequestTransformer = withCookies(cookie) 40 | def withCookies(cookies: (String, String)*): RequestTransformer = appendHeaders( cookies.map(p => Cookie(p._1, p._2)) ) 41 | 42 | 43 | private def appendHeaders[H <: HttpHeader](headers: Iterable[H]): RequestTransformer = r => 44 | r.withHeaders( r.headers ++ headers) 45 | } 46 | 47 | trait HttpClientRequestBodyTransformers extends HttpClientContentTypes { 48 | @deprecated("use `withTextPayload`", since = "Dec18, 2017") 49 | def withPayload(body: String, contentType: ContentType = TextPlain): RequestTransformer = withPayload(ByteString(body).toByteBuffer.array, contentType) 50 | def withTextPayload(body: String, contentType: ContentType = TextPlain): RequestTransformer = withPayload(ByteString(body).toByteBuffer.array, contentType) 51 | def withPayload(bytes: Array[Byte], contentType: ContentType): RequestTransformer = setBody(HttpEntity(contentType, bytes)) 52 | def withPayload(path: Path, contentType: ContentType): RequestTransformer = setBody(HttpEntity.fromPath(contentType, path)) 53 | def withPayload(xml: Node): RequestTransformer = setBody(HttpEntity(XmlContent, WixHttpTestkitResources.xmlPrinter.format(xml))) 54 | 55 | // todo: enable default marshaller when deprecated `withPayload` is removed 56 | def withPayload(entity: AnyRef)(implicit marshaller: Marshaller/* = Marshaller.Implicits.marshaller*/): RequestTransformer = 57 | withTextPayload(marshaller.marshall(entity), JsonContent) 58 | 59 | def withFormData(formParams: (String, String)*): RequestTransformer = setBody(FormData(formParams.toMap).toEntity) 60 | 61 | def withMultipartData(parts: (String, RequestPart)*): RequestTransformer = 62 | setBody( Multipart.FormData(parts.map { 63 | case (n, p) => Multipart.FormData.BodyPart(n, p.asBodyPartEntity, p.withAdditionalParams) 64 | }:_*) 65 | .toEntity) 66 | 67 | private def setBody(entity: RequestEntity): RequestTransformer = _.withEntity(entity = entity) 68 | } 69 | 70 | object RequestPartOps { 71 | 72 | implicit class `RequestPart --> HttpEntity`(private val r: RequestPart) extends AnyVal { 73 | def asBodyPartEntity: BodyPartEntity = r match { 74 | case PlainRequestPart(v, c) => HttpEntity(v).withContentType(c) 75 | case BinaryRequestPart(b, c, _) => HttpEntity(c, b) 76 | case FileRequestPart(f, c, _) => HttpEntity.fromPath(c, f.toPath) 77 | case FileNameRequestPart(p, c, fn) => FileRequestPart(new File(p), c, fn).asBodyPartEntity 78 | } 79 | } 80 | 81 | implicit class `RequestPart --> AdditionalParams`(private val r: RequestPart) extends AnyVal { 82 | def withAdditionalParams: Map[String, String] = r match { 83 | case _: PlainRequestPart => NoAdditionalParams 84 | case BinaryRequestPart(_, _, fn) => additionalParams(fn) 85 | case FileRequestPart(_, _, fn) => additionalParams(fn) 86 | case FileNameRequestPart(_, _, fn) => additionalParams(fn) 87 | } 88 | 89 | private def additionalParams(filenameOpt: Option[String]) = 90 | filenameOpt.map(fn => Map("filename" -> fn)) 91 | .getOrElse( NoAdditionalParams ) 92 | 93 | private def NoAdditionalParams = Map.empty[String, String] 94 | } 95 | } 96 | 97 | 98 | trait HttpClientRequestTransformersOps { 99 | implicit class TransformerConcatenation(first: RequestTransformer) { 100 | def and(second: RequestTransformer): RequestTransformer = first andThen second 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /http-testkit-client/src/main/scala/com/wix/e2e/http/client/transformers/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client 2 | 3 | package object transformers extends HttpClientTransformers -------------------------------------------------------------------------------- /http-testkit-client/src/test/scala/com/wix/e2e/http/client/drivers/PathBuilderTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.drivers 2 | 3 | import akka.http.scaladsl.model.Uri 4 | import com.wix.e2e.http.BaseUri 5 | import com.wix.test.random.{randomInt, randomStr} 6 | import org.specs2.matcher.Matcher 7 | import org.specs2.matcher.Matchers._ 8 | 9 | 10 | trait PathBuilderTestSupport { 11 | val contextRoot = s"/$randomStr" 12 | val contextRootWithMultiplePaths = s"/$randomStr/$randomStr/$randomStr" 13 | val relativePath = s"/$randomStr" 14 | val relativePathWithMultipleParts = s"/$randomStr/$randomStr/$randomStr" 15 | val baseUri = BaseUriGen.random 16 | val escapedCharacters = "!'();:@+$,/?%#[]\"'/\\" //&= 17 | } 18 | 19 | object BaseUriGen { 20 | def random: BaseUri = BaseUri(randomStr.toLowerCase, randomInt(1, 65536), Some(s"/$randomStr")) 21 | } 22 | 23 | object UrlBuilderMatchers { 24 | def beUrl(url: String): Matcher[Uri] = be_===(url) ^^ { (_: Uri).toString } 25 | } 26 | -------------------------------------------------------------------------------- /http-testkit-client/src/test/scala/com/wix/e2e/http/client/extractors/HttpMessageExtractorsTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.extractors 2 | 3 | import akka.http.scaladsl.model._ 4 | import com.wix.e2e.http.api.Marshaller.Implicits._ 5 | import com.wix.e2e.http.drivers.{HttpClientTransformersTestSupport, SomePayload} 6 | import org.specs2.mutable.Spec 7 | import org.specs2.specification.Scope 8 | 9 | class HttpMessageExtractorsTest extends Spec with HttpMessageExtractors { 10 | 11 | trait ctx extends Scope with HttpClientTransformersTestSupport 12 | 13 | "Message Extractors" should { 14 | "extract response body" should { 15 | "as unmarshalled JSON" in new ctx { 16 | HttpResponse(entity = marshaller.marshall(payload)).extractAs[SomePayload] must_=== payload 17 | } 18 | 19 | "as string" in new ctx { 20 | HttpResponse(entity = HttpEntity(strBody)).extractAsString must_=== strBody 21 | } 22 | 23 | "as array of bytes" in new ctx { 24 | HttpResponse(entity = HttpEntity(someBytes)).extractAsBytes must_=== someBytes 25 | } 26 | } 27 | 28 | "extract response entity" should { 29 | "as unmarshalled JSON" in new ctx { 30 | HttpEntity(marshaller.marshall(payload)).extractAs[SomePayload] must_=== payload 31 | } 32 | 33 | "as string" in new ctx { 34 | HttpEntity(strBody).extractAsString must_=== strBody 35 | } 36 | 37 | "as array of bytes" in new ctx { 38 | HttpEntity(someBytes).extractAsBytes must_=== someBytes 39 | } 40 | } 41 | 42 | "extract request body" should { 43 | "as unmarshalled JSON" in new ctx { 44 | HttpRequest(entity = marshaller.marshall(payload)).extractAs[SomePayload] must_=== payload 45 | } 46 | 47 | "as string" in new ctx { 48 | HttpRequest(entity = HttpEntity(strBody)).extractAsString must_=== strBody 49 | } 50 | 51 | "as array of bytes" in new ctx { 52 | HttpRequest(entity = HttpEntity(someBytes)).extractAsBytes must_=== someBytes 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /http-testkit-client/src/test/scala/com/wix/e2e/http/client/internals/PathBuilderTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client.internals 2 | 3 | import java.net.URLEncoder 4 | 5 | import com.wix.e2e.http.client.drivers.PathBuilderTestSupport 6 | import com.wix.e2e.http.client.drivers.UrlBuilderMatchers._ 7 | import org.specs2.mutable.SpecWithJUnit 8 | import org.specs2.specification.Scope 9 | 10 | class PathBuilderTest extends SpecWithJUnit { 11 | 12 | trait ctx extends Scope 13 | with PathBuilderTestSupport 14 | 15 | "Url building" should { 16 | 17 | "handle context path with single path" in new ctx { 18 | baseUri.copy(contextRoot = Some(contextRoot)).asUri must beUrl(s"http://${baseUri.host}:${baseUri.port}$contextRoot") 19 | } 20 | 21 | "handle empty context root" in new ctx { 22 | baseUri.copy(contextRoot = None).asUri must beUrl(s"http://${baseUri.host}:${baseUri.port}") 23 | baseUri.copy(contextRoot = Some("")).asUri must beUrl(s"http://${baseUri.host}:${baseUri.port}") 24 | baseUri.copy(contextRoot = Some(" ")).asUri must beUrl(s"http://${baseUri.host}:${baseUri.port}") 25 | } 26 | 27 | "handle context path with more than one path" in new ctx { 28 | baseUri.copy(contextRoot = Some(contextRootWithMultiplePaths)).asUri must beUrl(s"http://${baseUri.host}:${baseUri.port}$contextRootWithMultiplePaths") 29 | } 30 | 31 | "handle no context and empty relative path" in new ctx { 32 | baseUri.copy(contextRoot = None).asUriWith("/") must beUrl(s"http://${baseUri.host}:${baseUri.port}") 33 | baseUri.copy(contextRoot = None).asUriWith("") must beUrl(s"http://${baseUri.host}:${baseUri.port}") 34 | baseUri.copy(contextRoot = None).asUriWith(" ") must beUrl(s"http://${baseUri.host}:${baseUri.port}") 35 | } 36 | 37 | "ignore cases in which path and context root are single slash" in new ctx { 38 | baseUri.copy(contextRoot = Some("/")).asUriWith("/") must beUrl(s"http://${baseUri.host}:${baseUri.port}") 39 | baseUri.copy(contextRoot = None).asUriWith("/") must beUrl(s"http://${baseUri.host}:${baseUri.port}") 40 | } 41 | 42 | "allow to append relative path" in new ctx { 43 | baseUri.copy(contextRoot = None).asUriWith(relativePath) must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath") 44 | baseUri.copy(contextRoot = Some("")).asUriWith(relativePath) must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath") 45 | baseUri.copy(contextRoot = Some("/")).asUriWith(relativePath) must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath") 46 | } 47 | 48 | "allow to append relative path with multiple parts" in new ctx { 49 | baseUri.copy(contextRoot = None).asUriWith(relativePathWithMultipleParts) must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePathWithMultipleParts") 50 | } 51 | 52 | "properly combine context root and relative path" in new ctx { 53 | baseUri.copy(contextRoot = Some(contextRoot)).asUriWith(relativePath) must beUrl(s"http://${baseUri.host}:${baseUri.port}$contextRoot$relativePath") 54 | baseUri.copy(contextRoot = Some(contextRootWithMultiplePaths)).asUriWith(relativePathWithMultipleParts) must beUrl(s"http://${baseUri.host}:${baseUri.port}$contextRootWithMultiplePaths$relativePathWithMultipleParts") 55 | } 56 | 57 | "support context root that doesn't start with /" in new ctx { 58 | baseUri.copy(contextRoot = Some(contextRoot.stripPrefix("/"))).asUri must beUrl(s"http://${baseUri.host}:${baseUri.port}$contextRoot") 59 | baseUri.copy(contextRoot = None).asUriWith(relativePath.stripPrefix("/")) must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath") 60 | } 61 | 62 | "support relative path with explicit request params" in new ctx { 63 | baseUri.copy(contextRoot = None).asUriWith(s"$relativePath?key=val") must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath?key=val") 64 | } 65 | 66 | "support relative path with explicit request escaped params" in new ctx { 67 | baseUri.copy(contextRoot = None).asUriWith(s"$relativePath?key=val&encoded=$escapedCharacters") must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath?key=val&encoded=${URLEncoder.encode(escapedCharacters, "UTF-8")}") 68 | } 69 | 70 | "support relative path with explicit request params without value" in new ctx { 71 | baseUri.copy(contextRoot = None).asUriWith(s"$relativePath?key") must beUrl(s"http://${baseUri.host}:${baseUri.port}$relativePath?key") 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /http-testkit-client/src/test/scala/com/wix/e2e/http/drivers/HttpClientTransformersTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.drivers 2 | 3 | import java.nio.file.{Files, Path} 4 | 5 | import akka.http.scaladsl.model.HttpRequest 6 | import com.wix.e2e.http.HttpRequest 7 | import com.wix.e2e.http.client.extractors._ 8 | import com.wix.e2e.http.client.transformers._ 9 | import com.wix.e2e.http.matchers.RequestMatcher 10 | import com.wix.test.random._ 11 | import org.specs2.matcher.Matchers.contain 12 | 13 | trait HttpClientTransformersTestSupport { 14 | val request = HttpRequest() 15 | 16 | val keyValue1 = randomStrPair 17 | val keyValue2 = randomStrPair 18 | val keyValue3 = randomStrPair 19 | val escapedCharacters = "!'();:@&=+$,/?%#[]\"'/\\" 20 | val userAgent = randomStr 21 | val someBody = randomStr 22 | val someBytes = randomBytes(100) 23 | val payload = SomePayload(randomStr, randomStr) 24 | val strBody = randomStr 25 | 26 | val partName = randomStr 27 | val fileNameOpt = randomStrOpt 28 | 29 | val plainRequestPart = randomStr -> PlainRequestPart(randomStr) 30 | val plainRequestXmlPart = randomStr -> PlainRequestPart(randomStr, HttpClientContentTypes.XmlContent) 31 | val binaryRequestPart = randomStr -> BinaryRequestPart(randomBytes(20)) 32 | val binaryRequestXmlPart = randomStr -> BinaryRequestPart(randomBytes(20), HttpClientContentTypes.XmlContent) 33 | val binaryRequestXmlPartAndFilename = randomStr -> BinaryRequestPart(randomBytes(20), HttpClientContentTypes.XmlContent, fileNameOpt) 34 | 35 | 36 | def givenFileWith(content: Array[Byte]): Path = { 37 | val f = Files.createTempFile("multipart", ".tmp") 38 | Files.write(f, content) 39 | f 40 | } 41 | } 42 | 43 | case class SomePayload(key: String, value: String) 44 | 45 | object HttpClientTransformersMatchers extends HttpClientTransformers { 46 | 47 | def haveBodyPartWith(part: (String, PlainRequestPart)): RequestMatcher = 48 | ( contain(s"""Content-Disposition: form-data; name="${part._1}"""") and 49 | contain(s"""Content-Type: ${part._2.contentType.value}""") and 50 | contain(part._2.body) ) ^^ { (_: HttpRequest).entity.extractAsString } 51 | 52 | // todo: matcher binary data on multipart request 53 | def haveBinaryBodyPartWith(part: (String, BinaryRequestPart)): RequestMatcher = 54 | ( contain(s"""Content-Disposition: form-data;""") and 55 | contain(s"""; name="${part._1}"""") and 56 | (if (part._2.filename.isEmpty) contain(";") else contain(s"""; filename="${part._2.filename.get}""")) and 57 | contain(s"""Content-Type: ${part._2.contentType.value}""") and 58 | contain(s"""Content-Type: ${part._2.contentType.value}""") /*and 59 | contain(part._2.body)*/ ) ^^ { (_: HttpRequest).entity.extractAsString } // todo: match body 60 | 61 | def haveFileBodyPartWith(part: (String, FileRequestPart)): RequestMatcher = 62 | ( contain(s"""Content-Disposition: form-data;""") and 63 | contain(s"""; name="${part._1}"""") and 64 | (if (part._2.filename.isEmpty) contain(";") else contain(s"""; filename="${part._2.filename.get}""")) and 65 | contain(s"""Content-Type: ${part._2.contentType.value}""") and 66 | contain(s"""Content-Type: ${part._2.contentType.value}""") /*and 67 | contain(part._2.body)*/ ) ^^ { (_: HttpRequest).entity.extractAsString } // todo: match body 68 | 69 | } 70 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/BaseUri.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | @implicitNotFound( 6 | """Cannot find system under test host/port. 7 | Please specify implicit val baseUri: BaseUri parameter.""") 8 | case class BaseUri(host: String = "localhost", port: Int, contextRoot: Option[String] = None) 9 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/WixHttpTestkitResources.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http 2 | 3 | import java.util.concurrent.Executors 4 | 5 | import akka.actor.ActorSystem 6 | import akka.stream.SystemMaterializer 7 | import com.wix.e2e.http.utils._ 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.xml.PrettyPrinter 11 | 12 | object WixHttpTestkitResources { 13 | implicit val system = ActorSystem("wix-http-testkit") 14 | implicit val materializer = SystemMaterializer.get(system).materializer 15 | private val threadPool = Executors.newCachedThreadPool 16 | implicit val executionContext = ExecutionContext.fromExecutor(threadPool) 17 | 18 | system.registerOnTermination { 19 | threadPool.shutdownNow() 20 | } 21 | 22 | def xmlPrinter = new PrettyPrinter(80, 2) 23 | 24 | sys.addShutdownHook { 25 | system.terminate() 26 | waitFor(system.whenTerminated) 27 | } 28 | } -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/api/Marshaller.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.api 2 | 3 | import com.wix.e2e.http.exceptions.MissingMarshallerException 4 | 5 | import scala.util.control.Exception.handling 6 | 7 | trait Marshaller { 8 | def unmarshall[T : Manifest](jsonStr: String): T 9 | def marshall[T](t: T): String 10 | } 11 | 12 | 13 | object Marshaller { 14 | object Implicits { 15 | implicit val marshaller: Marshaller = defaultMarshaller 16 | } 17 | 18 | 19 | private def defaultMarshaller = 20 | createFirst( ExternalMarshaller.lookup ) 21 | .orElse( createFirst( DefaultMarshaller.lookup ) ) 22 | .getOrElse( new NopMarshaller ) 23 | 24 | private def createFirst(classes: Iterable[Class[_]]): Option[Marshaller] = 25 | classes.foldLeft( Option.empty[Marshaller] ) { 26 | case (None, clazz) => newInstance(clazz) 27 | case (m, _) => m 28 | } 29 | 30 | private def newInstance(clazz: Class[_]): Option[Marshaller] = 31 | handling(classOf[Exception]) 32 | .by( { _ => 33 | println(s"[ERROR]: Failed to create marshaller instance [$clazz].") 34 | None 35 | }) { 36 | Some(clazz.getConstructor() 37 | .newInstance() 38 | .asInstanceOf[Marshaller]) 39 | } 40 | } 41 | 42 | object DefaultMarshaller { 43 | val DefaultMarshallerClassName = "com.wix.e2e.http.json.JsonJacksonMarshaller" 44 | val HttpTestkitBundledMarshallers = Seq(DefaultMarshallerClassName, classOf[NopMarshaller].getName) 45 | 46 | def lookup: Option[Class[_]] = 47 | try { 48 | Option(Class.forName(DefaultMarshallerClassName)) 49 | } catch { 50 | case _: Exception => None 51 | } 52 | } 53 | 54 | class NopMarshaller extends Marshaller { 55 | def marshall[T](t: T): String = throwMissingMarshallerError 56 | def unmarshall[T: Manifest](jsonStr: String): T = throwMissingMarshallerError 57 | 58 | private def throwMissingMarshallerError = throw new MissingMarshallerException 59 | } 60 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/api/api.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.api 2 | 3 | import com.wix.e2e.http.{BaseUri, HttpRequest, RequestHandler} 4 | 5 | trait MockWebServer extends BaseWebServer with AdjustableServerBehavior 6 | trait StubWebServer extends BaseWebServer with RequestRecordSupport with AdjustableServerBehavior 7 | 8 | trait BaseWebServer { 9 | def baseUri: BaseUri 10 | 11 | def start(): this.type 12 | def stop(): this.type 13 | } 14 | 15 | 16 | trait RequestRecordSupport { 17 | def recordedRequests: Seq[HttpRequest] 18 | def clearRecordedRequests(): Unit 19 | } 20 | 21 | trait AdjustableServerBehavior { 22 | def appendAll(handlers: RequestHandler*): Unit 23 | def replaceWith(handlers: RequestHandler*): Unit 24 | } 25 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/config/Config.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.config 2 | 3 | import scala.concurrent.duration._ 4 | import scala.util.Try 5 | 6 | object Config { 7 | private val DefaultTimeoutConfig = "wix.config.default-timeout" 8 | 9 | val DefaultTimeout: FiniteDuration = read( DefaultTimeoutConfig ).getOrElse(5.seconds) 10 | 11 | private def read(property: String) = Option(System.getProperty(property)).flatMap( parse ) 12 | 13 | private def parse(timeoutStr: String) = Try( timeoutStr.toLong.seconds ).toOption 14 | } 15 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/exceptions/exceptions.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.exceptions 2 | 3 | import com.wix.e2e.http.BaseUri 4 | 5 | class ConnectionRefusedException(baseUri: BaseUri) extends RuntimeException(s"Unable to connect to port ${baseUri.port}") 6 | 7 | class MissingMarshallerException extends RuntimeException(s"Unable to locate marshaller in classpath, Wix HTTP Testkit supports a default marshaller or a custom marshaller\nfor more information please check documentation at https://github.com/wix/wix-http-testkit/blob/master/MARSHALLER.md") 8 | 9 | class MarshallerErrorException(content: String, t: Throwable) extends RuntimeException(s"Failed to unmarshall: [$content]", t) 10 | 11 | class UserAgentModificationNotSupportedException 12 | extends IllegalArgumentException("`user-agent` is a special header and cannot be used in `withHeaders`. Use `withUserAgent` method instead.") 13 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/info/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http 2 | 3 | package object info { 4 | val HttpTestkitVersion = "0.1.26-SNAPSHOT" 5 | } 6 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e 2 | 3 | import akka.http.scaladsl.{model => akka} 4 | 5 | package object http { 6 | type RequestHandler = PartialFunction[HttpRequest, HttpResponse] 7 | type RequestTransformer = HttpRequest => HttpRequest 8 | type HttpRequest = akka.HttpRequest 9 | type HttpResponse = akka.HttpResponse 10 | } 11 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala/com/wix/e2e/http/utils/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http 2 | 3 | import com.wix.e2e.http.config.Config.DefaultTimeout 4 | 5 | import scala.concurrent.duration._ 6 | import scala.concurrent.{Await, Future} 7 | 8 | package object utils { 9 | 10 | def awaitFor[T](future: Future[T]) (implicit atMost: Duration = DefaultTimeout): T = 11 | Await.result(future, atMost) 12 | 13 | def waitFor[T](future: Future[T]) (implicit atMost: Duration = DefaultTimeout): T = 14 | awaitFor(future) 15 | } 16 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala_2.13+/com/wix/e2e/http/api/ExternalMarshaller.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.api 2 | 3 | import com.wix.e2e.http.api.DefaultMarshaller.HttpTestkitBundledMarshallers 4 | import org.reflections.Reflections 5 | 6 | import scala.jdk.CollectionConverters._ 7 | 8 | 9 | object ExternalMarshaller { 10 | def lookup: Seq[Class[_]] = { 11 | new Reflections().getSubTypesOf(classOf[Marshaller]).asScala 12 | .filterNot( c => HttpTestkitBundledMarshallers.contains(c.getName) ) 13 | .toSeq 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /http-testkit-core/src/main/scala_2.13-/com/wix/e2e/http/api/ExternalMarshaller.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.api 2 | 3 | import com.wix.e2e.http.api.DefaultMarshaller.HttpTestkitBundledMarshallers 4 | import org.reflections.Reflections 5 | 6 | import scala.collection.JavaConverters._ 7 | 8 | object ExternalMarshaller { 9 | def lookup: Seq[Class[_]] = { 10 | new Reflections().getSubTypesOf(classOf[Marshaller]).asScala 11 | .filterNot( c => HttpTestkitBundledMarshallers.contains(c.getName) ) 12 | .toSeq 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /http-testkit-marshaller-jackson/src/main/scala/com/wix/e2e/http/json/JsonJacksonMarshaller.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.json 2 | 3 | import java.lang.reflect.{ParameterizedType, Type} 4 | 5 | import com.fasterxml.jackson.core.`type`.TypeReference 6 | import com.fasterxml.jackson.databind.ObjectMapper 7 | import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 8 | import com.fasterxml.jackson.databind.json.JsonMapper 9 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module 10 | import com.fasterxml.jackson.datatype.joda.JodaModule 11 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 12 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule 13 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 14 | import com.wix.e2e.http.api.Marshaller 15 | 16 | class JsonJacksonMarshaller extends Marshaller { 17 | 18 | def unmarshall[T : Manifest](jsonStr: String): T = objectMapper.readValue(jsonStr, typeReference[T]) 19 | def marshall[T](t: T): String = objectMapper.writeValueAsString(t) 20 | 21 | def configure: ObjectMapper = objectMapper 22 | 23 | private val objectMapper = JsonMapper.builder 24 | .addModule(new Jdk8Module()) 25 | .addModules(new JodaModule, new ParameterNamesModule, new JavaTimeModule) 26 | .addModule(new DefaultScalaModule) 27 | .disable( WRITE_DATES_AS_TIMESTAMPS ) 28 | .build() 29 | 30 | private def typeReference[T: Manifest] = new TypeReference[T] { 31 | override def getType = typeFromManifest(manifest[T]) 32 | } 33 | 34 | private def typeFromManifest(m: Manifest[_]): Type = { 35 | if (m.typeArguments.isEmpty) { m.runtimeClass } 36 | else new ParameterizedType { 37 | def getRawType = m.runtimeClass 38 | def getActualTypeArguments = m.typeArguments.map(typeFromManifest).toArray 39 | def getOwnerType = null 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /http-testkit-marshaller-jackson/src/test/scala/com/wix/e2e/http/json/JsonJacksonMarshallerTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.json 2 | 3 | import java.time.LocalDateTime 4 | import java.util.Optional 5 | 6 | import com.fasterxml.jackson.databind.ObjectMapper 7 | import com.wix.e2e.http.api.Marshaller 8 | import com.wix.e2e.http.json.MarshallingTestObjects.SomeCaseClass 9 | import com.wix.test.random._ 10 | import org.joda.time.DateTimeZone.UTC 11 | import org.joda.time.{DateTime, DateTimeZone} 12 | import org.specs2.mutable.Spec 13 | import org.specs2.specification.Scope 14 | 15 | class JsonJacksonMarshallerTest extends Spec { 16 | 17 | trait ctx extends Scope { 18 | val someStr = randomStr 19 | val javaDateTime = LocalDateTime.now() 20 | val someCaseClass = SomeCaseClass(randomStr, randomInt) 21 | val dateTime = new DateTime 22 | val dateTimeUTC = new DateTime(UTC) 23 | 24 | val marshaller: Marshaller = new JsonJacksonMarshaller 25 | } 26 | 27 | 28 | "JsonJacksonMarshaller" should { 29 | 30 | "marshall scala option properly" in new ctx { 31 | marshaller.unmarshall[Option[String]]( 32 | marshaller.marshall( Some(someStr) ) 33 | ) must beSome(someStr) 34 | } 35 | 36 | "marshall scala case classes properly" in new ctx { 37 | marshaller.unmarshall[SomeCaseClass]( 38 | marshaller.marshall( someCaseClass ) 39 | ) must_=== someCaseClass 40 | } 41 | 42 | "marshall datetime without zone" in new ctx { 43 | marshaller.unmarshall[DateTime]( 44 | marshaller.marshall( dateTime.withZone(DateTimeZone.getDefault) ) 45 | ) must_=== dateTime.withZone(UTC) 46 | } 47 | 48 | "marshall date time to textual format in UTC" in new ctx { 49 | marshaller.marshall( dateTime ) must contain(dateTime.withZone(UTC).toString) 50 | } 51 | 52 | 53 | "marshall java.time objects" in new ctx { 54 | marshaller.unmarshall[LocalDateTime]( 55 | marshaller.marshall( javaDateTime ) 56 | ) must_=== javaDateTime 57 | 58 | } 59 | 60 | "marshall java 8 Optional" in new ctx { 61 | marshaller.unmarshall[Optional[DateTime]]( 62 | marshaller.marshall( dateTimeUTC ) 63 | ) must_=== Optional.of(dateTimeUTC) 64 | 65 | marshaller.unmarshall[Optional[SomeCaseClass]]( 66 | marshaller.marshall( someCaseClass ) 67 | ) must_=== Optional.of(someCaseClass) 68 | } 69 | 70 | "expose jackson object mapper to allow external configuration" in new ctx { 71 | marshaller.asInstanceOf[JsonJacksonMarshaller].configure must beAnInstanceOf[ObjectMapper] 72 | } 73 | } 74 | } 75 | 76 | object MarshallingTestObjects { 77 | case class SomeCaseClass(s: String, i: Int) 78 | } 79 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/main/scala/com/wix/e2e/http/matchers/RequestMatchers.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers 2 | 3 | import com.wix.e2e.http.matchers.internal._ 4 | 5 | trait RequestMatchers extends RequestMethodMatchers 6 | with RequestUrlMatchers 7 | with RequestHeadersMatchers 8 | with RequestCookiesMatchers 9 | with RequestBodyMatchers 10 | with RequestRecorderMatchers 11 | with RequestContentTypeMatchers 12 | 13 | object RequestMatchers extends RequestMatchers 14 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/main/scala/com/wix/e2e/http/matchers/ResponseMatchers.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers 2 | 3 | import com.wix.e2e.http.matchers.internal._ 4 | 5 | trait ResponseMatchers extends ResponseStatusMatchers 6 | with ResponseCookiesMatchers 7 | with ResponseHeadersMatchers 8 | with ResponseBodyMatchers 9 | with ResponseSpecialHeadersMatchers 10 | with ResponseBodyAndStatusMatchers 11 | with ResponseStatusAndHeaderMatchers 12 | 13 | object ResponseMatchers extends ResponseMatchers 14 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/main/scala/com/wix/e2e/http/matchers/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http 2 | 3 | import org.scalatest.matchers.Matcher 4 | 5 | package object matchers { 6 | type ResponseMatcher = Matcher[HttpResponse] 7 | type RequestMatcher = Matcher[HttpRequest] 8 | } 9 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/drivers/MarshallerTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.drivers 2 | 3 | import com.wix.e2e.http.api.Marshaller 4 | 5 | import scala.collection.concurrent.TrieMap 6 | import scala.language.reflectiveCalls 7 | 8 | trait MarshallerTestSupport { 9 | val marshaller = new Marshaller { 10 | val unmarshallResult = TrieMap.empty[String, AnyRef] 11 | val unmarshallError = TrieMap.empty[String, Throwable] 12 | 13 | def unmarshall[T: Manifest](jsonStr: String) = { 14 | unmarshallError.get(jsonStr).foreach( throw _ ) 15 | unmarshallResult.getOrElse(jsonStr, throw new UnsupportedOperationException) 16 | .asInstanceOf[T] 17 | } 18 | 19 | def marshall[T](t: T) = ??? 20 | } 21 | 22 | def givenUnmarshallerWith[T <: AnyRef](someEntity: T, forContent: String)(implicit mn: Manifest[T]): Unit = 23 | marshaller.unmarshallResult.put(forContent, someEntity) 24 | 25 | def givenBadlyBehavingUnmarshallerFor[T : Manifest](withContent: String): Unit = 26 | marshaller.unmarshallError.put(withContent, new RuntimeException) 27 | } 28 | 29 | trait CustomMarshallerProvider { 30 | def marshaller: Marshaller 31 | implicit def customMarshaller: Marshaller = marshaller 32 | } 33 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/drivers/MatchersTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.drivers 2 | 3 | import akka.http.scaladsl.model.headers.HttpCookiePair 4 | import org.scalatest.matchers.should.Matchers._ 5 | import org.scalatest.matchers.{MatchResult, Matcher} 6 | 7 | trait MatchersTestSupport { 8 | def failureMessageFor[T](matcher: Matcher[T], matchedOn: T): String = 9 | matcher.apply( matchedOn ).failureMessage 10 | } 11 | 12 | object CommonTestMatchers { 13 | 14 | def cookieWith(value: String): Matcher[HttpCookiePair] = be(value) compose { (_: HttpCookiePair).value } 15 | 16 | case class AlwaysMatcher[T]() extends Matcher[T] { 17 | def apply(left: T): MatchResult = MatchResult(matches = true, "", "") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/drivers/RequestRecordTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.drivers 2 | 3 | import com.wix.e2e.http.HttpRequest 4 | import com.wix.e2e.http.api.RequestRecordSupport 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory.aRandomRequest 6 | import com.wix.e2e.http.matchers.drivers.RequestRecorderFactory._ 7 | 8 | trait RequestRecordTestSupport { 9 | val request = aRandomRequest 10 | val anotherRequest = aRandomRequest 11 | val yetAnotherRequest = aRandomRequest 12 | val andAnotherRequest = aRandomRequest 13 | 14 | val anEmptyRequestRecorder = aRequestRecorderWith() 15 | 16 | } 17 | 18 | object RequestRecorderFactory { 19 | 20 | def aRequestRecorderWith(requests: HttpRequest*) = new RequestRecordSupport { 21 | val recordedRequests = requests 22 | def clearRecordedRequests() = {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/RequestBodyMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.exceptions.MarshallerErrorException 4 | import com.wix.e2e.http.matchers.RequestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.MarshallingTestObjects.SomeCaseClass 7 | import com.wix.e2e.http.matchers.drivers.{CustomMarshallerProvider, HttpMessageTestSupport, MarshallerTestSupport, MatchersTestSupport} 8 | import org.scalatest.matchers.should.Matchers._ 9 | import org.scalatest.wordspec.AnyWordSpec 10 | 11 | 12 | class RequestBodyMatchersTest extends AnyWordSpec with MatchersTestSupport { 13 | 14 | trait ctx extends HttpMessageTestSupport with MarshallerTestSupport with CustomMarshallerProvider 15 | 16 | "ResponseBodyMatchers" should { 17 | 18 | "exact match on response body" in new ctx { 19 | aRequestWith(content) should haveBodyWith(content) 20 | aRequestWith(content) should not( haveBodyWith(anotherContent) ) 21 | } 22 | 23 | "match underlying matcher with body content" in new ctx { 24 | aRequestWith(content) should haveBodyThat(must = be( content )) 25 | aRequestWith(content) should not( haveBodyThat(must = be( anotherContent )) ) 26 | } 27 | 28 | "exact match on response binary body" in new ctx { 29 | aRequestWith(binaryContent) should haveBodyWith(binaryContent) 30 | aRequestWith(binaryContent) should not( haveBodyWith(anotherBinaryContent) ) 31 | } 32 | 33 | "match underlying matcher with binary body content" in new ctx { 34 | aRequestWith(binaryContent) should haveBodyDataThat(must = be( binaryContent )) 35 | aRequestWith(binaryContent) should not( haveBodyDataThat(must = be( anotherBinaryContent )) ) 36 | } 37 | 38 | "handle empty body" in new ctx { 39 | aRequestWithoutBody should not( haveBodyWith(content)) 40 | } 41 | 42 | "support unmarshalling body content with user custom unmarshaller" in new ctx { 43 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 44 | 45 | aRequestWith(content) should haveBodyWith(entity = someObject) 46 | aRequestWith(content) should not( haveBodyWith(entity = anotherObject) ) 47 | } 48 | 49 | "provide a meaningful explanation why match failed" in new ctx { 50 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 51 | 52 | failureMessageFor(haveBodyEntityThat(must = be(anotherObject)), matchedOn = aRequestWith(content)) shouldBe 53 | s"Failed to match: ['$someObject' != '$anotherObject'] with content: ['$content']" 54 | failureMessageFor(not(haveBodyEntityThat(must = be(anotherObject))), matchedOn = aRequestWith(content)) shouldBe 55 | s"Failed to match: ['$someObject'] was not equal to ['$anotherObject'] for content: ['$content']" 56 | failureMessageFor(not( haveBodyEntityThat(must = be(someObject))), matchedOn = aRequestWith(content)) shouldBe 57 | s"Failed to match: ['$someObject'] was equal to content: ['$content']" 58 | } 59 | 60 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 61 | failureMessageFor(haveBodyWith(entity = be(someObject)), matchedOn = aRequestWith(content)) shouldBe 62 | "Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyThat` instead." 63 | failureMessageFor(not( haveBodyWith(entity = be(someObject)) ), matchedOn = aRequestWith(content)) shouldBe 64 | "Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyThat` instead." 65 | } 66 | 67 | "provide a proper message to user in case of a badly behaving marshaller" in new ctx { 68 | givenBadlyBehavingUnmarshallerFor[SomeCaseClass](withContent = content) 69 | 70 | the [MarshallerErrorException] thrownBy haveBodyWith(entity = someObject).apply( aRequestWith(content) ) 71 | } 72 | 73 | "support custom matcher for user object" in new ctx { 74 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 75 | 76 | aRequestWith(content) should haveBodyEntityThat(must = be(someObject)) 77 | aRequestWith(content) should not( haveBodyEntityThat(must = be(anotherObject)) ) 78 | } 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/RequestContentTypeMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.ContentTypes._ 4 | import com.wix.e2e.http.matchers.RequestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class RequestContentTypeMatchersTest extends AnyWordSpec with MatchersTestSupport { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | "RequestContentTypeMatchers" should { 15 | 16 | "exact match on request json content type" in new ctx { 17 | aRequestWith(`application/json`) should haveJsonBody 18 | aRequestWith(`text/csv(UTF-8)`) should not( haveJsonBody ) 19 | } 20 | 21 | "exact match on request text plain content type" in new ctx { 22 | aRequestWith(`text/plain(UTF-8)`) should haveTextPlainBody 23 | aRequestWith(`text/csv(UTF-8)`) should not( haveTextPlainBody ) 24 | } 25 | 26 | "exact match on request form url encoded content type" in new ctx { 27 | aRequestWith(`application/x-www-form-urlencoded`) should haveFormUrlEncodedBody 28 | aRequestWith(`text/csv(UTF-8)`) should not( haveFormUrlEncodedBody ) 29 | } 30 | 31 | "exact match on multipart request content type" in new ctx { 32 | aRequestWith(`multipart/form-data`) should haveMultipartFormBody 33 | aRequestWith(`text/csv(UTF-8)`) should not( haveMultipartFormBody ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/RequestCookiesMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.CommonTestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class RequestCookiesMatchersTest extends AnyWordSpec with MatchersTestSupport { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | "ResponseCookiesMatchers" should { 15 | 16 | "match if cookiePair with name is found" in new ctx { 17 | aRequestWithCookies(cookiePair) should receivedCookieWith(cookiePair._1) 18 | } 19 | 20 | "failure message should describe which cookies are present and which did not match" in new ctx { 21 | failureMessageFor( receivedCookieWith(cookiePair._1), matchedOn = aRequestWithCookies(anotherCookiePair, yetAnotherCookiePair)) should 22 | include(s"Could not find cookie that matches for request contained cookies with names: ['${anotherCookiePair._1}', '${yetAnotherCookiePair._1}'") 23 | failureMessageFor( not( receivedCookieThat(be(cookiePair._1)) ), matchedOn = aRequestWithCookies(cookiePair, anotherCookiePair)) shouldBe 24 | s"Request contained a cookie that matched, request has the following cookies: ['${cookiePair._1}', '${anotherCookiePair._1}'" 25 | } 26 | 27 | "failure message for response withoout cookies will print that the response did not contain any cookies" in new ctx { 28 | failureMessageFor( receivedCookieWith(cookiePair._1), matchedOn = aRequestWithNoCookies) shouldBe 29 | "Request did not contain any Cookie headers." 30 | failureMessageFor( not( receivedCookieWith(cookiePair._1) ), matchedOn = aRequestWithNoCookies) shouldBe 31 | "Request did not contain any Cookie headers." 32 | } 33 | 34 | "allow to compose matcher with custom cookiePair matcher" in new ctx { 35 | aRequestWithCookies(cookiePair) should receivedCookieThat(must = cookieWith(cookiePair._2)) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/RequestMethodMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.HttpMethods._ 4 | import com.wix.e2e.http.matchers.RequestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpMessageTestSupport 6 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | 11 | class RequestMethodMatchersTest extends AnyWordSpec { 12 | 13 | trait ctx extends HttpMessageTestSupport 14 | 15 | "RequestMethodMatchers" should { 16 | 17 | "match all request methods" in new ctx { 18 | Seq(POST -> bePost, GET -> beGet, PUT -> bePut, DELETE -> beDelete, 19 | HEAD -> beHead, OPTIONS -> beOptions, 20 | PATCH -> bePatch, TRACE -> beTrace, CONNECT -> beConnect) 21 | .foreach { case (method, matcherForMethod) => 22 | 23 | aRequestWith( method ) should matcherForMethod 24 | aRequestWith( randomMethodThatIsNot( method )) should not( matcherForMethod ) 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/RequestUrlMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.CommonTestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | 11 | class RequestUrlMatchersTest extends AnyWordSpec with MatchersTestSupport { 12 | 13 | trait ctx extends HttpMessageTestSupport 14 | 15 | "RequestUrlMatchers" should { 16 | 17 | "match exact path" in new ctx { 18 | aRequestWithPath(somePath) should havePath(somePath) 19 | aRequestWithPath(somePath) should not( havePath(anotherPath) ) 20 | } 21 | 22 | "match exact path matcher" in new ctx { 23 | aRequestWithPath(somePath) should havePathThat(must = be( somePath )) 24 | aRequestWithPath(somePath) should not( havePathThat(must = be( anotherPath )) ) 25 | } 26 | // if first ignore first slash ??? 27 | 28 | "contain parameter will check if any parameter is present" in new ctx { 29 | aRequestWithParameters(parameter, anotherParameter) should haveAnyParamOf(parameter) 30 | aRequestWithParameters(parameter) should not( haveAnyParamOf(anotherParameter) ) 31 | } 32 | 33 | "return detailed message on hasAnyOf match failure" in new ctx { 34 | failureMessageFor(haveAnyParamOf(parameter, anotherParameter), matchedOn = aRequestWithParameters(yetAnotherParameter, andAnotherParameter)) shouldBe 35 | s"Could not find parameter [${parameter._1}, ${anotherParameter._1}] but found those: [${yetAnotherParameter._1}, ${andAnotherParameter._1}]" 36 | } 37 | 38 | "contain parameter will check if all parameters are present" in new ctx { 39 | aRequestWithParameters(parameter, anotherParameter, yetAnotherParameter) should haveAllParamFrom(parameter, anotherParameter) 40 | aRequestWithParameters(parameter, yetAnotherParameter) should not( haveAllParamFrom(parameter, anotherParameter) ) 41 | } 42 | 43 | "allOf matcher will return a message stating what was found, and what is missing from parameter list" in new ctx { 44 | failureMessageFor(haveAllParamFrom(parameter, anotherParameter), matchedOn = aRequestWithParameters(parameter, yetAnotherParameter)) shouldBe 45 | s"Could not find parameter [${anotherParameter._1}] but found those: [${parameter._1}]." 46 | } 47 | 48 | "same parameter as will check if the same parameters is present" in new ctx { 49 | aRequestWithParameters(parameter, anotherParameter) should haveTheSameParamsAs(parameter, anotherParameter) 50 | aRequestWithParameters(parameter, anotherParameter) should not( haveTheSameParamsAs(parameter) ) 51 | aRequestWithParameters(parameter) should not( haveTheSameParamsAs(parameter, anotherParameter) ) 52 | } 53 | 54 | "haveTheSameParametersAs matcher will return a message stating what was found, and what is missing from parameter list" in new ctx { 55 | failureMessageFor(haveTheSameParamsAs(parameter, anotherParameter), matchedOn = aRequestWithParameters(parameter, yetAnotherParameter)) shouldBe 56 | s"Request parameters are not identical, missing parameters from request: [${anotherParameter._1}], request contained extra parameters: [${yetAnotherParameter._1}]." 57 | } 58 | 59 | "request with no parameters will show a 'no parameters' message" in new ctx { 60 | failureMessageFor(haveAnyParamOf(parameter), matchedOn = aRequestWithNoParameters ) shouldBe 61 | "Request did not contain any request parameters." 62 | 63 | failureMessageFor(haveAllParamFrom(parameter), matchedOn = aRequestWithNoParameters ) shouldBe 64 | "Request did not contain any request parameters." 65 | 66 | failureMessageFor(haveTheSameParamsAs(parameter), matchedOn = aRequestWithNoParameters ) shouldBe 67 | "Request did not contain any request parameters." 68 | } 69 | 70 | "match if any parameter satisfy the composed matcher" in new ctx { 71 | aRequestWithParameters(parameter) should haveAnyParamThat(must = be(parameter._2), withParamName = parameter._1) 72 | aRequestWithParameters(parameter) should not( haveAnyParamThat(must = be(anotherParameter._2), withParamName = anotherParameter._1) ) 73 | } 74 | 75 | "return informative error messages" in new ctx { 76 | failureMessageFor(haveAnyParamThat(must = AlwaysMatcher(), withParamName = nonExistingParamName), matchedOn = aRequestWithParameters(parameter)) shouldBe 77 | s"Request contain parameter names: [${parameter._1}] which did not contain: [$nonExistingParamName]" 78 | failureMessageFor(haveAnyParamThat(must = AlwaysMatcher(), withParamName = nonExistingParamName), matchedOn = aRequestWithNoParameters) shouldBe 79 | "Request did not contain any parameters." 80 | failureMessageFor(haveAnyParamThat(must = be(anotherParameter._2), withParamName = parameter._1), matchedOn = aRequestWithParameters(parameter)) shouldBe 81 | s"Request parameter [${parameter._1}], did not match { ${be(anotherParameter._2).apply(parameter._2).failureMessage} }" 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseBodyAndStatusMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.api.Marshaller.Implicits.marshaller 4 | import com.wix.e2e.http.matchers.ResponseMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 6 | import com.wix.e2e.http.matchers.drivers.HttpResponseMatchers._ 7 | import com.wix.e2e.http.matchers.drivers.MarshallingTestObjects.SomeCaseClass 8 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 9 | import org.scalatest.matchers.should.Matchers._ 10 | import org.scalatest.wordspec.AnyWordSpec 11 | 12 | 13 | class ResponseBodyAndStatusMatchersTest extends AnyWordSpec with MatchersTestSupport { 14 | 15 | trait ctx extends HttpMessageTestSupport 16 | 17 | "ResponseBodyAndStatusMatchers" should { 18 | 19 | "match successful request with body content" in new ctx { 20 | aSuccessfulResponseWith(content) should beSuccessfulWith(content) 21 | aSuccessfulResponseWith(content) should not( beSuccessfulWith(anotherContent) ) 22 | } 23 | 24 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 25 | failureMessageFor(beSuccessfulWith(entity = be(content)), matchedOn = aResponseWith(content)) shouldBe 26 | s"Matcher misuse: `beSuccessfulWith` received a matcher to match against, please use `beSuccessfulWithEntityThat` instead." 27 | } 28 | 29 | "match successful request with body content matcher" in new ctx { 30 | aSuccessfulResponseWith(content) should beSuccessfulWithBodyThat(must = be( content )) 31 | aSuccessfulResponseWith(content) should not( beSuccessfulWithBodyThat(must = be( anotherContent )) ) 32 | } 33 | 34 | "match invalid request with body content" in new ctx { 35 | anInvalidResponseWith(content) should beInvalidWith(content) 36 | anInvalidResponseWith(content) should not( beInvalidWith(anotherContent) ) 37 | } 38 | 39 | "match invalid request with body content matcher" in new ctx { 40 | anInvalidResponseWith(content) should beInvalidWithBodyThat(must = be( content )) 41 | anInvalidResponseWith(content) should not( beInvalidWithBodyThat(must = be( anotherContent )) ) 42 | } 43 | 44 | "match successful request with binary body content" in new ctx { 45 | aSuccessfulResponseWith(binaryContent) should beSuccessfulWith(binaryContent) 46 | aSuccessfulResponseWith(binaryContent) should not( beSuccessfulWith(anotherBinaryContent) ) 47 | } 48 | 49 | "match successful request with binary body content matcher" in new ctx { 50 | aSuccessfulResponseWith(binaryContent) should beSuccessfulWithBodyDataThat(must = be( binaryContent )) 51 | aSuccessfulResponseWith(binaryContent) should not( beSuccessfulWithBodyDataThat(must = be( anotherBinaryContent )) ) 52 | } 53 | 54 | "match successful request with entity" in new ctx { 55 | aSuccessfulResponseWith(marshaller.marshall(someObject)) should beSuccessfulWith( someObject ) 56 | aSuccessfulResponseWith(marshaller.marshall(someObject)) should not( beSuccessfulWith( anotherObject ) ) 57 | } 58 | 59 | "match successful request with entity with custom marshaller" in new ctx { 60 | aSuccessfulResponseWith(marshaller.marshall(someObject)) should beSuccessfulWith( someObject ) 61 | aSuccessfulResponseWith(marshaller.marshall(someObject)) should not( beSuccessfulWith( anotherObject ) ) 62 | } 63 | 64 | "match successful request with entity matcher" in new ctx { 65 | aSuccessfulResponseWith(marshaller.marshall(someObject)) should beSuccessfulWithEntityThat[SomeCaseClass]( must = be( someObject ) ) 66 | aSuccessfulResponseWith(marshaller.marshall(someObject)) should not( beSuccessfulWithEntityThat[SomeCaseClass]( must = be( anotherObject ) ) ) 67 | } 68 | 69 | "match successful request with headers" in new ctx { 70 | aSuccessfulResponseWith(header, anotherHeader) should beSuccessfulWithHeaders(header, anotherHeader) 71 | aSuccessfulResponseWith(header) should not( beSuccessfulWithHeaders(anotherHeader) ) 72 | } 73 | 74 | "match successful request with header matcher" in new ctx { 75 | aSuccessfulResponseWith(header) should beSuccessfulWithHeaderThat(must = be(header._2), withHeaderName = header._1) 76 | aSuccessfulResponseWith(header) should not( beSuccessfulWithHeaderThat(must = be(anotherHeader._2), withHeaderName = header._1) ) 77 | } 78 | 79 | "match successful request with cookies" in new ctx { 80 | aSuccessfulResponseWithCookies(cookie, anotherCookie) should beSuccessfulWithCookie(cookie.name) 81 | aSuccessfulResponseWithCookies(cookie) should not( beSuccessfulWithCookie(anotherCookie.name) ) 82 | } 83 | 84 | "match successful request with cookiePair matcher" in new ctx { 85 | aSuccessfulResponseWithCookies(cookie) should beSuccessfulWithCookieThat(must = cookieWith(cookie.value)) 86 | aSuccessfulResponseWithCookies(cookie) should not( beSuccessfulWithCookieThat(must = cookieWith(anotherCookie.value)) ) 87 | } 88 | 89 | "provide a proper message to user sent a matcher to an `haveBodyWith` matcher" in new ctx { 90 | failureMessageFor(haveBodyWith(entity = be(someObject)), matchedOn = aResponseWith(content)) shouldBe 91 | s"Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyThat` instead." 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseBodyMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.exceptions.MarshallerErrorException 4 | import com.wix.e2e.http.matchers.ResponseMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 6 | import com.wix.e2e.http.matchers.drivers.MarshallingTestObjects.SomeCaseClass 7 | import com.wix.e2e.http.matchers.drivers.{CustomMarshallerProvider, HttpMessageTestSupport, MarshallerTestSupport, MatchersTestSupport} 8 | import org.scalatest.matchers.should.Matchers._ 9 | import org.scalatest.wordspec.AnyWordSpec 10 | 11 | class ResponseBodyMatchersTest extends AnyWordSpec with MatchersTestSupport { 12 | 13 | trait ctx extends HttpMessageTestSupport with MarshallerTestSupport with CustomMarshallerProvider 14 | 15 | "ResponseBodyMatchers" should { 16 | 17 | "exact match on response body" in new ctx { 18 | aResponseWith(content) should haveBodyWith(content) 19 | aResponseWith(content) should not( haveBodyWith(anotherContent) ) 20 | } 21 | 22 | "match underlying matcher with body content" in new ctx { 23 | aResponseWith(content) should haveBodyThat(must = be( content )) 24 | aResponseWith(content) should not( haveBodyThat(must = be( anotherContent )) ) 25 | } 26 | 27 | "exact match on response binary body" in new ctx { 28 | aResponseWith(binaryContent) should haveBodyWith(binaryContent) 29 | aResponseWith(binaryContent) should not( haveBodyWith(anotherBinaryContent) ) 30 | } 31 | 32 | "match underlying matcher with binary body content" in new ctx { 33 | aResponseWith(binaryContent) should haveBodyDataThat(must = be( binaryContent )) 34 | aResponseWith(binaryContent) should not( haveBodyDataThat(must = be( anotherBinaryContent )) ) 35 | } 36 | 37 | "handle empty body" in new ctx { 38 | aResponseWithoutBody should not( haveBodyWith(content)) 39 | } 40 | 41 | "support unmarshalling body content with user custom unmarshaller" in new ctx { 42 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 43 | 44 | aResponseWith(content) should haveBodyWith(entity = someObject) 45 | aResponseWith(content) should not( haveBodyWith(entity = anotherObject) ) 46 | } 47 | 48 | "provide a meaningful explanation why match failed" in new ctx { 49 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 50 | 51 | failureMessageFor(haveBodyEntityThat(must = be(anotherObject)), matchedOn = aResponseWith(content)) shouldBe 52 | s"Failed to match: ['$someObject' != '$anotherObject'] with content: [$content]" 53 | } 54 | 55 | "provide a proper message to user in case of a badly behaving marshaller" in new ctx { 56 | givenBadlyBehavingUnmarshallerFor[SomeCaseClass](withContent = content) 57 | 58 | the[MarshallerErrorException] thrownBy haveBodyWith(entity = someObject).apply( aResponseWith(content) ) 59 | } 60 | 61 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 62 | failureMessageFor(haveBodyWith(entity = be(someObject)), matchedOn = aResponseWith(content)) shouldBe 63 | s"Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyThat` instead." 64 | } 65 | 66 | "support custom matcher for user object" in new ctx { 67 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 68 | 69 | aResponseWith(content) should haveBodyEntityThat(must = be(someObject)) 70 | aResponseWith(content) should not( haveBodyEntityThat(must = be(anotherObject)) ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseContentLengthMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.scalatest.matchers.should.Matchers._ 7 | import org.scalatest.wordspec.AnyWordSpec 8 | 9 | 10 | class ResponseContentLengthMatchersTest extends AnyWordSpec with MatchersTestSupport { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | "ResponseContentLengthMatchers" should { 15 | 16 | "support matching against specific content length" in new ctx { 17 | aResponseWith(contentWith(length = length)) should haveContentLength(length = length) 18 | aResponseWith(contentWith(length = anotherLength)) should not( haveContentLength(length = length) ) 19 | } 20 | 21 | "support matching content length against response without content length" in new ctx { 22 | aResponseWithoutContentLength should not( haveContentLength(length = length) ) 23 | } 24 | 25 | "support matching against response without content length" in new ctx { 26 | aResponseWithoutContentLength should haveNoContentLength 27 | aResponseWith(contentWith(length = length)) should not( haveNoContentLength ) 28 | } 29 | 30 | "failure message should describe what was the expected content length and what was found" in new ctx { 31 | failureMessageFor(haveContentLength(length = length), matchedOn = aResponseWith(contentWith(length = anotherLength))) shouldBe 32 | s"Expected content length [$length] does not match actual content length [$anotherLength]" 33 | } 34 | 35 | "failure message should reflect that content length header was not found" in new ctx { 36 | failureMessageFor(haveContentLength(length = length), matchedOn = aResponseWithoutContentLength) shouldBe 37 | s"Expected content length [$length] but response did not contain `content-length` header." 38 | } 39 | 40 | "failure message should reflect that content length header exists while trying to match against a content length that doesn't exists" in new ctx { 41 | failureMessageFor(haveNoContentLength, matchedOn = aResponseWith(contentWith(length = length))) shouldBe 42 | s"Expected no `content-length` header but response did contain `content-length` header with size [$length]." 43 | } 44 | 45 | "failure message if someone tries to match content-length in headers matchers" in new ctx { 46 | failureMessageFor(haveAllHeadersOf(contentLengthHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 47 | """`Content-Length` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 48 | |Use `haveContentLength` matcher instead.""".stripMargin 49 | failureMessageFor(haveAnyHeadersOf(contentLengthHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 50 | """`Content-Length` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 51 | |Use `haveContentLength` matcher instead.""".stripMargin 52 | failureMessageFor(haveTheSameHeadersAs(contentLengthHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 53 | """`Content-Length` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 54 | |Use `haveContentLength` matcher instead.""".stripMargin 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseContentTypeMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.scalatest.matchers.should.Matchers._ 7 | import org.scalatest.wordspec.AnyWordSpec 8 | 9 | 10 | class ResponseContentTypeMatchersTest extends AnyWordSpec with MatchersTestSupport { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | 15 | "ResponseContentTypeMatchers" should { 16 | 17 | "support matching against json content type" in new ctx { 18 | aResponseWithContentType("application/json") should beJsonResponse 19 | aResponseWithContentType("text/plain") should not( beJsonResponse ) 20 | } 21 | 22 | "support matching against text plain content type" in new ctx { 23 | aResponseWithContentType("text/plain") should beTextPlainResponse 24 | aResponseWithContentType("application/json") should not( beTextPlainResponse ) 25 | } 26 | 27 | "support matching against form url encoded content type" in new ctx { 28 | aResponseWithContentType("application/x-www-form-urlencoded") should beFormUrlEncodedResponse 29 | aResponseWithContentType("application/json") should not( beFormUrlEncodedResponse ) 30 | } 31 | 32 | "show proper error in case matching against a malformed content type" in new ctx { 33 | failureMessageFor(haveContentType(malformedContentType), matchedOn = aResponseWithContentType(anotherContentType)) should 34 | include(s"Cannot match against a malformed content type: $malformedContentType") 35 | } 36 | 37 | "support matching against content type" in new ctx { 38 | aResponseWithContentType(contentType) should haveContentType(contentType) 39 | } 40 | 41 | "failure message should describe what was the expected content type and what was found" in new ctx { 42 | failureMessageFor(haveContentType(contentType), matchedOn = aResponseWithContentType(anotherContentType)) shouldBe 43 | s"Expected content type [$contentType] does not match actual content type [$anotherContentType]" 44 | } 45 | 46 | "failure message in case no content type for body should be handled" in new ctx { 47 | failureMessageFor(haveContentType(contentType), matchedOn = aResponseWithoutBody) shouldBe 48 | "Request body does not have a set content type" 49 | } 50 | 51 | "failure message if someone tries to match content-type in headers matchers" in new ctx { 52 | failureMessageFor(haveAllHeadersOf(contentTypeHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 53 | """`Content-Type` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 54 | |Use `haveContentType` matcher instead (or `beJsonResponse`, `beTextPlainResponse`, `beFormUrlEncodedResponse`).""".stripMargin 55 | failureMessageFor(haveAnyHeadersOf(contentTypeHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 56 | """`Content-Type` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 57 | |Use `haveContentType` matcher instead (or `beJsonResponse`, `beTextPlainResponse`, `beFormUrlEncodedResponse`).""".stripMargin 58 | failureMessageFor(haveTheSameHeadersAs(contentTypeHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 59 | """`Content-Type` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 60 | |Use `haveContentType` matcher instead (or `beJsonResponse`, `beTextPlainResponse`, `beFormUrlEncodedResponse`).""".stripMargin 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseCookiesMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseMatchers._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class ResponseCookiesMatchersTest extends AnyWordSpec with MatchersTestSupport { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | "ResponseCookiesMatchers" should { 15 | 16 | "match if cookiePair with name is found" in new ctx { 17 | aResponseWithCookies(cookie) should receivedCookieWith(cookie.name) 18 | } 19 | 20 | "failure message should describe which cookies are present and which did not match" in new ctx { 21 | failureMessageFor(receivedCookieWith(cookie.name), matchedOn = aResponseWithCookies(anotherCookie, yetAnotherCookie)) should 22 | ( include(cookie.name) and include(anotherCookie.name) and include(yetAnotherCookie.name) ) 23 | } 24 | 25 | "failure message for response withoout cookies will print that the response did not contain any cookies" in new ctx { 26 | receivedCookieWith(cookie.name).apply( aResponseWithNoCookies ).failureMessage should 27 | include("Response did not contain any `Set-Cookie` headers.") 28 | } 29 | 30 | "allow to compose matcher with custom cookiePair matcher" in new ctx { 31 | aResponseWithCookies(cookie) should receivedCookieThat(must = cookieWith(cookie.value)) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseHeadersMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.CommonTestMatchers.AlwaysMatcher 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | 11 | class ResponseHeadersMatchersTest extends AnyWordSpec with MatchersTestSupport { 12 | 13 | trait ctx extends HttpMessageTestSupport 14 | 15 | "ResponseHeadersMatchers" should { 16 | 17 | "contain header will check if any header is present" in new ctx { 18 | aResponseWithHeaders(header, anotherHeader) should haveAnyHeadersOf(header) 19 | } 20 | 21 | "return detailed message on hasAnyOf match failure" in new ctx { 22 | failureMessageFor(haveAnyHeadersOf(header, anotherHeader), matchedOn = aResponseWithHeaders(yetAnotherHeader, andAnotherHeader)) shouldBe 23 | s"Could not find header [${header._1}, ${anotherHeader._1}] but found those: [${yetAnotherHeader._1}, ${andAnotherHeader._1}]" 24 | } 25 | 26 | "contain header will check if all headers are present" in new ctx { 27 | aResponseWithHeaders(header, anotherHeader, yetAnotherHeader) should haveAllHeadersOf(header, anotherHeader) 28 | } 29 | 30 | "allOf matcher will return a message stating what was found, and what is missing from header list" in new ctx { 31 | failureMessageFor(haveAllHeadersOf(header, anotherHeader), matchedOn = aResponseWithHeaders(yetAnotherHeader, header)) shouldBe 32 | s"Could not find header [${anotherHeader._1}] but found those: [${header._1}]." 33 | } 34 | 35 | "same header as will check if the same headers is present" in new ctx { 36 | aResponseWithHeaders(header, anotherHeader) should haveTheSameHeadersAs(header, anotherHeader) 37 | aResponseWithHeaders(header, anotherHeader) should not( haveTheSameHeadersAs(header) ) 38 | aResponseWithHeaders(header) should not( haveTheSameHeadersAs(header, anotherHeader) ) 39 | } 40 | 41 | "haveTheSameHeadersAs matcher will return a message stating what was found, and what is missing from header list" in new ctx { 42 | failureMessageFor(haveTheSameHeadersAs(header, anotherHeader), matchedOn = aResponseWithHeaders(yetAnotherHeader, header)) shouldBe 43 | s"Request header is not identical, missing headers from request: [${anotherHeader._1}], request contained extra headers: [${yetAnotherHeader._1}]." 44 | } 45 | 46 | "header name compare should be case insensitive" in new ctx { 47 | aResponseWithHeaders(header) should haveAnyHeadersOf(header.copy(_1 = header._1.toUpperCase)) 48 | aResponseWithHeaders(header) should not( haveAnyHeadersOf(header.copy(_2 = header._2.toUpperCase)) ) 49 | 50 | aResponseWithHeaders(header) should haveAllHeadersOf(header.copy(_1 = header._1.toUpperCase)) 51 | aResponseWithHeaders(header) should not( haveAllHeadersOf(header.copy(_2 = header._2.toUpperCase)) ) 52 | 53 | aResponseWithHeaders(header) should haveTheSameHeadersAs(header.copy(_1 = header._1.toUpperCase)) 54 | aResponseWithHeaders(header) should not( haveTheSameHeadersAs(header.copy(_2 = header._2.toUpperCase)) ) 55 | } 56 | 57 | "request with no headers will show a 'no headers' message" in new ctx { 58 | failureMessageFor(haveAnyHeadersOf(header), matchedOn = aResponseWithNoHeaders ) shouldBe 59 | "Response did not contain any headers." 60 | 61 | failureMessageFor(haveAllHeadersOf(header), matchedOn = aResponseWithNoHeaders ) shouldBe 62 | "Response did not contain any headers." 63 | 64 | failureMessageFor(haveTheSameHeadersAs(header), matchedOn = aResponseWithNoHeaders ) shouldBe 65 | "Response did not contain any headers." 66 | } 67 | 68 | "ignore cookies and set cookies from headers comparison" in new ctx { 69 | aResponseWithCookies(cookie) should not( haveAnyHeadersOf("Set-Cookie" -> s"${cookie.name}=${cookie.value}") ) 70 | aResponseWithCookies(cookie) should not( haveAllHeadersOf("Set-Cookie" -> s"${cookie.name}=${cookie.value}") ) 71 | aResponseWithCookies(cookie) should not( haveTheSameHeadersAs("Set-Cookie" -> s"${cookie.name}=${cookie.value}") ) 72 | } 73 | 74 | "match if any header satisfy the composed matcher" in new ctx { 75 | aResponseWithHeaders(header) should haveAnyHeaderThat(must = be(header._2), withHeaderName = header._1) 76 | aResponseWithHeaders(header) should not( haveAnyHeaderThat(must = be(anotherHeader._2), withHeaderName = header._1) ) 77 | } 78 | 79 | "return informative error messages" in new ctx { 80 | failureMessageFor(haveAnyHeaderThat(must = AlwaysMatcher(), withHeaderName = nonExistingHeaderName), matchedOn = aResponseWithHeaders(header)) shouldBe 81 | s"Response contain header names: [${header._1}] which did not contain: [$nonExistingHeaderName]" 82 | failureMessageFor(haveAnyHeaderThat(must = AlwaysMatcher(), withHeaderName = nonExistingHeaderName), matchedOn = aResponseWithNoHeaders) shouldBe 83 | "Response did not contain any headers." 84 | failureMessageFor(haveAnyHeaderThat(must = be(anotherHeader._2), withHeaderName = header._1), matchedOn = aResponseWithHeaders(header)) shouldBe 85 | s"Response header [${header._1}], did not match { ${be(anotherHeader._2).apply(header._2).failureMessage} }" 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseStatusAndHeaderMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.StatusCodes.{Found, MovedPermanently} 4 | import com.wix.e2e.http.matchers.ResponseMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class ResponseStatusAndHeaderMatchersTest extends AnyWordSpec with MatchersTestSupport { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | "ResponseStatusAndHeaderMatchers" should { 15 | 16 | "match against a response that is temporarily redirected to url" in new ctx { 17 | aRedirectResponseTo(url) should beRedirectedTo(url) 18 | aRedirectResponseTo(url) should not( beRedirectedTo(anotherUrl) ) 19 | aRedirectResponseTo(url).withStatus(randomStatusThatIsNot(Found)) should not( beRedirectedTo(url) ) 20 | } 21 | 22 | "match against a response that is permanently redirected to url" in new ctx { 23 | aPermanentlyRedirectResponseTo(url) should bePermanentlyRedirectedTo(url) 24 | aPermanentlyRedirectResponseTo(url) should not( bePermanentlyRedirectedTo(anotherUrl) ) 25 | aPermanentlyRedirectResponseTo(url).withStatus(randomStatusThatIsNot(MovedPermanently)) should not( bePermanentlyRedirectedTo(url) ) 26 | } 27 | 28 | "match against url params even if params has a different order" in new ctx { 29 | aRedirectResponseTo(s"$url?param1=val1¶m2=val2") should beRedirectedTo(s"$url?param2=val2¶m1=val1") 30 | aPermanentlyRedirectResponseTo(s"$url?param1=val1¶m2=val2") should bePermanentlyRedirectedTo(s"$url?param2=val2¶m1=val1") 31 | } 32 | 33 | "match will fail for different protocol" in new ctx { 34 | aRedirectResponseTo(s"http://example.com") should not( beRedirectedTo(s"https://example.com") ) 35 | aPermanentlyRedirectResponseTo(s"http://example.com") should not( bePermanentlyRedirectedTo(s"https://example.com") ) 36 | } 37 | 38 | "match will fail for different host and port" in new ctx { 39 | aRedirectResponseTo(s"http://example.com") should not( beRedirectedTo(s"http://example.org") ) 40 | aRedirectResponseTo(s"http://example.com:99") should not( beRedirectedTo(s"http://example.com:81") ) 41 | aPermanentlyRedirectResponseTo(s"http://example.com") should not( bePermanentlyRedirectedTo(s"http://example.org") ) 42 | aPermanentlyRedirectResponseTo(s"http://example.com:99") should not( bePermanentlyRedirectedTo(s"http://example.com:81") ) 43 | } 44 | 45 | "port 80 is removed by akka http" in new ctx { 46 | aRedirectResponseTo(s"http://example.com:80") should beRedirectedTo(s"http://example.com") 47 | aPermanentlyRedirectResponseTo(s"http://example.com:80") should bePermanentlyRedirectedTo(s"http://example.com") 48 | } 49 | 50 | "match will fail for different path" in new ctx { 51 | aRedirectResponseTo(s"http://example.com/path1") should not( beRedirectedTo(s"http://example.com/path2") ) 52 | aPermanentlyRedirectResponseTo(s"http://example.com/path1") should not( bePermanentlyRedirectedTo(s"http://example.org/path2") ) 53 | } 54 | 55 | "match will fail for different hash fragment" in new ctx { 56 | aRedirectResponseTo(s"http://example.com/path#fragment") should not( beRedirectedTo(s"http://example.com/path#anotherFxragment") ) 57 | aPermanentlyRedirectResponseTo(s"http://example.com/path#fragment") should not( bePermanentlyRedirectedTo(s"http://example.com/path#anotherFxragment") ) 58 | } 59 | 60 | "failure message in case response does not have location header" in new ctx { 61 | failureMessageFor(beRedirectedTo(url), matchedOn = aRedirectResponseWithoutLocationHeader) shouldBe 62 | "Response does not contain Location header." 63 | failureMessageFor(bePermanentlyRedirectedTo(url), matchedOn = aPermanentlyRedirectResponseWithoutLocationHeader) shouldBe 64 | "Response does not contain Location header." 65 | } 66 | 67 | "failure message in case trying to match against a malformed url" in new ctx { 68 | failureMessageFor(beRedirectedTo(malformedUrl), matchedOn = aRedirectResponseTo(url)) shouldBe 69 | s"Matching against a malformed url: [$malformedUrl]." 70 | failureMessageFor(bePermanentlyRedirectedTo(malformedUrl), matchedOn = aPermanentlyRedirectResponseTo(url)) shouldBe 71 | s"Matching against a malformed url: [$malformedUrl]." 72 | } 73 | 74 | "failure message in case response have different urls should show the actual url and the expected url" in new ctx { 75 | failureMessageFor(beRedirectedTo(url), matchedOn = aRedirectResponseTo(s"$url?param1=val1")) shouldBe 76 | s"""Response is redirected to a different url: 77 | |actual: $url?param1=val1 78 | |expected: $url 79 | |""".stripMargin 80 | failureMessageFor(bePermanentlyRedirectedTo(url), matchedOn = aPermanentlyRedirectResponseTo(s"$url?param1=val1")) shouldBe 81 | s"""Response is redirected to a different url: 82 | |actual: $url?param1=val1 83 | |expected: $url 84 | |""".stripMargin 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseStatusMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.StatusCodes._ 4 | import com.wix.e2e.http.matchers.ResponseMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpMessageTestSupport 6 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 7 | import org.scalatest.matchers.should.Matchers._ 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class ResponseStatusMatchersTest extends AnyWordSpec { 11 | 12 | trait ctx extends HttpMessageTestSupport 13 | 14 | 15 | "ResponseStatusMatchers" should { 16 | Seq(OK -> beSuccessful, NoContent -> beNoContent, Created -> beSuccessfullyCreated, Accepted -> beAccepted, // 2xx 17 | 18 | Found -> beRedirect, MovedPermanently -> bePermanentlyRedirect, //3xx 19 | 20 | // 4xx 21 | Forbidden -> beRejected, NotFound -> beNotFound, BadRequest -> beInvalid, PayloadTooLarge -> beRejectedTooLarge, 22 | Unauthorized -> beUnauthorized, MethodNotAllowed -> beNotSupported, Conflict -> beConflict, PreconditionFailed -> bePreconditionFailed, 23 | UnprocessableEntity -> beUnprocessableEntity, PreconditionRequired -> bePreconditionRequired, TooManyRequests -> beTooManyRequests, 24 | RequestHeaderFieldsTooLarge -> beRejectedRequestTooLarge, 25 | 26 | ServiceUnavailable -> beUnavailable, InternalServerError -> beInternalServerError, NotImplemented -> beNotImplemented // 5xx 27 | ).foreach { case (status, matcherForStatus) => 28 | 29 | s"match against status ${status.value}" in new ctx { 30 | aResponseWith( status ) should matcherForStatus 31 | aResponseWith( randomStatusThatIsNot(status) ) should not( matcherForStatus ) 32 | } 33 | } 34 | 35 | "allow matching against status code" in new ctx { 36 | val status = randomStatus 37 | aResponseWith( status ) should haveStatus(code = status.intValue ) 38 | aResponseWith( status ) should not( haveStatus(code = randomStatusThatIsNot(status).intValue ) ) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /http-testkit-scala-test/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseTransferEncodingMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.TransferEncodings 4 | import akka.http.scaladsl.model.TransferEncodings._ 5 | import com.wix.e2e.http.matchers.ResponseMatchers._ 6 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 7 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 8 | import org.scalatest.matchers.should.Matchers._ 9 | import org.scalatest.wordspec.AnyWordSpec 10 | 11 | 12 | class ResponseTransferEncodingMatchersTest extends AnyWordSpec with MatchersTestSupport { 13 | 14 | trait ctx extends HttpMessageTestSupport 15 | 16 | 17 | "ResponseTransferEncodingMatchersTest" should { 18 | 19 | "support matching against chunked transfer encoding" in new ctx { 20 | aChunkedResponse should beChunkedResponse 21 | aResponseWithoutTransferEncoding should not( beChunkedResponse ) 22 | aResponseWithTransferEncodings(compress) should not( beChunkedResponse ) 23 | aResponseWithTransferEncodings(chunked) should beChunkedResponse 24 | } 25 | 26 | "failure message in case no transfer encoding header should state that response did not have the proper header" in new ctx { 27 | failureMessageFor(beChunkedResponse, matchedOn = aResponseWithoutTransferEncoding) shouldBe 28 | "Expected Chunked response while response did not contain `Transfer-Encoding` header" 29 | } 30 | 31 | "failure message in case transfer encoding header exists should state that transfer encoding has a different value" in new ctx { 32 | failureMessageFor(beChunkedResponse, matchedOn = aResponseWithTransferEncodings(compress, TransferEncodings.deflate)) shouldBe 33 | "Expected Chunked response while response has `Transfer-Encoding` header with values ['compress', 'deflate']" 34 | } 35 | 36 | "support matching against transfer encoding header values" in new ctx { 37 | aResponseWithTransferEncodings(compress) should haveTransferEncodings("compress") 38 | aResponseWithTransferEncodings(compress) should not( haveTransferEncodings("deflate") ) 39 | } 40 | 41 | "support matching against transfer encoding header with multiple values, matcher will validate that response has all of the expected values" in new ctx { 42 | aResponseWithTransferEncodings(compress, deflate) should haveTransferEncodings("deflate", "compress") 43 | aResponseWithTransferEncodings(compress, deflate) should haveTransferEncodings("compress") 44 | } 45 | 46 | "properly match chunked encoding" in new ctx { 47 | aChunkedResponse should haveTransferEncodings("chunked") 48 | aChunkedResponseWith(compress) should haveTransferEncodings("compress", "chunked") 49 | aChunkedResponseWith(compress) should haveTransferEncodings("chunked") 50 | } 51 | 52 | "failure message should describe what was the expected transfer encodings and what was found" in new ctx { 53 | failureMessageFor(haveTransferEncodings("deflate", "compress"), matchedOn = aChunkedResponseWith(gzip)) shouldBe 54 | s"Expected transfer encodings ['deflate', 'compress'] does not match actual transfer encoding ['chunked', 'gzip']" 55 | } 56 | 57 | "failure message in case no Transfer-Encoding for response should be handled" in new ctx { 58 | failureMessageFor(haveTransferEncodings("chunked"), matchedOn = aResponseWithoutTransferEncoding) shouldBe 59 | "Response did not contain `Transfer-Encoding` header." 60 | } 61 | 62 | "failure message if someone tries to match content-type in headers matchers" in new ctx { 63 | failureMessageFor(haveAllHeadersOf(transferEncodingHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 64 | """`Transfer-Encoding` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 65 | |Use `beChunkedResponse` or `haveTransferEncodings` matcher instead.""".stripMargin 66 | failureMessageFor(haveAnyHeadersOf(transferEncodingHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 67 | """`Transfer-Encoding` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 68 | |Use `beChunkedResponse` or `haveTransferEncodings` matcher instead.""".stripMargin 69 | failureMessageFor(haveTheSameHeadersAs(transferEncodingHeader), matchedOn = aResponseWithContentType(contentType)) shouldBe 70 | """`Transfer-Encoding` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 71 | |Use `beChunkedResponse` or `haveTransferEncodings` matcher instead.""".stripMargin 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /http-testkit-server/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = "ERROR" 3 | } -------------------------------------------------------------------------------- /http-testkit-server/src/main/scala/com/wix/e2e/http/server/builders/builders.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.server.builders 2 | 3 | import com.wix.e2e.http.RequestHandler 4 | import com.wix.e2e.http.api.{MockWebServer, StubWebServer} 5 | import com.wix.e2e.http.server.internals.{MockAkkaHttpWebServer, StubAkkaHttpMockWebServer} 6 | 7 | class StubWebServerBuilder(handlers: Seq[RequestHandler], port: Option[Int]) { 8 | 9 | def onPort(port: Int) = new StubWebServerBuilder(handlers, port = Option(port)) 10 | def addHandlers(handler: RequestHandler, handlers: RequestHandler*) = new StubWebServerBuilder(handlers = this.handlers ++ (handler +: handlers), port) 11 | def addHandler(handler: RequestHandler) = addHandlers(handler) 12 | 13 | def build: StubWebServer = new StubAkkaHttpMockWebServer(handlers, port) 14 | } 15 | 16 | class MockWebServerBuilder(handlers: Seq[RequestHandler], port: Option[Int]) { 17 | 18 | def onPort(port: Int) = new MockWebServerBuilder(handlers = handlers, Option(port)) 19 | 20 | def build: MockWebServer = new MockAkkaHttpWebServer(handlers, port) 21 | } 22 | -------------------------------------------------------------------------------- /http-testkit-server/src/main/scala/com/wix/e2e/http/server/internals/AkkaHttpMockWebServer.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.server.internals 2 | 3 | import akka.http.scaladsl.Http 4 | import akka.http.scaladsl.Http.ServerBinding 5 | import akka.http.scaladsl.model.headers.{ProductVersion, Server} 6 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 7 | import akka.http.scaladsl.settings.ServerSettings 8 | import com.wix.e2e.http.api.BaseWebServer 9 | import com.wix.e2e.http.info.HttpTestkitVersion 10 | import com.wix.e2e.http.utils._ 11 | import com.wix.e2e.http.{BaseUri, RequestHandler, WixHttpTestkitResources} 12 | 13 | import scala.concurrent.Future 14 | import scala.concurrent.duration._ 15 | 16 | abstract class AkkaHttpMockWebServer(specificPort: Option[Int], val initialHandlers: Seq[RequestHandler]) 17 | extends BaseWebServer 18 | with AdjustableServerBehaviorSupport { 19 | 20 | import WixHttpTestkitResources.{executionContext, materializer, system} 21 | 22 | protected def serverBehavior: RequestHandler 23 | 24 | def start() = this.synchronized { 25 | val s = waitFor( Http().newServerAt("localhost", 26 | port = specificPort.getOrElse( AllocateDynamicPort )) 27 | .withSettings( customSettings ) 28 | .bind(TransformToStrictAndHandle) ) 29 | serverBinding = Option(s) 30 | println(s"Web server started on port: ${baseUri.port}.") 31 | this 32 | } 33 | 34 | def stop() = this.synchronized { 35 | serverBinding.foreach{ s => 36 | waitFor( s.unbind() ) 37 | } 38 | serverBinding = None 39 | this 40 | } 41 | 42 | def baseUri = 43 | specificPort.map( p => BaseUri("localhost", port = p) ) 44 | .orElse( serverBinding.map( s => BaseUri(port = s.localAddress.getPort) )) 45 | .getOrElse( throw new IllegalStateException("Server port and baseUri will have value after server is started") ) 46 | 47 | private var serverBinding: Option[ServerBinding] = None 48 | private val AllocateDynamicPort = 0 49 | private val TransformToStrictAndHandle: HttpRequest => Future[HttpResponse] = _.toStrict(1.minutes).map( serverBehavior ) 50 | private def customSettings = { 51 | val settings = ServerSettings(system) 52 | settings.withTransparentHeadRequests(false) 53 | .withParserSettings( settings.parserSettings 54 | .withMaxHeaderValueLength(32 * 1024) ) 55 | .withServerHeader( Some(Server(ProductVersion("server-http-testkit", HttpTestkitVersion))) ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /http-testkit-server/src/main/scala/com/wix/e2e/http/server/internals/StubAkkaHttpMockWebServer.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.server.internals 2 | 3 | import akka.http.scaladsl.model.{HttpResponse, StatusCodes} 4 | import com.wix.e2e.http._ 5 | import com.wix.e2e.http.api.{AdjustableServerBehavior, MockWebServer, StubWebServer} 6 | 7 | import scala.collection.mutable.ListBuffer 8 | 9 | class StubAkkaHttpMockWebServer(initialHandlers: Seq[RequestHandler], specificPort: Option[Int]) 10 | extends AkkaHttpMockWebServer(specificPort, initialHandlers) 11 | with StubWebServer { 12 | 13 | 14 | def recordedRequests: Seq[HttpRequest] = this.synchronized { 15 | requests.toSeq 16 | } 17 | 18 | def clearRecordedRequests() = this.synchronized { 19 | requests.clear() 20 | } 21 | 22 | private val requests = ListBuffer.empty[HttpRequest] 23 | 24 | private val SuccessfulHandler: RequestHandler = { case _ => HttpResponse(status = StatusCodes.OK) } 25 | private def StubServerHandlers = (currentHandlers :+ SuccessfulHandler).reduce(_ orElse _) 26 | private val RequestRecorderHandler: RequestHandler = { case r => 27 | this.synchronized { 28 | requests.append(r) 29 | } 30 | StubServerHandlers.apply(r) 31 | } 32 | 33 | protected val serverBehavior = RequestRecorderHandler 34 | } 35 | 36 | class MockAkkaHttpWebServer(initialHandlers: Seq[RequestHandler], specificPort: Option[Int]) 37 | extends AkkaHttpMockWebServer(specificPort, initialHandlers) 38 | with MockWebServer { 39 | 40 | private val NotFoundHandler: RequestHandler = { case _ => HttpResponse(status = StatusCodes.NotFound) } 41 | private def MockServerHandlers = (currentHandlers :+ NotFoundHandler).reduce(_ orElse _) 42 | private val AdjustableHandler: RequestHandler = { case r => 43 | MockServerHandlers.apply(r) 44 | } 45 | 46 | protected val serverBehavior = AdjustableHandler 47 | } 48 | 49 | trait AdjustableServerBehaviorSupport extends AdjustableServerBehavior { 50 | private val localHandlers: ListBuffer[RequestHandler] = ListBuffer(initialHandlers:_*) 51 | 52 | def initialHandlers: Seq[RequestHandler] 53 | 54 | def currentHandlers: Seq[RequestHandler] = this.synchronized { 55 | localHandlers.toSeq 56 | } 57 | 58 | def appendAll(handlers: RequestHandler*) = this.synchronized { 59 | localHandlers.appendAll(handlers) 60 | } 61 | 62 | def replaceWith(handlers: RequestHandler*) = this.synchronized { 63 | localHandlers.clear() 64 | appendAll(handlers:_*) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/main/scala/com/wix/e2e/http/matchers/RequestMatchers.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers 2 | 3 | import com.wix.e2e.http.matchers.internal._ 4 | 5 | trait RequestMatchers extends RequestMethodMatchers 6 | with RequestUrlMatchers 7 | with RequestHeadersMatchers 8 | with RequestCookiesMatchers 9 | with RequestBodyMatchers 10 | with RequestRecorderMatchers 11 | with RequestContentTypeMatchers 12 | 13 | object RequestMatchers extends RequestMatchers 14 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/main/scala/com/wix/e2e/http/matchers/ResponseMatchers.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers 2 | 3 | import com.wix.e2e.http.matchers.internal._ 4 | 5 | trait ResponseMatchers extends ResponseStatusMatchers 6 | with ResponseCookiesMatchers 7 | with ResponseHeadersMatchers 8 | with ResponseBodyMatchers 9 | with ResponseSpecialHeadersMatchers 10 | with ResponseBodyAndStatusMatchers 11 | with ResponseStatusAndHeaderMatchers 12 | 13 | object ResponseMatchers extends ResponseMatchers 14 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/main/scala/com/wix/e2e/http/matchers/internal/HeaderMatching.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.{HttpHeader, HttpMessage} 4 | import org.specs2.matcher.{Expectable, MatchResult, Matcher, createExpectable} 5 | 6 | case class HeaderComparisonResult(identical: Seq[(String, String)], missing: Seq[(String, String)], extra: Seq[(String, String)]) 7 | 8 | object HeaderComparison { 9 | 10 | private def compareHeader(header1: (String, String), header2: (String, String)) = header1._1.toLowerCase == header2._1.toLowerCase && header1._2 == header2._2 11 | 12 | def compare(expectedHeaders: Seq[(String, String)], actualHeaders: Seq[(String, String)]): HeaderComparisonResult = { 13 | val identical = expectedHeaders.filter(h1 => actualHeaders.exists(h2 => compareHeader(h1, h2))) 14 | val missing = expectedHeaders.filter(h1 => !identical.exists(h2 => compareHeader(h1, h2))) 15 | val extra = actualHeaders.filter(h1 => !identical.exists(h2 => compareHeader(h1, h2))) 16 | 17 | HeaderComparisonResult(identical, missing, extra) 18 | } 19 | 20 | } 21 | 22 | abstract class HttpMessageType(val name: String) { 23 | def lowerCaseName: String = name.toLowerCase 24 | def isCookieHeader(header: HttpHeader): Boolean 25 | } 26 | 27 | 28 | trait HeaderMatching[T <: HttpMessage] { 29 | protected def httpMessageType: HttpMessageType 30 | 31 | protected def specialHeaders: Map[String, String] = Map.empty 32 | 33 | def haveAnyHeadersOf(headers: (String, String)*): Matcher[T] = 34 | haveHeaderInternal(headers, _.identical.nonEmpty, 35 | res => s"Could not find header [${res.missing.map(_._1).mkString(", ")}] but found those: [${res.extra.map(_._1).mkString(", ")}]") 36 | 37 | def haveAllHeadersOf(headers: (String, String)*): Matcher[T] = 38 | haveHeaderInternal(headers, _.missing.isEmpty, 39 | res => s"Could not find header [${res.missing.map(_._1).mkString(", ")}] but found those: [${res.identical.map(_._1).mkString(", ")}].") 40 | 41 | def haveTheSameHeadersAs(headers: (String, String)*): Matcher[T] = 42 | haveHeaderInternal(headers, r => r.extra.isEmpty && r.missing.isEmpty, 43 | res => s"${httpMessageType.name} header is not identical, missing headers from ${httpMessageType.lowerCaseName}: [${res.missing.map(_._1).mkString(", ")}], ${httpMessageType.lowerCaseName} contained extra headers: [${res.extra.map(_._1).mkString(", ")}].") 44 | 45 | 46 | def haveAnyHeaderThat(must: Matcher[String], withHeaderName: String): Matcher[T] = new Matcher[T] { 47 | def apply[S <: T](t: Expectable[S]): MatchResult[S] = { 48 | val actual = t.value 49 | val actualHeaders = actual.headers.filterNot(httpMessageType.isCookieHeader) 50 | val foundHeader = actualHeaders.find(_.name.equalsIgnoreCase(withHeaderName)).map(_.value) 51 | 52 | foundHeader match { 53 | case None if actualHeaders.isEmpty => failure(s"${httpMessageType.name} did not contain any headers.", t) 54 | case None => failure(s"${httpMessageType.name} contain header names: [${actualHeaders.map(_.name).mkString(", ")}] which did not contain: [$withHeaderName]", t) 55 | case Some(value) if must.apply(createExpectable(value)).isSuccess => success("ok", t) 56 | case Some(value) => failure(s"${httpMessageType.name} header [$withHeaderName], did not match { ${must.apply(createExpectable(value)).message} }", t) 57 | } 58 | } 59 | } 60 | 61 | private def haveHeaderInternal(expectedHeaders: Seq[(String, String)], 62 | comparator: HeaderComparisonResult => Boolean, 63 | errorMessage: HeaderComparisonResult => String): Matcher[T] = new Matcher[T] { 64 | 65 | def apply[S <: T](t: Expectable[S]): MatchResult[S] = 66 | checkSpecialHeaders(t).getOrElse(buildHeaderMatchResult(t)) 67 | 68 | private def checkSpecialHeaders[S <: T](t: Expectable[S]) = 69 | expectedHeaders.map(h => h._1.toLowerCase) 70 | .collectFirst { case name if specialHeaders.contains(name) => failure(specialHeaders(name), t) } 71 | 72 | private def buildHeaderMatchResult[S <: T](t: Expectable[S]) = { 73 | val actual = t.value 74 | val actualHeaders = actual.headers 75 | .filterNot(httpMessageType.isCookieHeader) 76 | .map(h => h.name -> h.value) 77 | 78 | val comparisonResult = HeaderComparison.compare(expectedHeaders, actualHeaders) 79 | 80 | if (comparator(comparisonResult)) success("ok", t) 81 | else if (actualHeaders.isEmpty) failure(s"${httpMessageType.name} did not contain any headers.", t) 82 | else failure(errorMessage(comparisonResult), t) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/main/scala/com/wix/e2e/http/matchers/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http 2 | 3 | import org.specs2.matcher.Matcher 4 | 5 | package object matchers { 6 | type ResponseMatcher = Matcher[HttpResponse] 7 | type RequestMatcher = Matcher[HttpRequest] 8 | } 9 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/drivers/MarshallerTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.drivers 2 | 3 | import com.wix.e2e.http.api.Marshaller 4 | import org.specs2.matcher.ThrownExpectations 5 | import org.specs2.mock.Mockito 6 | 7 | trait MarshallerTestSupport extends Mockito with ThrownExpectations { 8 | val marshaller: Marshaller = mock[Marshaller] 9 | 10 | def givenUnmarshallerWith[T : Manifest](someEntity: T, forContent: String): Unit = 11 | marshaller.unmarshall[T](forContent) returns someEntity 12 | 13 | def givenBadlyBehavingUnmarshallerFor[T : Manifest](withContent: String): Unit = 14 | marshaller.unmarshall[T](withContent) throws new RuntimeException 15 | } 16 | 17 | trait CustomMarshallerProvider { 18 | def marshaller: Marshaller 19 | implicit def customMarshaller: Marshaller = marshaller 20 | } 21 | 22 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/drivers/MatchersTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.drivers 2 | 3 | import akka.http.scaladsl.model.headers.HttpCookiePair 4 | import org.specs2.matcher.Matchers._ 5 | import org.specs2.matcher.{Matcher, MustThrownExpectationsCreation} 6 | 7 | trait MatchersTestSupport { self: MustThrownExpectationsCreation => 8 | def failureMessageFor[T](matcher: Matcher[T], matchedOn: T): String = 9 | matcher.apply( createMustExpectable(matchedOn) ).message 10 | } 11 | 12 | object CommonTestMatchers { 13 | def cookieWith(value: String): Matcher[HttpCookiePair] = be_===(value) ^^ { (_: HttpCookiePair).value aka "cookie value" } 14 | } 15 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/drivers/RequestRecorderTestSupport.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.drivers 2 | 3 | import com.wix.e2e.http.HttpRequest 4 | import com.wix.e2e.http.api.RequestRecordSupport 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory.aRandomRequest 6 | import com.wix.e2e.http.matchers.drivers.RequestRecorderFactory.aRequestRecorderWith 7 | 8 | trait RequestRecorderTestSupport { 9 | val request = aRandomRequest 10 | val anotherRequest = aRandomRequest 11 | val yetAnotherRequest = aRandomRequest 12 | val andAnotherRequest = aRandomRequest 13 | 14 | val anEmptyRequestRecorder = aRequestRecorderWith() 15 | 16 | } 17 | 18 | object RequestRecorderFactory { 19 | 20 | def aRequestRecorderWith(requests: HttpRequest*) = new RequestRecordSupport { 21 | val recordedRequests = requests 22 | def clearRecordedRequests() = {} 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestBodyMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 5 | import com.wix.e2e.http.matchers.drivers.MarshallingTestObjects.SomeCaseClass 6 | import com.wix.e2e.http.matchers.drivers.{CustomMarshallerProvider, HttpMessageTestSupport, MarshallerTestSupport, MatchersTestSupport} 7 | import org.specs2.matcher.CaseClassDiffs._ 8 | import org.specs2.matcher.ResultMatchers._ 9 | import org.specs2.mutable.Spec 10 | import org.specs2.specification.Scope 11 | 12 | class RequestBodyMatchersTest extends Spec with MatchersTestSupport { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport with MarshallerTestSupport with CustomMarshallerProvider 15 | 16 | "ResponseBodyMatchers" should { 17 | 18 | "exact match on response body" in new ctx { 19 | aRequestWith(content) must haveBodyWith(content) 20 | aRequestWith(content) must not( haveBodyWith(anotherContent) ) 21 | } 22 | 23 | "match underlying matcher with body content" in new ctx { 24 | aRequestWith(content) must haveBodyThat(must = be_===( content )) 25 | aRequestWith(content) must not( haveBodyThat(must = be_===( anotherContent )) ) 26 | } 27 | 28 | "exact match on response binary body" in new ctx { 29 | aRequestWith(binaryContent) must haveBodyWith(binaryContent) 30 | aRequestWith(binaryContent) must not( haveBodyWith(anotherBinaryContent) ) 31 | } 32 | 33 | "match underlying matcher with binary body content" in new ctx { 34 | aRequestWith(binaryContent) must haveBodyDataThat(must = be_===( binaryContent )) 35 | aRequestWith(binaryContent) must not( haveBodyDataThat(must = be_===( anotherBinaryContent )) ) 36 | } 37 | 38 | "handle empty body" in new ctx { 39 | aRequestWithoutBody must not( haveBodyWith(content)) 40 | } 41 | 42 | "support unmarshalling body content with user custom unmarshaller" in new ctx { 43 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 44 | 45 | aRequestWith(content) must haveBodyWith(entity = someObject) 46 | aRequestWith(content) must not( haveBodyWith(entity = anotherObject) ) 47 | } 48 | 49 | "provide a meaningful explanation why match failed" in new ctx { 50 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 51 | 52 | failureMessageFor(haveBodyEntityThat(must = be_===(anotherObject)), matchedOn = aRequestWith(content)) must_=== 53 | s"Failed to match: [SomeCaseClass(s: '${someObject.s}' != '${anotherObject.s}', i: ${someObject.i} != ${anotherObject.i})] with content: [$content]" 54 | } 55 | 56 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 57 | failureMessageFor(haveBodyWith(entity = be_===(someObject)), matchedOn = aRequestWith(content)) must_=== 58 | "Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyThat` instead." 59 | } 60 | 61 | "provide a proper message to user in case of a badly behaving marshaller" in new ctx { 62 | givenBadlyBehavingUnmarshallerFor[SomeCaseClass](withContent = content) 63 | 64 | haveBodyWith(entity = someObject).apply( aRequestWith(content) ) must beError(s"Failed to unmarshall: \\[$content\\]") 65 | } 66 | 67 | "support custom matcher for user object" in new ctx { 68 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 69 | 70 | aRequestWith(content) must haveBodyEntityThat(must = be_===(someObject)) 71 | aRequestWith(content) must not( haveBodyEntityThat(must = be_===(anotherObject)) ) 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestContentTypeMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.ContentTypes._ 4 | import com.wix.e2e.http.matchers.RequestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | class RequestContentTypeMatchersTest extends Spec with MatchersTestSupport { 11 | 12 | trait ctx extends Scope with HttpMessageTestSupport 13 | 14 | "RequestContentTypeMatchers" should { 15 | 16 | "exact match on request json content type" in new ctx { 17 | aRequestWith(`application/json`) must haveJsonBody 18 | aRequestWith(`text/csv(UTF-8)`) must not( haveJsonBody ) 19 | } 20 | 21 | "exact match on request text plain content type" in new ctx { 22 | aRequestWith(`text/plain(UTF-8)`) must haveTextPlainBody 23 | aRequestWith(`text/csv(UTF-8)`) must not( haveTextPlainBody ) 24 | } 25 | 26 | "exact match on request form url encoded content type" in new ctx { 27 | aRequestWith(`application/x-www-form-urlencoded`) must haveFormUrlEncodedBody 28 | aRequestWith(`text/csv(UTF-8)`) must not( haveFormUrlEncodedBody ) 29 | } 30 | 31 | "exact match on multipart request content type" in new ctx { 32 | aRequestWith(`multipart/form-data`) must haveMultipartFormBody 33 | aRequestWith(`text/csv(UTF-8)`) must not( haveMultipartFormBody ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestCookiesMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.CommonTestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.specs2.matcher.Matchers._ 8 | import org.specs2.mutable.Spec 9 | import org.specs2.specification.Scope 10 | 11 | 12 | class RequestCookiesMatchersTest extends Spec with MatchersTestSupport { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport 15 | 16 | "ResponseCookiesMatchers" should { 17 | 18 | "match if cookie with name is found" in new ctx { 19 | aRequestWithCookies(cookiePair) must receivedCookieWith(cookiePair._1) 20 | } 21 | 22 | "failure message should describe which cookies are present and which did not match" in new ctx { 23 | failureMessageFor(receivedCookieWith(cookiePair._1), matchedOn = aRequestWithCookies(anotherCookiePair, yetAnotherCookiePair)) must 24 | contain(cookiePair._1) and contain(anotherCookiePair._1) and contain(yetAnotherCookiePair._1) 25 | } 26 | 27 | "failure message for response withoout cookies will print that the response did not contain any cookies" in new ctx { 28 | receivedCookieWith(cookiePair._1).apply( aRequestWithNoCookies ).message must 29 | contain("Request did not contain any Cookie headers.") 30 | } 31 | 32 | "allow to compose matcher with custom cookie matcher" in new ctx { 33 | aRequestWithCookies(cookiePair) must receivedCookieThat(must = cookieWith(cookiePair._2)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestHeadersMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.specs2.matcher.AlwaysMatcher 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | 11 | class RequestHeadersMatchersTest extends Spec with MatchersTestSupport { 12 | 13 | trait ctx extends Scope with HttpMessageTestSupport 14 | 15 | "RequestHeadersMatchers" should { 16 | 17 | "contain header will check if any header is present" in new ctx { 18 | aRequestWithHeaders(header, anotherHeader) must haveAnyHeadersOf(header) 19 | } 20 | 21 | "return detailed message on hasAnyOf match failure" in new ctx { 22 | failureMessageFor(haveAnyHeadersOf(header, anotherHeader), matchedOn = aRequestWithHeaders(yetAnotherHeader, andAnotherHeader)) must_=== 23 | s"Could not find header [${header._1}, ${anotherHeader._1}] but found those: [${yetAnotherHeader._1}, ${andAnotherHeader._1}]" 24 | } 25 | 26 | "contain header will check if all headers are present" in new ctx { 27 | aRequestWithHeaders(header, anotherHeader, yetAnotherHeader) must haveAllHeadersOf(header, anotherHeader) 28 | } 29 | 30 | "allOf matcher will return a message stating what was found, and what is missing from header list" in new ctx { 31 | failureMessageFor(haveAllHeadersOf(header, anotherHeader), matchedOn = aRequestWithHeaders(yetAnotherHeader, header)) must_=== 32 | s"Could not find header [${anotherHeader._1}] but found those: [${header._1}]." 33 | } 34 | 35 | "same header as will check if the same headers is present" in new ctx { 36 | aRequestWithHeaders(header, anotherHeader) must haveTheSameHeadersAs(header, anotherHeader) 37 | aRequestWithHeaders(header, anotherHeader) must not( haveTheSameHeadersAs(header) ) 38 | aRequestWithHeaders(header) must not( haveTheSameHeadersAs(header, anotherHeader) ) 39 | } 40 | 41 | "haveTheSameHeadersAs matcher will return a message stating what was found, and what is missing from header list" in new ctx { 42 | failureMessageFor(haveTheSameHeadersAs(header, anotherHeader), matchedOn = aRequestWithHeaders(yetAnotherHeader, header)) must_=== 43 | s"Request header is not identical, missing headers from request: [${anotherHeader._1}], request contained extra headers: [${yetAnotherHeader._1}]." 44 | } 45 | 46 | "header name compare should be case insensitive" in new ctx { 47 | aRequestWithHeaders(header) must haveAnyHeadersOf(header.copy(_1 = header._1.toUpperCase)) 48 | aRequestWithHeaders(header) must not( haveAnyHeadersOf(header.copy(_2 = header._2.toUpperCase)) ) 49 | 50 | aRequestWithHeaders(header) must haveAllHeadersOf(header.copy(_1 = header._1.toUpperCase)) 51 | aRequestWithHeaders(header) must not( haveAllHeadersOf(header.copy(_2 = header._2.toUpperCase)) ) 52 | 53 | aRequestWithHeaders(header) must haveTheSameHeadersAs(header.copy(_1 = header._1.toUpperCase)) 54 | aRequestWithHeaders(header) must not( haveTheSameHeadersAs(header.copy(_2 = header._2.toUpperCase)) ) 55 | } 56 | 57 | "request with no headers will show a 'no headers' message" in new ctx { 58 | failureMessageFor(haveAnyHeadersOf(header), matchedOn = aRequestWithNoHeaders ) must_=== 59 | "Request did not contain any headers." 60 | 61 | failureMessageFor(haveAllHeadersOf(header), matchedOn = aRequestWithNoHeaders ) must_=== 62 | "Request did not contain any headers." 63 | 64 | failureMessageFor(haveTheSameHeadersAs(header), matchedOn = aRequestWithNoHeaders ) must_=== 65 | "Request did not contain any headers." 66 | } 67 | 68 | "ignore cookies and set cookies from headers comparison" in new ctx { 69 | aRequestWithCookies(cookiePair) must not( haveAnyHeadersOf("Cookie" -> s"${cookiePair._1}=${cookiePair._2}") ) 70 | aRequestWithCookies(cookiePair) must not( haveAllHeadersOf("Cookie" -> s"${cookiePair._1}=${cookiePair._2}") ) 71 | aRequestWithCookies(cookiePair) must not( haveTheSameHeadersAs("Cookie" -> s"${cookiePair._1}=${cookiePair._2}") ) 72 | aRequestWithCookies(cookiePair) must not( haveAnyHeaderThat(must = be_===(s"${cookiePair._1}=${cookiePair._2}"), withHeaderName = "Cookie") ) 73 | } 74 | 75 | "match if any header satisfy the composed matcher" in new ctx { 76 | aRequestWithHeaders(header) must haveAnyHeaderThat(must = be_===(header._2), withHeaderName = header._1) 77 | aRequestWithHeaders(header) must not( haveAnyHeaderThat(must = be_===(anotherHeader._2), withHeaderName = header._1) ) 78 | } 79 | 80 | "return informative error messages" in new ctx { 81 | failureMessageFor(haveAnyHeaderThat(must = AlwaysMatcher(), withHeaderName = nonExistingHeaderName), matchedOn = aRequestWithHeaders(header)) must_=== 82 | s"Request contain header names: [${header._1}] which did not contain: [$nonExistingHeaderName]" 83 | failureMessageFor(haveAnyHeaderThat(must = AlwaysMatcher(), withHeaderName = nonExistingHeaderName), matchedOn = aRequestWithNoHeaders) must_=== 84 | "Request did not contain any headers." 85 | failureMessageFor(haveAnyHeaderThat(must = be_===(anotherHeader._2), withHeaderName = header._1), matchedOn = aRequestWithHeaders(header)) must_=== 86 | s"Request header [${header._1}], did not match { ${be_===(anotherHeader._2).apply(header._2).message} }" 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestMethodMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.HttpMethods._ 4 | import com.wix.e2e.http.matchers.RequestMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 6 | import com.wix.e2e.http.matchers.drivers.HttpMessageTestSupport 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | 11 | class RequestMethodMatchersTest extends Spec { 12 | 13 | trait ctx extends Scope with HttpMessageTestSupport 14 | 15 | "RequestMethodMatchers" should { 16 | 17 | "match all request methods" in new ctx { 18 | Seq(POST -> bePost, GET -> beGet, PUT -> bePut, DELETE -> beDelete, 19 | HEAD -> beHead, OPTIONS -> beOptions, 20 | PATCH -> bePatch, TRACE -> beTrace, CONNECT -> beConnect) 21 | .foreach { case (method, matcherForMethod) => 22 | 23 | aRequestWith( method ) must matcherForMethod 24 | aRequestWith( randomMethodThatIsNot( method )) must not( matcherForMethod ) 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestRecorderMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.RequestRecorderFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{MatchersTestSupport, RequestRecorderTestSupport} 6 | import org.specs2.matcher.AlwaysMatcher 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | class RequestRecorderMatchersTest extends Spec with MatchersTestSupport { 11 | 12 | trait ctx extends Scope with RequestRecorderTestSupport 13 | 14 | "RequestRecorderMatchers" should { 15 | 16 | "check that request recorder has any of the given requests" in new ctx { 17 | aRequestRecorderWith(request, anotherRequest) must receivedAnyOf(request) 18 | aRequestRecorderWith(request) must not( receivedAnyOf(anotherRequest) ) 19 | } 20 | 21 | "return detailed message on hasAnyOf match failure" in new ctx { 22 | failureMessageFor(receivedAnyOf(request, anotherRequest), matchedOn = aRequestRecorderWith(yetAnotherRequest, yetAnotherRequest)) must_=== 23 | s"""Could not find requests: 24 | |1: $request, 25 | |2: $anotherRequest 26 | | 27 | |but found those: 28 | |1: $yetAnotherRequest, 29 | |2: $yetAnotherRequest""".stripMargin 30 | } 31 | 32 | "contain header will check if all requests are present" in new ctx { 33 | aRequestRecorderWith(request, anotherRequest, yetAnotherRequest) must receivedAllOf(request, anotherRequest) 34 | aRequestRecorderWith(request) must not( receivedAllOf(request, anotherRequest) ) 35 | } 36 | 37 | "allOf matcher will return a message stating what was found, and what is missing from recorded requests list" in new ctx { 38 | failureMessageFor(receivedAllOf(request, anotherRequest), matchedOn = aRequestRecorderWith(request, yetAnotherRequest)) must_=== 39 | s"""Could not find requests: 40 | |1: $anotherRequest 41 | | 42 | |but found those: 43 | |1: $request""".stripMargin 44 | } 45 | 46 | "same request as will check if the same requests is present" in new ctx { 47 | aRequestRecorderWith(request, anotherRequest) must receivedTheSameRequestsAs(request, anotherRequest) 48 | aRequestRecorderWith(request, anotherRequest) must not( receivedTheSameRequestsAs(request) ) 49 | aRequestRecorderWith(request) must not( receivedTheSameRequestsAs(request, anotherRequest) ) 50 | } 51 | 52 | "receivedTheSameRequestsAs matcher will return a message stating what was found, and what is missing from header list" in new ctx { 53 | failureMessageFor(receivedTheSameRequestsAs(request, anotherRequest), matchedOn = aRequestRecorderWith(request, yetAnotherRequest)) must_=== 54 | s"""Requests are not identical, missing requests are: 55 | |1: $anotherRequest 56 | | 57 | |added requests found: 58 | |1: $yetAnotherRequest""".stripMargin 59 | } 60 | 61 | "if no recorded requests were found, error message returned will be 'no requests' message" in new ctx { 62 | failureMessageFor(receivedAnyOf(request), matchedOn = anEmptyRequestRecorder ) must_=== 63 | "Server did not receive any requests." 64 | 65 | failureMessageFor(receivedAllOf(request), matchedOn = anEmptyRequestRecorder ) must_=== 66 | "Server did not receive any requests." 67 | 68 | failureMessageFor(receivedTheSameRequestsAs(request), matchedOn = anEmptyRequestRecorder ) must_=== 69 | "Server did not receive any requests." 70 | } 71 | 72 | "match if any request satisfy the composed matcher" in new ctx { 73 | aRequestRecorderWith(request) must receivedAnyRequestThat(must = be_===(request)) 74 | aRequestRecorderWith(request) must not( receivedAnyRequestThat(must = be_===(anotherRequest)) ) 75 | } 76 | 77 | "return informative error messages" in new ctx { 78 | failureMessageFor(receivedAnyRequestThat(must = be_===(anotherRequest)), matchedOn = aRequestRecorderWith(request)) must_=== 79 | s"""Could not find any request that matches: 80 | |1: ${ be_===(anotherRequest).apply(request).message.replaceAll("\n", "") }""".stripMargin 81 | failureMessageFor(receivedAnyRequestThat(must = AlwaysMatcher()), matchedOn = anEmptyRequestRecorder) must_=== 82 | "Server did not receive any requests." 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/RequestUrlMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.RequestMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpRequestFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.specs2.matcher.AlwaysMatcher 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | 11 | class RequestUrlMatchersTest extends Spec with MatchersTestSupport { 12 | 13 | trait ctx extends Scope with HttpMessageTestSupport 14 | 15 | "RequestUrlMatchers" should { 16 | 17 | "match exact path" in new ctx { 18 | aRequestWithPath(somePath) must havePath(somePath) 19 | aRequestWithPath(somePath) must not( havePath(anotherPath) ) 20 | } 21 | 22 | "match exact path matcher" in new ctx { 23 | aRequestWithPath(somePath) must havePathThat(must = be_===( somePath )) 24 | aRequestWithPath(somePath) must not( havePathThat(must = be_===( anotherPath )) ) 25 | } 26 | // if first ignore first slash ??? 27 | 28 | "contain parameter will check if any parameter is present" in new ctx { 29 | aRequestWithParameters(parameter, anotherParameter) must haveAnyParamOf(parameter) 30 | aRequestWithParameters(parameter) must not( haveAnyParamOf(anotherParameter) ) 31 | } 32 | 33 | "return detailed message on hasAnyOf match failure" in new ctx { 34 | failureMessageFor(haveAnyParamOf(parameter, anotherParameter), matchedOn = aRequestWithParameters(yetAnotherParameter, andAnotherParameter)) must_=== 35 | s"Could not find parameter [${parameter._1}, ${anotherParameter._1}] but found those: [${yetAnotherParameter._1}, ${andAnotherParameter._1}]" 36 | } 37 | 38 | "contain parameter will check if all parameters are present" in new ctx { 39 | aRequestWithParameters(parameter, anotherParameter, yetAnotherParameter) must haveAllParamFrom(parameter, anotherParameter) 40 | aRequestWithParameters(parameter, yetAnotherParameter) must not( haveAllParamFrom(parameter, anotherParameter) ) 41 | } 42 | 43 | "allOf matcher will return a message stating what was found, and what is missing from parameter list" in new ctx { 44 | failureMessageFor(haveAllParamFrom(parameter, anotherParameter), matchedOn = aRequestWithParameters(parameter, yetAnotherParameter)) must_=== 45 | s"Could not find parameter [${anotherParameter._1}] but found those: [${parameter._1}]." 46 | } 47 | 48 | "same parameter as will check if the same parameters is present" in new ctx { 49 | aRequestWithParameters(parameter, anotherParameter) must haveTheSameParamsAs(parameter, anotherParameter) 50 | aRequestWithParameters(parameter, anotherParameter) must not( haveTheSameParamsAs(parameter) ) 51 | aRequestWithParameters(parameter) must not( haveTheSameParamsAs(parameter, anotherParameter) ) 52 | } 53 | 54 | "haveTheSameParametersAs matcher will return a message stating what was found, and what is missing from parameter list" in new ctx { 55 | failureMessageFor(haveTheSameParamsAs(parameter, anotherParameter), matchedOn = aRequestWithParameters(parameter, yetAnotherParameter)) must_=== 56 | s"Request parameters are not identical, missing parameters from request: [${anotherParameter._1}], request contained extra parameters: [${yetAnotherParameter._1}]." 57 | } 58 | 59 | "request with no parameters will show a 'no parameters' message" in new ctx { 60 | failureMessageFor(haveAnyParamOf(parameter), matchedOn = aRequestWithNoParameters ) must_=== 61 | "Request did not contain any request parameters." 62 | 63 | failureMessageFor(haveAllParamFrom(parameter), matchedOn = aRequestWithNoParameters ) must_=== 64 | "Request did not contain any request parameters." 65 | 66 | failureMessageFor(haveTheSameParamsAs(parameter), matchedOn = aRequestWithNoParameters ) must_=== 67 | "Request did not contain any request parameters." 68 | } 69 | 70 | "match if any parameter satisfy the composed matcher" in new ctx { 71 | aRequestWithParameters(parameter) must haveAnyParamThat(must = be_===(parameter._2), withParamName = parameter._1) 72 | aRequestWithParameters(parameter) must not( haveAnyParamThat(must = be_===(anotherParameter._2), withParamName = anotherParameter._1) ) 73 | } 74 | 75 | "return informative error messages" in new ctx { 76 | failureMessageFor(haveAnyParamThat(must = AlwaysMatcher(), withParamName = nonExistingParamName), matchedOn = aRequestWithParameters(parameter)) must_=== 77 | s"Request contain parameter names: [${parameter._1}] which did not contain: [$nonExistingParamName]" 78 | failureMessageFor(haveAnyParamThat(must = AlwaysMatcher(), withParamName = nonExistingParamName), matchedOn = aRequestWithNoParameters) must_=== 79 | "Request did not contain any parameters." 80 | failureMessageFor(haveAnyParamThat(must = be_===(anotherParameter._2), withParamName = parameter._1), matchedOn = aRequestWithParameters(parameter)) must_=== 81 | s"Request parameter [${parameter._1}], did not match { ${be_===(anotherParameter._2).apply(parameter._2).message} }" 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseBodyAndStatusMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.api.Marshaller.Implicits.marshaller 4 | import com.wix.e2e.http.matchers.ResponseMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 6 | import com.wix.e2e.http.matchers.drivers.HttpResponseMatchers._ 7 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 8 | import org.specs2.mutable.Spec 9 | import org.specs2.specification.Scope 10 | 11 | 12 | class ResponseBodyAndStatusMatchersTest extends Spec with MatchersTestSupport { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport 15 | 16 | 17 | "ResponseBodyAndStatusMatchers" should { 18 | 19 | "match successful response with body content" in new ctx { 20 | aSuccessfulResponseWith(content) must beSuccessfulWith(content) 21 | aSuccessfulResponseWith(content) must not( beSuccessfulWith(anotherContent) ) 22 | } 23 | 24 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 25 | failureMessageFor(beSuccessfulWith(entity = be_===(content)), matchedOn = aResponseWith(content)) must_=== 26 | s"Matcher misuse: `beSuccessfulWith` received a matcher to match against, please use `beSuccessfulWithEntityThat` instead." 27 | } 28 | 29 | "match successful response with body content matcher" in new ctx { 30 | aSuccessfulResponseWith(content) must beSuccessfulWithBodyThat(must = be_===( content )) 31 | aSuccessfulResponseWith(content) must not( beSuccessfulWithBodyThat(must = be_===( anotherContent )) ) 32 | } 33 | 34 | "match invalid response with body content" in new ctx { 35 | anInvalidResponseWith(content) must beInvalidWith(content) 36 | anInvalidResponseWith(content) must not( beInvalidWith(anotherContent) ) 37 | } 38 | 39 | "match invalid response with body content matcher" in new ctx { 40 | anInvalidResponseWith(content) must beInvalidWithBodyThat(must = be_===( content )) 41 | anInvalidResponseWith(content) must not( beInvalidWithBodyThat(must = be_===( anotherContent )) ) 42 | } 43 | 44 | "match successful response with binary body content" in new ctx { 45 | aSuccessfulResponseWith(binaryContent) must beSuccessfulWith(binaryContent) 46 | aSuccessfulResponseWith(binaryContent) must not( beSuccessfulWith(anotherBinaryContent) ) 47 | } 48 | 49 | "match successful response with binary body content matcher" in new ctx { 50 | aSuccessfulResponseWith(binaryContent) must beSuccessfulWithBodyDataThat(must = be_===( binaryContent )) 51 | aSuccessfulResponseWith(binaryContent) must not( beSuccessfulWithBodyDataThat(must = be_===( anotherBinaryContent )) ) 52 | } 53 | 54 | "match successful response with entity" in new ctx { 55 | aSuccessfulResponseWith(marshaller.marshall(someObject)) must beSuccessfulWith( someObject ) 56 | aSuccessfulResponseWith(marshaller.marshall(someObject)) must not( beSuccessfulWith( anotherObject ) ) 57 | } 58 | 59 | "match successful response with entity with custom marshaller" in new ctx { 60 | aSuccessfulResponseWith(marshaller.marshall(someObject)) must beSuccessfulWith( someObject ) 61 | aSuccessfulResponseWith(marshaller.marshall(someObject)) must not( beSuccessfulWith( anotherObject ) ) 62 | } 63 | 64 | "match successful response with entity matcher" in new ctx { 65 | aSuccessfulResponseWith(marshaller.marshall(someObject)) must beSuccessfulWithEntityThat( must = be_===( someObject ) ) 66 | aSuccessfulResponseWith(marshaller.marshall(someObject)) must not( beSuccessfulWithEntityThat( must = be_===( anotherObject ) ) ) 67 | } 68 | 69 | "match successful response with headers" in new ctx { 70 | aSuccessfulResponseWith(header, anotherHeader) must beSuccessfulWithHeaders(header, anotherHeader) 71 | aSuccessfulResponseWith(header) must not( beSuccessfulWithHeaders(anotherHeader) ) 72 | } 73 | 74 | "match successful response with header matcher" in new ctx { 75 | aSuccessfulResponseWith(header) must beSuccessfulWithHeaderThat(must = be_===(header._2), withHeaderName = header._1) 76 | aSuccessfulResponseWith(header) must not( beSuccessfulWithHeaderThat(must = be_===(anotherHeader._2), withHeaderName = header._1) ) 77 | } 78 | 79 | "match successful response with cookies" in new ctx { 80 | aSuccessfulResponseWithCookies(cookie, anotherCookie) must beSuccessfulWithCookie(cookie.name) 81 | aSuccessfulResponseWithCookies(cookie) must not( beSuccessfulWithCookie(anotherCookie.name) ) 82 | } 83 | 84 | "match successful response with cookie matcher" in new ctx { 85 | aSuccessfulResponseWithCookies(cookie) must beSuccessfulWithCookieThat(must = cookieWith(cookie.value)) 86 | aSuccessfulResponseWithCookies(cookie) must not( beSuccessfulWithCookieThat(must = cookieWith(anotherCookie.value)) ) 87 | } 88 | 89 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 90 | failureMessageFor(haveBodyWith(entity = be_===(someObject)), matchedOn = aResponseWith(content)) must_=== 91 | s"Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyWithEntityThat` instead." 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseBodyMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.MarshallingTestObjects.SomeCaseClass 6 | import com.wix.e2e.http.matchers.drivers.{CustomMarshallerProvider, HttpMessageTestSupport, MarshallerTestSupport, MatchersTestSupport} 7 | import org.specs2.matcher.CaseClassDiffs._ 8 | import org.specs2.matcher.ResultMatchers._ 9 | import org.specs2.mutable.Spec 10 | import org.specs2.specification.Scope 11 | 12 | class ResponseBodyMatchersTest extends Spec with MatchersTestSupport { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport with MarshallerTestSupport with CustomMarshallerProvider 15 | 16 | "ResponseBodyMatchers" should { 17 | 18 | "exact match on response body" in new ctx { 19 | aResponseWith(content) must haveBodyWith(content) 20 | aResponseWith(content) must not( haveBodyWith(anotherContent) ) 21 | } 22 | 23 | "match underlying matcher with body content" in new ctx { 24 | aResponseWith(content) must haveBodyThat(must = be_===( content )) 25 | aResponseWith(content) must not( haveBodyThat(must = be_===( anotherContent )) ) 26 | } 27 | 28 | "exact match on response binary body" in new ctx { 29 | aResponseWith(binaryContent) must haveBodyWith(binaryContent) 30 | aResponseWith(binaryContent) must not( haveBodyWith(anotherBinaryContent) ) 31 | } 32 | 33 | "match underlying matcher with binary body content" in new ctx { 34 | aResponseWith(binaryContent) must haveBodyDataThat(must = be_===( binaryContent )) 35 | aResponseWith(binaryContent) must not( haveBodyDataThat(must = be_===( anotherBinaryContent )) ) 36 | } 37 | 38 | "handle empty body" in new ctx { 39 | aResponseWithoutBody must not( haveBodyWith(content)) 40 | } 41 | 42 | "support unmarshalling body content with user custom unmarshaller" in new ctx { 43 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 44 | 45 | aResponseWith(content) must haveBodyWith(entity = someObject) 46 | aResponseWith(content) must not( haveBodyWith(entity = anotherObject) ) 47 | } 48 | 49 | "provide a meaningful explanation why match failed" in new ctx { 50 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 51 | 52 | failureMessageFor(haveBodyWithEntityThat(must = be_===(anotherObject)), matchedOn = aResponseWith(content)) must_=== 53 | s"Failed to match: [SomeCaseClass(s: '${someObject.s}' != '${anotherObject.s}', i: ${someObject.i} != ${anotherObject.i})] with content: [$content]" 54 | } 55 | 56 | "provide a proper message to user in case of a badly behaving marshaller" in new ctx { 57 | givenBadlyBehavingUnmarshallerFor[SomeCaseClass](withContent = content) 58 | 59 | haveBodyWith(entity = someObject).apply( aResponseWith(content) ) must beError(s"Failed to unmarshall: \\[$content\\]") 60 | } 61 | 62 | "provide a proper message to user sent a matcher to an entity matcher" in new ctx { 63 | failureMessageFor(haveBodyWith(entity = be_===(someObject)), matchedOn = aResponseWith(content)) must_=== 64 | s"Matcher misuse: `haveBodyWith` received a matcher to match against, please use `haveBodyWithEntityThat` instead." 65 | } 66 | 67 | "support custom matcher for user object" in new ctx { 68 | givenUnmarshallerWith[SomeCaseClass](someObject, forContent = content) 69 | 70 | aResponseWith(content) must haveBodyWithEntityThat(must = be_===(someObject)) 71 | aResponseWith(content) must not( haveBodyWithEntityThat(must = be_===(anotherObject)) ) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseContentLengthMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.specs2.mutable.Spec 7 | import org.specs2.specification.Scope 8 | 9 | 10 | class ResponseContentLengthMatchersTest extends Spec with MatchersTestSupport { 11 | 12 | trait ctx extends Scope with HttpMessageTestSupport 13 | 14 | "ResponseContentLengthMatchers" should { 15 | 16 | "support matching against specific content length" in new ctx { 17 | aResponseWith(contentWith(length = length)) must haveContentLength(length = length) 18 | aResponseWith(contentWith(length = anotherLength)) must not( haveContentLength(length = length) ) 19 | } 20 | 21 | "support matching content length against response without content length" in new ctx { 22 | aResponseWithoutContentLength must not( haveContentLength(length = length) ) 23 | } 24 | 25 | "support matching against response without content length" in new ctx { 26 | aResponseWithoutContentLength must haveNoContentLength 27 | aResponseWith(contentWith(length = length)) must not( haveNoContentLength ) 28 | } 29 | 30 | "failure message should describe what was the expected content length and what was found" in new ctx { 31 | failureMessageFor(haveContentLength(length = length), matchedOn = aResponseWith(contentWith(length = anotherLength))) must_=== 32 | s"Expected content length [$length] does not match actual content length [$anotherLength]" 33 | } 34 | 35 | "failure message should reflect that content length header was not found" in new ctx { 36 | failureMessageFor(haveContentLength(length = length), matchedOn = aResponseWithoutContentLength) must_=== 37 | s"Expected content length [$length] but response did not contain `content-length` header." 38 | } 39 | 40 | "failure message should reflect that content length header exists while trying to match against a content length that doesn't exists" in new ctx { 41 | failureMessageFor(haveNoContentLength, matchedOn = aResponseWith(contentWith(length = length))) must_=== 42 | s"Expected no `content-length` header but response did contain `content-length` header with size [$length]." 43 | } 44 | 45 | "failure message if someone tries to match content-length in headers matchers" in new ctx { 46 | failureMessageFor(haveAllHeadersOf(contentLengthHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 47 | """`Content-Length` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 48 | |Use `haveContentLength` matcher instead.""".stripMargin 49 | failureMessageFor(haveAnyHeadersOf(contentLengthHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 50 | """`Content-Length` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 51 | |Use `haveContentLength` matcher instead.""".stripMargin 52 | failureMessageFor(haveTheSameHeadersAs(contentLengthHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 53 | """`Content-Length` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 54 | |Use `haveContentLength` matcher instead.""".stripMargin 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseContentTypeMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.specs2.mutable.Spec 7 | import org.specs2.specification.Scope 8 | 9 | 10 | class ResponseContentTypeMatchersTest extends Spec with MatchersTestSupport { 11 | 12 | trait ctx extends Scope with HttpMessageTestSupport 13 | 14 | 15 | "ResponseContentTypeMatchers" should { 16 | 17 | "support matching against json content type" in new ctx { 18 | aResponseWithContentType("application/json") must beJsonResponse 19 | aResponseWithContentType("text/plain") must not( beJsonResponse ) 20 | } 21 | 22 | "support matching against text plain content type" in new ctx { 23 | aResponseWithContentType("text/plain") must beTextPlainResponse 24 | aResponseWithContentType("application/json") must not( beTextPlainResponse ) 25 | } 26 | 27 | "support matching against form url encoded content type" in new ctx { 28 | aResponseWithContentType("application/x-www-form-urlencoded") must beFormUrlEncodedResponse 29 | aResponseWithContentType("application/json") must not( beFormUrlEncodedResponse ) 30 | } 31 | 32 | "show proper error in case matching against a malformed content type" in new ctx { 33 | failureMessageFor(haveContentType(malformedContentType), matchedOn = aResponseWithContentType(anotherContentType)) must 34 | contain(s"Cannot match against a malformed content type: $malformedContentType") 35 | } 36 | 37 | "support matching against content type" in new ctx { 38 | aResponseWithContentType(contentType) must haveContentType(contentType) 39 | } 40 | 41 | "failure message should describe what was the expected content type and what was found" in new ctx { 42 | failureMessageFor(haveContentType(contentType), matchedOn = aResponseWithContentType(anotherContentType)) must_=== 43 | s"Expected content type [$contentType] does not match actual content type [$anotherContentType]" 44 | } 45 | 46 | "failure message in case no content type for body should be handled" in new ctx { 47 | failureMessageFor(haveContentType(contentType), matchedOn = aResponseWithoutBody) must_=== 48 | "Response body does not have a set content type" 49 | } 50 | 51 | "failure message if someone tries to match content-type in headers matchers" in new ctx { 52 | failureMessageFor(haveAllHeadersOf(contentTypeHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 53 | """`Content-Type` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 54 | |Use `haveContentType` matcher instead (or `beJsonResponse`, `beTextPlainResponse`, `beFormUrlEncodedResponse`).""".stripMargin 55 | failureMessageFor(haveAnyHeadersOf(contentTypeHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 56 | """`Content-Type` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 57 | |Use `haveContentType` matcher instead (or `beJsonResponse`, `beTextPlainResponse`, `beFormUrlEncodedResponse`).""".stripMargin 58 | failureMessageFor(haveTheSameHeadersAs(contentTypeHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 59 | """`Content-Type` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 60 | |Use `haveContentType` matcher instead (or `beJsonResponse`, `beTextPlainResponse`, `beFormUrlEncodedResponse`).""".stripMargin 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseCookiesMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseMatchers._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.specs2.matcher.Matchers._ 8 | import org.specs2.mutable.Spec 9 | import org.specs2.specification.Scope 10 | 11 | 12 | class ResponseCookiesMatchersTest extends Spec with MatchersTestSupport { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport 15 | 16 | "ResponseCookiesMatchers" should { 17 | 18 | "match if cookie with name is found" in new ctx { 19 | aResponseWithCookies(cookie) must receivedCookieWith(cookie.name) 20 | } 21 | 22 | "failure message should describe which cookies are present and which did not match" in new ctx { 23 | failureMessageFor(receivedCookieWith(cookie.name), matchedOn = aResponseWithCookies(anotherCookie, yetAnotherCookie)) must 24 | contain(cookie.name) and contain(anotherCookie.name) and contain(yetAnotherCookie.name) 25 | } 26 | 27 | "failure message for response withoout cookies will print that the response did not contain any cookies" in new ctx { 28 | receivedCookieWith(cookie.name).apply( aResponseWithNoCookies ).message must 29 | contain("Response did not contain any `Set-Cookie` headers.") 30 | } 31 | 32 | "allow to compose matcher with custom cookie matcher" in new ctx { 33 | aResponseWithCookies(cookie) must receivedCookieThat(must = cookieWith(cookie.value)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseHeadersMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import com.wix.e2e.http.matchers.ResponseMatchers._ 4 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 5 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 6 | import org.specs2.matcher.AlwaysMatcher 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | 11 | class ResponseHeadersMatchersTest extends Spec with MatchersTestSupport { 12 | 13 | trait ctx extends Scope with HttpMessageTestSupport 14 | 15 | "ResponseHeadersMatchers" should { 16 | 17 | "contain header will check if any header is present" in new ctx { 18 | aResponseWithHeaders(header, anotherHeader) must haveAnyHeadersOf(header) 19 | } 20 | 21 | "return detailed message on hasAnyOf match failure" in new ctx { 22 | failureMessageFor(haveAnyHeadersOf(header, anotherHeader), matchedOn = aResponseWithHeaders(yetAnotherHeader, andAnotherHeader)) must_=== 23 | s"Could not find header [${header._1}, ${anotherHeader._1}] but found those: [${yetAnotherHeader._1}, ${andAnotherHeader._1}]" 24 | } 25 | 26 | "contain header will check if all headers are present" in new ctx { 27 | aResponseWithHeaders(header, anotherHeader, yetAnotherHeader) must haveAllHeadersOf(header, anotherHeader) 28 | } 29 | 30 | "allOf matcher will return a message stating what was found, and what is missing from header list" in new ctx { 31 | failureMessageFor(haveAllHeadersOf(header, anotherHeader), matchedOn = aResponseWithHeaders(yetAnotherHeader, header)) must_=== 32 | s"Could not find header [${anotherHeader._1}] but found those: [${header._1}]." 33 | } 34 | 35 | "same header as will check if the same headers is present" in new ctx { 36 | aResponseWithHeaders(header, anotherHeader) must haveTheSameHeadersAs(header, anotherHeader) 37 | aResponseWithHeaders(header, anotherHeader) must not( haveTheSameHeadersAs(header) ) 38 | aResponseWithHeaders(header) must not( haveTheSameHeadersAs(header, anotherHeader) ) 39 | } 40 | 41 | "haveTheSameHeadersAs matcher will return a message stating what was found, and what is missing from header list" in new ctx { 42 | failureMessageFor(haveTheSameHeadersAs(header, anotherHeader), matchedOn = aResponseWithHeaders(yetAnotherHeader, header)) must_=== 43 | s"Response header is not identical, missing headers from response: [${anotherHeader._1}], response contained extra headers: [${yetAnotherHeader._1}]." 44 | } 45 | 46 | "header name compare should be case insensitive" in new ctx { 47 | aResponseWithHeaders(header) must haveAnyHeadersOf(header.copy(_1 = header._1.toUpperCase)) 48 | aResponseWithHeaders(header) must not( haveAnyHeadersOf(header.copy(_2 = header._2.toUpperCase)) ) 49 | 50 | aResponseWithHeaders(header) must haveAllHeadersOf(header.copy(_1 = header._1.toUpperCase)) 51 | aResponseWithHeaders(header) must not( haveAllHeadersOf(header.copy(_2 = header._2.toUpperCase)) ) 52 | 53 | aResponseWithHeaders(header) must haveTheSameHeadersAs(header.copy(_1 = header._1.toUpperCase)) 54 | aResponseWithHeaders(header) must not( haveTheSameHeadersAs(header.copy(_2 = header._2.toUpperCase)) ) 55 | } 56 | 57 | "response with no headers will show a 'no headers' message" in new ctx { 58 | failureMessageFor(haveAnyHeadersOf(header), matchedOn = aResponseWithNoHeaders ) must_=== 59 | "Response did not contain any headers." 60 | 61 | failureMessageFor(haveAllHeadersOf(header), matchedOn = aResponseWithNoHeaders ) must_=== 62 | "Response did not contain any headers." 63 | 64 | failureMessageFor(haveTheSameHeadersAs(header), matchedOn = aResponseWithNoHeaders ) must_=== 65 | "Response did not contain any headers." 66 | } 67 | 68 | "ignore cookies and set cookies from headers comparison" in new ctx { 69 | aResponseWithCookies(cookie) must not( haveAnyHeadersOf("Set-Cookie" -> s"${cookie.name}=${cookie.value}") ) 70 | aResponseWithCookies(cookie) must not( haveAllHeadersOf("Set-Cookie" -> s"${cookie.name}=${cookie.value}") ) 71 | aResponseWithCookies(cookie) must not( haveTheSameHeadersAs("Set-Cookie" -> s"${cookie.name}=${cookie.value}") ) 72 | } 73 | 74 | "match if any header satisfy the composed matcher" in new ctx { 75 | aResponseWithHeaders(header) must haveAnyHeaderThat(must = be_===(header._2), withHeaderName = header._1) 76 | aResponseWithHeaders(header) must not( haveAnyHeaderThat(must = be_===(anotherHeader._2), withHeaderName = header._1) ) 77 | } 78 | 79 | "return informative error messages" in new ctx { 80 | failureMessageFor(haveAnyHeaderThat(must = AlwaysMatcher(), withHeaderName = nonExistingHeaderName), matchedOn = aResponseWithHeaders(header)) must_=== 81 | s"Response contain header names: [${header._1}] which did not contain: [$nonExistingHeaderName]" 82 | failureMessageFor(haveAnyHeaderThat(must = AlwaysMatcher(), withHeaderName = nonExistingHeaderName), matchedOn = aResponseWithNoHeaders) must_=== 83 | "Response did not contain any headers." 84 | failureMessageFor(haveAnyHeaderThat(must = be_===(anotherHeader._2), withHeaderName = header._1), matchedOn = aResponseWithHeaders(header)) must_=== 85 | s"Response header [${header._1}], did not match { ${be_===(anotherHeader._2).apply(header._2).message} }" 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseStatusAndHeaderMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.StatusCodes.{Found, MovedPermanently} 4 | import com.wix.e2e.http.matchers.ResponseMatchers._ 5 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 6 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 7 | import org.specs2.mutable.Spec 8 | import org.specs2.specification.Scope 9 | 10 | class ResponseStatusAndHeaderMatchersTest extends Spec with MatchersTestSupport { 11 | 12 | trait ctx extends Scope with HttpMessageTestSupport 13 | 14 | "ResponseStatusAndHeaderMatchers" should { 15 | 16 | "match against a response that is temporarily redirected to url" in new ctx { 17 | aRedirectResponseTo(url) must beRedirectedTo(url) 18 | aRedirectResponseTo(url) must not( beRedirectedTo(anotherUrl) ) 19 | aRedirectResponseTo(url).withStatus(randomStatusThatIsNot(Found)) must not( beRedirectedTo(url) ) 20 | } 21 | 22 | "match against a response that is permanently redirected to url" in new ctx { 23 | aPermanentlyRedirectResponseTo(url) must bePermanentlyRedirectedTo(url) 24 | aPermanentlyRedirectResponseTo(url) must not( bePermanentlyRedirectedTo(anotherUrl) ) 25 | aPermanentlyRedirectResponseTo(url).withStatus(randomStatusThatIsNot(MovedPermanently)) must not( bePermanentlyRedirectedTo(url) ) 26 | } 27 | 28 | "match against url params even if params has a different order" in new ctx { 29 | aRedirectResponseTo(s"$url?param1=val1¶m2=val2") must beRedirectedTo(s"$url?param2=val2¶m1=val1") 30 | aPermanentlyRedirectResponseTo(s"$url?param1=val1¶m2=val2") must bePermanentlyRedirectedTo(s"$url?param2=val2¶m1=val1") 31 | } 32 | 33 | "match will fail for different protocol" in new ctx { 34 | aRedirectResponseTo(s"http://example.com") must not( beRedirectedTo(s"https://example.com") ) 35 | aPermanentlyRedirectResponseTo(s"http://example.com") must not( bePermanentlyRedirectedTo(s"https://example.com") ) 36 | } 37 | 38 | "match will fail for different host and port" in new ctx { 39 | aRedirectResponseTo(s"http://example.com") must not( beRedirectedTo(s"http://example.org") ) 40 | aRedirectResponseTo(s"http://example.com:99") must not( beRedirectedTo(s"http://example.com:81") ) 41 | aPermanentlyRedirectResponseTo(s"http://example.com") must not( bePermanentlyRedirectedTo(s"http://example.org") ) 42 | aPermanentlyRedirectResponseTo(s"http://example.com:99") must not( bePermanentlyRedirectedTo(s"http://example.com:81") ) 43 | } 44 | 45 | "port 80 is removed by akka http" in new ctx { 46 | aRedirectResponseTo(s"http://example.com:80") must beRedirectedTo(s"http://example.com") 47 | aPermanentlyRedirectResponseTo(s"http://example.com:80") must bePermanentlyRedirectedTo(s"http://example.com") 48 | } 49 | 50 | "match will fail for different path" in new ctx { 51 | aRedirectResponseTo(s"http://example.com/path1") must not( beRedirectedTo(s"http://example.com/path2") ) 52 | aPermanentlyRedirectResponseTo(s"http://example.com/path1") must not( bePermanentlyRedirectedTo(s"http://example.org/path2") ) 53 | } 54 | 55 | "match will fail for different hash fragment" in new ctx { 56 | aRedirectResponseTo(s"http://example.com/path#fragment") must not( beRedirectedTo(s"http://example.com/path#anotherFxragment") ) 57 | aPermanentlyRedirectResponseTo(s"http://example.com/path#fragment") must not( bePermanentlyRedirectedTo(s"http://example.com/path#anotherFxragment") ) 58 | } 59 | 60 | "failure message in case response does not have location header" in new ctx { 61 | failureMessageFor(beRedirectedTo(url), matchedOn = aRedirectResponseWithoutLocationHeader) must_=== 62 | "Response does not contain Location header." 63 | failureMessageFor(bePermanentlyRedirectedTo(url), matchedOn = aPermanentlyRedirectResponseWithoutLocationHeader) must_=== 64 | "Response does not contain Location header." 65 | } 66 | 67 | "failure message in case trying to match against a malformed url" in new ctx { 68 | failureMessageFor(beRedirectedTo(malformedUrl), matchedOn = aRedirectResponseTo(url)) must_=== 69 | s"Matching against a malformed url: [$malformedUrl]." 70 | failureMessageFor(bePermanentlyRedirectedTo(malformedUrl), matchedOn = aPermanentlyRedirectResponseTo(url)) must_=== 71 | s"Matching against a malformed url: [$malformedUrl]." 72 | } 73 | 74 | "failure message in case response have different urls should show the actual url and the expected url" in new ctx { 75 | failureMessageFor(beRedirectedTo(url), matchedOn = aRedirectResponseTo(s"$url?param1=val1")) must_=== 76 | s"""Response is redirected to a different url: 77 | |actual: $url?param1=val1 78 | |expected: $url 79 | |""".stripMargin 80 | failureMessageFor(bePermanentlyRedirectedTo(url), matchedOn = aPermanentlyRedirectResponseTo(s"$url?param1=val1")) must_=== 81 | s"""Response is redirected to a different url: 82 | |actual: $url?param1=val1 83 | |expected: $url 84 | |""".stripMargin 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseStatusMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | 4 | import akka.http.scaladsl.model.StatusCodes._ 5 | import com.wix.e2e.http.matchers.ResponseMatchers._ 6 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 7 | import com.wix.e2e.http.matchers.drivers.HttpMessageTestSupport 8 | import org.specs2.mutable.Spec 9 | import org.specs2.specification.Scope 10 | 11 | 12 | class ResponseStatusMatchersTest extends Spec { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport 15 | 16 | 17 | "ResponseStatusMatchers" should { 18 | Seq(OK -> beSuccessful, NoContent -> beNoContent, Created -> beSuccessfullyCreated, Accepted -> beAccepted, // 2xx 19 | 20 | Found -> beRedirect, MovedPermanently -> bePermanentlyRedirect, //3xx 21 | 22 | // 4xx 23 | Forbidden -> beRejected, NotFound -> beNotFound, BadRequest -> beInvalid, PayloadTooLarge -> beRejectedTooLarge, 24 | Unauthorized -> beUnauthorized, MethodNotAllowed -> beNotSupported, Conflict -> beConflict, PreconditionFailed -> bePreconditionFailed, 25 | UnprocessableEntity -> beUnprocessableEntity, PreconditionRequired -> bePreconditionRequired, TooManyRequests -> beTooManyRequests, 26 | RequestHeaderFieldsTooLarge -> beRejectedRequestTooLarge, 27 | 28 | ServiceUnavailable -> beUnavailable, InternalServerError -> beInternalServerError, NotImplemented -> beNotImplemented // 5xx 29 | ).foreach { case (status, matcherForStatus) => 30 | 31 | s"match against status ${status.value}" in new ctx { 32 | aResponseWith( status ) must matcherForStatus 33 | aResponseWith( randomStatusThatIsNot(status) ) must not( matcherForStatus ) 34 | } 35 | } 36 | 37 | "allow matching against status code" in new ctx { 38 | val status = randomStatus 39 | aResponseWith( status ) must haveStatus(code = status.intValue ) 40 | aResponseWith( status ) must not( haveStatus(code = randomStatusThatIsNot(status).intValue ) ) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /http-testkit-specs2/src/test/scala/com/wix/e2e/http/matchers/internal/ResponseTransferEncodingMatchersTest.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.matchers.internal 2 | 3 | import akka.http.scaladsl.model.TransferEncodings 4 | import akka.http.scaladsl.model.TransferEncodings._ 5 | import com.wix.e2e.http.matchers.ResponseMatchers._ 6 | import com.wix.e2e.http.matchers.drivers.HttpResponseFactory._ 7 | import com.wix.e2e.http.matchers.drivers.{HttpMessageTestSupport, MatchersTestSupport} 8 | import org.specs2.mutable.Spec 9 | import org.specs2.specification.Scope 10 | 11 | 12 | class ResponseTransferEncodingMatchersTest extends Spec with MatchersTestSupport { 13 | 14 | trait ctx extends Scope with HttpMessageTestSupport 15 | 16 | 17 | "ResponseTransferEncodingMatchersTest" should { 18 | 19 | "support matching against chunked transfer encoding" in new ctx { 20 | aChunkedResponse must beChunkedResponse 21 | aResponseWithoutTransferEncoding must not( beChunkedResponse ) 22 | aResponseWithTransferEncodings(compress) must not( beChunkedResponse ) 23 | aResponseWithTransferEncodings(chunked) must beChunkedResponse 24 | } 25 | 26 | "failure message in case no transfer encoding header should state that response did not have the proper header" in new ctx { 27 | failureMessageFor(beChunkedResponse, matchedOn = aResponseWithoutTransferEncoding) must_=== 28 | "Expected Chunked response while response did not contain `Transfer-Encoding` header" 29 | } 30 | 31 | "failure message in case transfer encoding header exists should state that transfer encoding has a different value" in new ctx { 32 | failureMessageFor(beChunkedResponse, matchedOn = aResponseWithTransferEncodings(compress, TransferEncodings.deflate)) must_=== 33 | "Expected Chunked response while response has `Transfer-Encoding` header with values ['compress', 'deflate']" 34 | } 35 | 36 | "support matching against transfer encoding header values" in new ctx { 37 | aResponseWithTransferEncodings(compress) must haveTransferEncodings("compress") 38 | aResponseWithTransferEncodings(compress) must not( haveTransferEncodings("deflate") ) 39 | } 40 | 41 | "support matching against transfer encoding header with multiple values, matcher will validate that response has all of the expected values" in new ctx { 42 | aResponseWithTransferEncodings(compress, deflate) must haveTransferEncodings("deflate", "compress") 43 | aResponseWithTransferEncodings(compress, deflate) must haveTransferEncodings("compress") 44 | } 45 | 46 | "properly match chunked encoding" in new ctx { 47 | aChunkedResponse must haveTransferEncodings("chunked") 48 | aChunkedResponseWith(compress) must haveTransferEncodings("compress", "chunked") 49 | aChunkedResponseWith(compress) must haveTransferEncodings("chunked") 50 | } 51 | 52 | "failure message should describe what was the expected transfer encodings and what was found" in new ctx { 53 | failureMessageFor(haveTransferEncodings("deflate", "compress"), matchedOn = aChunkedResponseWith(gzip)) must_=== 54 | s"Expected transfer encodings ['deflate', 'compress'] does not match actual transfer encoding ['chunked', 'gzip']" 55 | } 56 | 57 | "failure message in case no Transfer-Encoding for response should be handled" in new ctx { 58 | failureMessageFor(haveTransferEncodings("chunked"), matchedOn = aResponseWithoutTransferEncoding) must_=== 59 | "Response did not contain `Transfer-Encoding` header." 60 | } 61 | 62 | "failure message if someone tries to match content-type in headers matchers" in new ctx { 63 | failureMessageFor(haveAllHeadersOf(transferEncodingHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 64 | """`Transfer-Encoding` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 65 | |Use `beChunkedResponse` or `haveTransferEncodings` matcher instead.""".stripMargin 66 | failureMessageFor(haveAnyHeadersOf(transferEncodingHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 67 | """`Transfer-Encoding` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 68 | |Use `beChunkedResponse` or `haveTransferEncodings` matcher instead.""".stripMargin 69 | failureMessageFor(haveTheSameHeadersAs(transferEncodingHeader), matchedOn = aResponseWithContentType(contentType)) must_=== 70 | """`Transfer-Encoding` is a special header and cannot be used in `haveAnyHeadersOf`, `haveAllHeadersOf`, `haveTheSameHeadersAs` matchers. 71 | |Use `beChunkedResponse` or `haveTransferEncodings` matcher instead.""".stripMargin 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /http-testkit-test-commons/src/main/scala/com/wix/test/random/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.test 2 | 3 | import scala.util.Random 4 | 5 | package object random { 6 | 7 | def randomStrOpt: Option[String] = Some( randomStr ) 8 | def randomStr: String = randomStrWith(length = 20) 9 | def randomStrWith(length: Int): String = 10 | Random.alphanumeric 11 | .take(length).mkString 12 | def randomStrPair = randomStr -> randomStr 13 | 14 | def randomInt: Int = Random.nextInt() 15 | 16 | def randomBytes(length:Int): Array[Byte] = { 17 | val result = Array.ofDim[Byte](length) 18 | Random.nextBytes(result) 19 | result 20 | } 21 | 22 | def randomInt(from: Int, to: Int): Int = { 23 | require(math.abs(to.toDouble - from.toDouble) <= Int.MaxValue.toDouble, s"Range can't exceed ${Int.MaxValue}") 24 | from + Random.nextInt(math.max(to - from, 1)) 25 | } 26 | 27 | def randomPort = randomInt(0, 65535) 28 | 29 | def randomPath = "/" + Seq.fill(5)(randomStr).mkString("/") 30 | def randomParameter = randomStr -> randomStr 31 | def randomHeader = randomStr -> randomStr 32 | 33 | } 34 | -------------------------------------------------------------------------------- /http-testkit/src/main/scala/com/wix/e2e/http/client/async/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client 2 | 3 | package object async extends NonBlockingHttpClientSupport -------------------------------------------------------------------------------- /http-testkit/src/main/scala/com/wix/e2e/http/client/sync/package.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.client 2 | 3 | package object sync extends BlockingHttpClientSupport -------------------------------------------------------------------------------- /http-testkit/src/main/scala/com/wix/e2e/http/server/WebServerFactory.scala: -------------------------------------------------------------------------------- 1 | package com.wix.e2e.http.server 2 | 3 | import com.wix.e2e.http.RequestHandler 4 | import com.wix.e2e.http.server.builders.{MockWebServerBuilder, StubWebServerBuilder} 5 | 6 | object WebServerFactory { 7 | def aStubWebServer: StubWebServerBuilder = new StubWebServerBuilder(Seq.empty, None) 8 | 9 | def aMockWebServer: MockWebServerBuilder = aMockWebServerWith(Seq.empty) 10 | def aMockWebServerWith(handler: RequestHandler, handlers: RequestHandler*): MockWebServerBuilder = aMockWebServerWith(handler +: handlers) 11 | def aMockWebServerWith(handlers: Seq[RequestHandler]): MockWebServerBuilder = new MockWebServerBuilder(handlers, None) 12 | } 13 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.0 2 | -------------------------------------------------------------------------------- /project/depends.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object depends { 4 | 5 | private val JacksonVersion = "2.13.1" 6 | private val AkkaHttpVersion = "10.2.7" 7 | private val AkkaVersion = "2.6.18" 8 | private val Specs2Version = "4.13.1" 9 | 10 | val specs2 = 11 | Seq("org.specs2" %% "specs2-core" % Specs2Version, 12 | "org.specs2" %% "specs2-junit" % Specs2Version, 13 | "org.specs2" %% "specs2-shapeless" % Specs2Version, 14 | "org.specs2" %% "specs2-mock" % Specs2Version ) 15 | val specs2Test = specs2.map(_ % Test) 16 | 17 | val scalaTest = "org.scalatest" %% "scalatest" % "3.2.10" 18 | 19 | val akkaHttp = 20 | Seq("com.typesafe.akka" %% "akka-http" % AkkaHttpVersion, 21 | "com.typesafe.akka" %% "akka-actor" % AkkaVersion, 22 | "com.typesafe.akka" %% "akka-stream" % AkkaVersion) 23 | 24 | val jackson = jacksonFor(JacksonVersion) 25 | 26 | private def jacksonFor(version: String) = 27 | Seq("com.fasterxml.jackson.core" % "jackson-databind" % version, 28 | "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % version, 29 | "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % version, 30 | "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % version, 31 | "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % version, 32 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % version ) 33 | 34 | val joda = Seq("joda-time" % "joda-time" % "2.10.13", 35 | "org.joda" % "joda-convert" % "2.2.2" ) 36 | 37 | val scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.3.0" 38 | 39 | val reflections = "org.reflections" % "reflections" % "0.10.2" 40 | 41 | val jsr305 = "com.google.code.findbugs" % "jsr305" % "3.0.2" 42 | 43 | val slf4jApi = "org.slf4j" % "slf4j-api" % "1.7.32" 44 | } 45 | -------------------------------------------------------------------------------- /project/pgp.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | -------------------------------------------------------------------------------- /project/release.sbt: -------------------------------------------------------------------------------- 1 | 2 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.9") 4 | -------------------------------------------------------------------------------- /project/sonatype.sbt: -------------------------------------------------------------------------------- 1 | 2 | 3 | credentials ++= ( 4 | for { 5 | username <- Option( System.getenv().get( "SONATYPE_USERNAME" ) ) 6 | password <- Option( System.getenv().get( "SONATYPE_PASSWORD" ) ) 7 | } yield Credentials( "Sonatype Nexus Repository Manager", "oss.sonatype.org", username, password ) 8 | ).toSeq 9 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.1.26-SNAPSHOT" --------------------------------------------------------------------------------