-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
ScalaWSSpec.scala
263 lines (222 loc) · 10.3 KB
/
ScalaWSSpec.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/
package play.it.libs
import org.specs2.matcher.MatchResult
import play.api.http.HeaderNames
import play.api.libs.ws.WSBodyReadables
import play.api.libs.ws.WSBodyWritables
import play.api.libs.oauth._
import play.api.test.PlaySpecification
import play.it.AkkaHttpIntegrationSpecification
import play.it.NettyIntegrationSpecification
import play.it.ServerIntegrationSpecification
class NettyScalaWSSpec extends ScalaWSSpec with NettyIntegrationSpecification
class AkkaHttpScalaWSSpec extends ScalaWSSpec with AkkaHttpIntegrationSpecification
trait ScalaWSSpec
extends PlaySpecification
with ServerIntegrationSpecification
with WSBodyWritables
with WSBodyReadables {
import java.io.File
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import akka.stream.scaladsl.FileIO
import akka.stream.scaladsl.Sink
import akka.stream.scaladsl.Source
import akka.util.ByteString
import play.api.libs.json.JsString
import play.api.libs.streams.Accumulator
import play.api.libs.ws._
import play.api.mvc.Results.Ok
import play.api.mvc._
import play.api.test._
import play.core.server.Server
import play.it.tools.HttpBinApplication
import play.shaded.ahc.org.asynchttpclient.RequestBuilderBase
import play.shaded.ahc.org.asynchttpclient.SignatureCalculator
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.concurrent.Await
import scala.concurrent.Future
"Web service client" title
sequential
"play.api.libs.ws.WSClient" should {
"make GET Requests" in withServer { ws =>
val req = ws.url("/get").get()
Await.result(req, Duration(1, SECONDS)).status.aka("status") must_== 200
}
"Get 404 errors" in withServer { ws =>
val req = ws.url("/post").get()
Await.result(req, Duration(1, SECONDS)).status.aka("status") must_== 404
}
"get a streamed response" in withResult(Results.Ok.chunked(Source(List("a", "b", "c")))) { ws =>
val res: Future[WSResponse] = ws.url("/get").stream()
val body: Source[ByteString, _] = await(res).bodyAsSource
val result: MatchResult[Any] = await(body.runWith(foldingSink)).utf8String.aka("streamed response") must_== "abc"
result
}
"streaming a request body" in withEchoServer { ws =>
val source = Source(List("a", "b", "c").map(ByteString.apply))
val res = ws.url("/post").withMethod("POST").withBody(source).execute()
val body = await(res).body
body must_== "abc"
}
"streaming a request body with manual content length" in withHeaderCheck { ws =>
val source = Source.single(ByteString("abc"))
val res = ws.url("/post").withMethod("POST").addHttpHeaders(CONTENT_LENGTH -> "3").withBody(source).execute()
val body = await(res).body
body must_== s"Content-Length: 3; Transfer-Encoding: -1"
}
"send a multipart request body" in withServer { ws =>
val file = new File(this.getClass.getResource("/testassets/foo.txt").toURI).toPath
val dp = MultipartFormData.DataPart("hello", "world")
val fp = MultipartFormData.FilePart("upload", "foo.txt", None, FileIO.fromPath(file))
val source: Source[MultipartFormData.Part[Source[ByteString, _]], _] = Source(List(dp, fp))
val res = ws.url("/post").post(source)
val jsonBody = await(res).json
(jsonBody \ "form" \ "hello").toOption must beSome(JsString("world"))
(jsonBody \ "file").toOption must beSome(JsString("This is a test asset."))
}
"send a multipart request body via withBody" in withServer { ws =>
val file = new File(this.getClass.getResource("/testassets/foo.txt").toURI)
val dp = MultipartFormData.DataPart("hello", "world")
val fp = MultipartFormData.FilePart("upload", "foo.txt", None, FileIO.fromPath(file.toPath))
val source = Source(List(dp, fp))
val res = ws.url("/post").withBody(source).withMethod("POST").execute()
val body = await(res).json
(body \ "form" \ "hello").toOption must beSome(JsString("world"))
(body \ "file").toOption must beSome(JsString("This is a test asset."))
}
"send a multipart request body with escaped 'name' and 'filename' params" in withEchoServer { ws =>
val file = new File(this.getClass.getResource("/testassets/foo.txt").toURI)
val dp = MultipartFormData.DataPart("f\ni\re\"l\nd1", "world")
val fp = MultipartFormData.FilePart(
"f\"i\rl\nef\"ie\nld\r1",
"f\rir\"s\ntf\ril\"e\n.txt",
None,
FileIO.fromPath(file.toPath)
)
val source = Source(List(dp, fp))
val res = ws.url("/post").withBody(source).withMethod("POST").execute()
val body = await(res).body
body must contain("""Content-Disposition: form-data; name="f%0Ai%0De%22l%0Ad1"""")
body must contain(
"""Content-Disposition: form-data; name="f%22i%0Dl%0Aef%22ie%0Ald%0D1"; filename="f%0Dir%22s%0Atf%0Dil%22e%0A.txt""""
)
}
"not throw an exception while signing requests" >> {
val calc = new CustomSigner
"without query string" in withServer { ws =>
ws.url("/").sign(calc).get().aka("signed request") must not(throwA[NullPointerException])
}
"with query string" in withServer { ws =>
ws.url("/").withQueryStringParameters("lorem" -> "ipsum").sign(calc).aka("signed request") must not(
throwA[Exception]
)
}
}
"preserve the case of an Authorization header" >> {
def withAuthorizationCheck[T](block: play.api.libs.ws.WSClient => T) = {
Server.withRouterFromComponents() { c =>
{
case _ =>
c.defaultActionBuilder { (req: Request[AnyContent]) =>
Results.Ok(req.headers.keys.filter(_.equalsIgnoreCase("authorization")).mkString)
}
}
} { implicit port => WsTestClient.withClient(block) }
}
"when signing with the OAuthCalculator" in {
val oauthCalc = {
val consumerKey = ConsumerKey("key", "secret")
val requestToken = RequestToken("token", "secret")
OAuthCalculator(consumerKey, requestToken)
}
"expect title-case header with signed request" in withAuthorizationCheck { ws =>
val body = await(ws.url("/").sign(oauthCalc).execute()).body
body must beEqualTo("Authorization").ignoreCase
}
}
// Attempt to replicate https://github.com/playframework/playframework/issues/7735
"when signing with a custom calculator" in {
val customCalc = new WSSignatureCalculator with SignatureCalculator {
def calculateAndAddSignature(
request: play.shaded.ahc.org.asynchttpclient.Request,
requestBuilder: RequestBuilderBase[_]
) = {
requestBuilder.addHeader(HeaderNames.AUTHORIZATION, "some value")
}
}
"expect title-case header with signed request" in withAuthorizationCheck { ws =>
val body = await(ws.url("/").sign(customCalc).execute()).body
body must_== ("Authorization")
}
}
// Attempt to replicate https://github.com/playframework/playframework/issues/7735
"when sending an explicit header" in {
"preserve a title-case 'Authorization' header" in withAuthorizationCheck { ws =>
val body = await(ws.url("/").withHttpHeaders("Authorization" -> "some value").execute()).body
body must_== ("Authorization")
}
"preserve a lower-case 'authorization' header" in withAuthorizationCheck { ws =>
val body = await(ws.url("/").withHttpHeaders("authorization" -> "some value").execute()).body
body must_== ("authorization")
}
}
}
}
def app = HttpBinApplication.app
val foldingSink = Sink.fold[ByteString, ByteString](ByteString.empty)((state, bs) => state ++ bs)
val isoString = {
// Converts the String "Hello €" to the ISO Counterparty
val sourceCharset = StandardCharsets.UTF_8
val buffer = ByteBuffer.wrap("Hello €".getBytes(sourceCharset))
val data = sourceCharset.decode(buffer)
val targetCharset = Charset.forName("Windows-1252")
new String(targetCharset.encode(data).array(), targetCharset)
}
implicit val materializer = app.materializer
def withServer[T](block: play.api.libs.ws.WSClient => T) = {
Server.withApplication(app) { implicit port => WsTestClient.withClient(block) }
}
def withEchoServer[T](block: play.api.libs.ws.WSClient => T) = {
def echo = BodyParser { req =>
Accumulator.source[ByteString].mapFuture { source => Future.successful(source).map(Right.apply) }
}
Server.withRouterFromComponents() { components =>
{
case _ =>
components.defaultActionBuilder(echo) { (req: Request[Source[ByteString, _]]) => Ok.chunked(req.body) }
}
} { implicit port => WsTestClient.withClient(block) }
}
def withResult[T](result: Result)(block: play.api.libs.ws.WSClient => T): T = {
Server.withRouterFromComponents() { c =>
{
case _ => c.defaultActionBuilder(result)
}
} { implicit port => WsTestClient.withClient(block) }
}
def withHeaderCheck[T](block: play.api.libs.ws.WSClient => T) = {
Server.withRouterFromComponents() { c =>
{
case _ =>
c.defaultActionBuilder { (req: Request[AnyContent]) =>
val contentLength = req.headers.get(CONTENT_LENGTH)
val transferEncoding = req.headers.get(TRANSFER_ENCODING)
Ok(s"Content-Length: ${contentLength.getOrElse(-1)}; Transfer-Encoding: ${transferEncoding.getOrElse(-1)}")
}
}
} { implicit port => WsTestClient.withClient(block) }
}
class CustomSigner extends WSSignatureCalculator with SignatureCalculator {
def calculateAndAddSignature(
request: play.shaded.ahc.org.asynchttpclient.Request,
requestBuilder: RequestBuilderBase[_]
) = {
// do nothing
}
}
}