├── .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 | [](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"
--------------------------------------------------------------------------------