OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library curl_client_test; | |
6 | |
7 import 'dart:io'; | |
8 import 'dart:isolate'; | |
9 import 'dart:json' as json; | |
10 import 'dart:uri'; | |
11 | |
12 import '../../../pkg/unittest/lib/unittest.dart'; | |
13 import '../../../pkg/http/lib/http.dart' as http; | |
14 import '../../pub/curl_client.dart'; | |
15 import '../../pub/io.dart'; | |
16 import '../../pub/utils.dart'; | |
17 | |
18 // TODO(rnystrom): All of the code from here to the first "---..." line was | |
19 // copied from pkg/http/test/utils.dart and pkg/http/lib/src/utils.dart. It's | |
20 // copied here because http/test/utils.dart is now using "package:" imports and | |
21 // this is not. You cannot mix those because you end up with duplicate copies of | |
22 // the same library in memory. Since curl_client is going away soon anyway, I'm | |
23 // just copying the code here. Delete all of this when curl client is removed. | |
24 | |
25 /// Returns the [Encoding] that corresponds to [charset]. Throws a | |
26 /// [FormatException] if no [Encoding] was found that corresponds to [charset]. | |
27 /// [charset] may not be null. | |
28 Encoding requiredEncodingForCharset(String charset) { | |
29 var encoding = _encodingForCharset(charset); | |
30 if (encoding != null) return encoding; | |
31 throw new FormatException('Unsupported encoding "$charset".'); | |
32 } | |
33 | |
34 /// Returns the [Encoding] that corresponds to [charset]. Returns null if no | |
35 /// [Encoding] was found that corresponds to [charset]. [charset] may not be | |
36 /// null. | |
37 Encoding _encodingForCharset(String charset) { | |
38 charset = charset.toLowerCase(); | |
39 if (charset == 'ascii' || charset == 'us-ascii') return Encoding.ASCII; | |
40 if (charset == 'utf-8') return Encoding.UTF_8; | |
41 if (charset == 'iso-8859-1') return Encoding.ISO_8859_1; | |
42 return null; | |
43 } | |
44 | |
45 /// Converts [bytes] into a [String] according to [encoding]. | |
46 String decodeString(List<int> bytes, Encoding encoding) { | |
47 // TODO(nweiz): implement this once issue 6284 is fixed. | |
48 return new String.fromCharCodes(bytes); | |
49 } | |
50 | |
51 /// The current server instance. | |
52 HttpServer _server; | |
53 | |
54 /// The URL for the current server instance. | |
55 Uri get serverUrl => Uri.parse('http://localhost:${_server.port}'); | |
56 | |
57 /// A dummy URL for constructing requests that won't be sent. | |
58 Uri get dummyUrl => Uri.parse('http://dartlang.org/'); | |
59 | |
60 /// Starts a new HTTP server. | |
61 void startServer() { | |
62 _server = new HttpServer(); | |
63 | |
64 _server.addRequestHandler((request) => request.path == '/error', | |
65 (request, response) { | |
66 response.statusCode = 400; | |
67 response.contentLength = 0; | |
68 response.outputStream.close(); | |
69 }); | |
70 | |
71 _server.addRequestHandler((request) => request.path == '/loop', | |
72 (request, response) { | |
73 var n = int.parse(Uri.parse(request.uri).query); | |
74 response.statusCode = 302; | |
75 response.headers.set('location', | |
76 serverUrl.resolve('/loop?${n + 1}').toString()); | |
77 response.contentLength = 0; | |
78 response.outputStream.close(); | |
79 }); | |
80 | |
81 _server.addRequestHandler((request) => request.path == '/redirect', | |
82 (request, response) { | |
83 response.statusCode = 302; | |
84 response.headers.set('location', serverUrl.resolve('/').toString()); | |
85 response.contentLength = 0; | |
86 response.outputStream.close(); | |
87 }); | |
88 | |
89 _server.defaultRequestHandler = (request, response) { | |
90 consumeInputStream(request.inputStream).then((requestBodyBytes) { | |
91 response.statusCode = 200; | |
92 response.headers.contentType = new ContentType("application", "json"); | |
93 | |
94 var requestBody; | |
95 if (requestBodyBytes.isEmpty) { | |
96 requestBody = null; | |
97 } else if (request.headers.contentType.charset != null) { | |
98 var encoding = requiredEncodingForCharset( | |
99 request.headers.contentType.charset); | |
100 requestBody = decodeString(requestBodyBytes, encoding); | |
101 } else { | |
102 requestBody = requestBodyBytes; | |
103 } | |
104 | |
105 var content = { | |
106 'method': request.method, | |
107 'path': request.path, | |
108 'headers': {} | |
109 }; | |
110 if (requestBody != null) content['body'] = requestBody; | |
111 request.headers.forEach((name, values) { | |
112 // These headers are automatically generated by dart:io, so we don't | |
113 // want to test them here. | |
114 if (name == 'cookie' || name == 'host') return; | |
115 | |
116 content['headers'][name] = values; | |
117 }); | |
118 | |
119 var outputEncoding; | |
120 var encodingName = request.queryParameters['response-encoding']; | |
121 if (encodingName != null) { | |
122 outputEncoding = requiredEncodingForCharset(encodingName); | |
123 } else { | |
124 outputEncoding = Encoding.ASCII; | |
125 } | |
126 | |
127 var body = json.stringify(content); | |
128 response.contentLength = body.length; | |
129 response.outputStream.writeString(body, outputEncoding); | |
130 response.outputStream.close(); | |
131 }); | |
132 }; | |
133 | |
134 _server.listen("127.0.0.1", 0); | |
135 } | |
136 | |
137 /// Stops the current HTTP server. | |
138 void stopServer() { | |
139 _server.close(); | |
140 _server = null; | |
141 } | |
142 | |
143 /// A matcher that matches JSON that parses to a value that matches the inner | |
144 /// matcher. | |
145 Matcher parse(matcher) => new _Parse(matcher); | |
146 | |
147 class _Parse extends BaseMatcher { | |
148 final Matcher _matcher; | |
149 | |
150 _Parse(this._matcher); | |
151 | |
152 bool matches(item, MatchState matchState) { | |
153 if (item is! String) return false; | |
154 | |
155 var parsed; | |
156 try { | |
157 parsed = json.parse(item); | |
158 } catch (e) { | |
159 return false; | |
160 } | |
161 | |
162 return _matcher.matches(parsed, matchState); | |
163 } | |
164 | |
165 Description describe(Description description) { | |
166 return description.add('parses to a value that ') | |
167 .addDescriptionOf(_matcher); | |
168 } | |
169 } | |
170 | |
171 /// A matcher for HttpExceptions. | |
172 const isHttpException = const _HttpException(); | |
173 | |
174 /// A matcher for functions that throw HttpException. | |
175 const Matcher throwsHttpException = | |
176 const Throws(isHttpException); | |
177 | |
178 class _HttpException extends TypeMatcher { | |
179 const _HttpException() : super("HttpException"); | |
180 bool matches(item, MatchState matchState) => item is HttpException; | |
181 } | |
182 | |
183 /// A matcher for RedirectLimitExceededExceptions. | |
184 const isRedirectLimitExceededException = | |
185 const _RedirectLimitExceededException(); | |
186 | |
187 /// A matcher for functions that throw RedirectLimitExceededException. | |
188 const Matcher throwsRedirectLimitExceededException = | |
189 const Throws(isRedirectLimitExceededException); | |
190 | |
191 class _RedirectLimitExceededException extends TypeMatcher { | |
192 const _RedirectLimitExceededException() : | |
193 super("RedirectLimitExceededException"); | |
194 | |
195 bool matches(item, MatchState matchState) => | |
196 item is RedirectLimitExceededException; | |
197 } | |
198 | |
199 // ---------------------------------------------------------------------------- | |
200 | |
201 void main() { | |
202 setUp(startServer); | |
203 tearDown(stopServer); | |
204 | |
205 test('head', () { | |
206 expect(new CurlClient().head(serverUrl).then((response) { | |
207 expect(response.statusCode, equals(200)); | |
208 expect(response.body, equals('')); | |
209 }), completes); | |
210 }); | |
211 | |
212 test('get', () { | |
213 expect(new CurlClient().get(serverUrl, headers: { | |
214 'X-Random-Header': 'Value', | |
215 'X-Other-Header': 'Other Value' | |
216 }).then((response) { | |
217 expect(response.statusCode, equals(200)); | |
218 expect(response.body, parse(equals({ | |
219 'method': 'GET', | |
220 'path': '/', | |
221 'headers': { | |
222 'x-random-header': ['Value'], | |
223 'x-other-header': ['Other Value'] | |
224 }, | |
225 }))); | |
226 }), completes); | |
227 }); | |
228 | |
229 test('post', () { | |
230 expect(new CurlClient().post(serverUrl, headers: { | |
231 'X-Random-Header': 'Value', | |
232 'X-Other-Header': 'Other Value' | |
233 }, fields: { | |
234 'some-field': 'value', | |
235 'other-field': 'other value' | |
236 }).then((response) { | |
237 expect(response.statusCode, equals(200)); | |
238 expect(response.body, parse(equals({ | |
239 'method': 'POST', | |
240 'path': '/', | |
241 'headers': { | |
242 'content-type': [ | |
243 'application/x-www-form-urlencoded; charset=UTF-8' | |
244 ], | |
245 'content-length': ['40'], | |
246 'x-random-header': ['Value'], | |
247 'x-other-header': ['Other Value'] | |
248 }, | |
249 'body': 'some-field=value&other-field=other+value' | |
250 }))); | |
251 }), completes); | |
252 }); | |
253 | |
254 test('post without fields', () { | |
255 expect(new CurlClient().post(serverUrl, headers: { | |
256 'X-Random-Header': 'Value', | |
257 'X-Other-Header': 'Other Value', | |
258 'Content-Type': 'text/plain' | |
259 }).then((response) { | |
260 expect(response.statusCode, equals(200)); | |
261 expect(response.body, parse(equals({ | |
262 'method': 'POST', | |
263 'path': '/', | |
264 'headers': { | |
265 'content-type': ['text/plain'], | |
266 'x-random-header': ['Value'], | |
267 'x-other-header': ['Other Value'] | |
268 } | |
269 }))); | |
270 }), completes); | |
271 }); | |
272 | |
273 test('put', () { | |
274 expect(new CurlClient().put(serverUrl, headers: { | |
275 'X-Random-Header': 'Value', | |
276 'X-Other-Header': 'Other Value' | |
277 }, fields: { | |
278 'some-field': 'value', | |
279 'other-field': 'other value' | |
280 }).then((response) { | |
281 expect(response.statusCode, equals(200)); | |
282 expect(response.body, parse(equals({ | |
283 'method': 'PUT', | |
284 'path': '/', | |
285 'headers': { | |
286 'content-type': [ | |
287 'application/x-www-form-urlencoded; charset=UTF-8' | |
288 ], | |
289 'content-length': ['40'], | |
290 'x-random-header': ['Value'], | |
291 'x-other-header': ['Other Value'] | |
292 }, | |
293 'body': 'some-field=value&other-field=other+value' | |
294 }))); | |
295 }), completes); | |
296 }); | |
297 | |
298 test('put without fields', () { | |
299 expect(new CurlClient().put(serverUrl, headers: { | |
300 'X-Random-Header': 'Value', | |
301 'X-Other-Header': 'Other Value', | |
302 'Content-Type': 'text/plain' | |
303 }).then((response) { | |
304 expect(response.statusCode, equals(200)); | |
305 expect(response.body, parse(equals({ | |
306 'method': 'PUT', | |
307 'path': '/', | |
308 'headers': { | |
309 'content-type': ['text/plain'], | |
310 'x-random-header': ['Value'], | |
311 'x-other-header': ['Other Value'] | |
312 } | |
313 }))); | |
314 }), completes); | |
315 }); | |
316 | |
317 test('delete', () { | |
318 expect(new CurlClient().delete(serverUrl, headers: { | |
319 'X-Random-Header': 'Value', | |
320 'X-Other-Header': 'Other Value' | |
321 }).then((response) { | |
322 expect(response.statusCode, equals(200)); | |
323 expect(response.body, parse(equals({ | |
324 'method': 'DELETE', | |
325 'path': '/', | |
326 'headers': { | |
327 'x-random-header': ['Value'], | |
328 'x-other-header': ['Other Value'] | |
329 } | |
330 }))); | |
331 }), completes); | |
332 }); | |
333 | |
334 test('read', () { | |
335 expect(new CurlClient().read(serverUrl, headers: { | |
336 'X-Random-Header': 'Value', | |
337 'X-Other-Header': 'Other Value' | |
338 }), completion(parse(equals({ | |
339 'method': 'GET', | |
340 'path': '/', | |
341 'headers': { | |
342 'x-random-header': ['Value'], | |
343 'x-other-header': ['Other Value'] | |
344 }, | |
345 })))); | |
346 }); | |
347 | |
348 test('read throws an error for a 4** status code', () { | |
349 expect(new CurlClient().read(serverUrl.resolve('/error')), | |
350 throwsHttpException); | |
351 }); | |
352 | |
353 test('readBytes', () { | |
354 var future = new CurlClient().readBytes(serverUrl, headers: { | |
355 'X-Random-Header': 'Value', | |
356 'X-Other-Header': 'Other Value' | |
357 }).then((bytes) => new String.fromCharCodes(bytes)); | |
358 | |
359 expect(future, completion(parse(equals({ | |
360 'method': 'GET', | |
361 'path': '/', | |
362 'headers': { | |
363 'x-random-header': ['Value'], | |
364 'x-other-header': ['Other Value'] | |
365 }, | |
366 })))); | |
367 }); | |
368 | |
369 test('readBytes throws an error for a 4** status code', () { | |
370 expect(new CurlClient().readBytes(serverUrl.resolve('/error')), | |
371 throwsHttpException); | |
372 }); | |
373 | |
374 test('#send a StreamedRequest', () { | |
375 var client = new CurlClient(); | |
376 var request = new http.StreamedRequest("POST", serverUrl); | |
377 request.headers[HttpHeaders.CONTENT_TYPE] = | |
378 'application/json; charset=utf-8'; | |
379 | |
380 var future = client.send(request).then((response) { | |
381 expect(response.statusCode, equals(200)); | |
382 return response.stream.bytesToString(); | |
383 }).whenComplete(client.close); | |
384 | |
385 expect(future, completion(parse(equals({ | |
386 'method': 'POST', | |
387 'path': '/', | |
388 'headers': { | |
389 'content-type': ['application/json; charset=utf-8'], | |
390 'transfer-encoding': ['chunked'] | |
391 }, | |
392 'body': '{"hello": "world"}' | |
393 })))); | |
394 | |
395 request.sink.add('{"hello": "world"}'.charCodes); | |
396 request.sink.close(); | |
397 }); | |
398 | |
399 test('with one redirect', () { | |
400 var url = serverUrl.resolve('/redirect'); | |
401 expect(new CurlClient().get(url).then((response) { | |
402 expect(response.statusCode, equals(200)); | |
403 expect(response.body, parse(equals({ | |
404 'method': 'GET', | |
405 'path': '/', | |
406 'headers': {} | |
407 }))); | |
408 }), completes); | |
409 }); | |
410 | |
411 test('with too many redirects', () { | |
412 expect(new CurlClient().get(serverUrl.resolve('/loop?1')), | |
413 throwsRedirectLimitExceededException); | |
414 }); | |
415 | |
416 test('with a generic failure', () { | |
417 expect(new CurlClient().get('url fail'), | |
418 throwsHttpException); | |
419 }); | |
420 | |
421 test('with one redirect via HEAD', () { | |
422 var url = serverUrl.resolve('/redirect'); | |
423 expect(new CurlClient().head(url).then((response) { | |
424 expect(response.statusCode, equals(200)); | |
425 }), completes); | |
426 }); | |
427 | |
428 test('with too many redirects via HEAD', () { | |
429 expect(new CurlClient().head(serverUrl.resolve('/loop?1')), | |
430 throwsRedirectLimitExceededException); | |
431 }); | |
432 | |
433 test('with a generic failure via HEAD', () { | |
434 expect(new CurlClient().head('url fail'), | |
435 throwsHttpException); | |
436 }); | |
437 | |
438 test('without following redirects', () { | |
439 var request = new http.Request('GET', serverUrl.resolve('/redirect')); | |
440 request.followRedirects = false; | |
441 expect(new CurlClient().send(request).then(http.Response.fromStream) | |
442 .then((response) { | |
443 expect(response.statusCode, equals(302)); | |
444 expect(response.isRedirect, true); | |
445 }), completes); | |
446 }); | |
447 } | |
OLD | NEW |