OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, 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 import 'dart:async'; |
| 6 import 'dart:convert'; |
| 7 |
| 8 import 'package:crypto/crypto.dart' as crypto; |
| 9 import 'package:http/http.dart' as http; |
| 10 import 'package:http/testing.dart' as http_testing; |
| 11 import 'package:http_parser/http_parser.dart' as http_parser; |
| 12 import 'package:mime/mime.dart' as mime; |
| 13 import 'package:unittest/unittest.dart'; |
| 14 |
| 15 const CONTENT_TYPE_JSON_UTF8 = 'application/json; charset=utf-8'; |
| 16 |
| 17 const RESPONSE_HEADERS = const { |
| 18 'content-type': CONTENT_TYPE_JSON_UTF8 |
| 19 }; |
| 20 |
| 21 class MockClient extends http.BaseClient { |
| 22 final String rootPath; |
| 23 final Uri rootUri; |
| 24 |
| 25 Map<String, Map<Pattern, Function>> mocks = {}; |
| 26 http_testing.MockClient client; |
| 27 |
| 28 MockClient(String rootPath) : |
| 29 rootPath = rootPath, |
| 30 rootUri = Uri.parse('https://www.googleapis.com${rootPath}') { |
| 31 client = new http_testing.MockClient(handler); |
| 32 } |
| 33 |
| 34 void register(String method, Pattern path, |
| 35 http_testing.MockClientHandler handler) { |
| 36 var map = mocks.putIfAbsent(method, () => new Map()); |
| 37 if (path is RegExp) { |
| 38 map[new RegExp('$rootPath${path.pattern}')] = handler; |
| 39 } else { |
| 40 map['$rootPath$path'] = handler; |
| 41 } |
| 42 } |
| 43 |
| 44 void registerUpload(String method, Pattern path, |
| 45 http_testing.MockClientHandler handler) { |
| 46 var map = mocks.putIfAbsent(method, () => new Map()); |
| 47 map['/upload$rootPath$path'] = handler; |
| 48 } |
| 49 |
| 50 void registerResumableUpload(String method, Pattern path, |
| 51 http_testing.MockClientHandler handler) { |
| 52 var map = mocks.putIfAbsent(method, () => new Map()); |
| 53 map['/resumable/upload$rootPath$path'] = handler; |
| 54 } |
| 55 |
| 56 void clear() { |
| 57 mocks = {}; |
| 58 } |
| 59 |
| 60 Future<http.Response> handler(http.Request request) { |
| 61 expect(request.url.host, 'www.googleapis.com'); |
| 62 var path = request.url.path; |
| 63 if (mocks[request.method] == null) { |
| 64 throw 'No mock handler for method ${request.method} found. ' |
| 65 'Request URL was: ${request.url}'; |
| 66 } |
| 67 var mockHandler; |
| 68 mocks[request.method].forEach((pattern, handler) { |
| 69 if (pattern.matchAsPrefix(path) != null) { |
| 70 mockHandler = handler; |
| 71 } |
| 72 }); |
| 73 if (mockHandler == null) { |
| 74 throw 'No mock handler for method ${request.method} and path ' |
| 75 '[$path] found. Request URL was: ${request.url}'; |
| 76 } |
| 77 return mockHandler(request); |
| 78 } |
| 79 |
| 80 Future<http.StreamedResponse> send(http.BaseRequest request) { |
| 81 return client.send(request); |
| 82 } |
| 83 |
| 84 Future<http.Response> respond(response) { |
| 85 return new Future.value( |
| 86 new http.Response( |
| 87 JSON.encode(response.toJson()), |
| 88 200, |
| 89 headers: RESPONSE_HEADERS)); |
| 90 } |
| 91 |
| 92 Future<http.Response> respondEmpty() { |
| 93 return new Future.value( |
| 94 new http.Response('', 200, headers: RESPONSE_HEADERS)); |
| 95 } |
| 96 |
| 97 Future<http.Response> respondInitiateResumableUpload(project) { |
| 98 Map headers = new Map.from(RESPONSE_HEADERS); |
| 99 headers['location'] = |
| 100 'https://www.googleapis.com/resumable/upload$rootPath' |
| 101 'b/$project/o?uploadType=resumable&alt=json&' |
| 102 'upload_id=AEnB2UqucpaWy7d5cr5iVQzmbQcQlLDIKiClrm0SAX3rJ7UN' |
| 103 'Mu5bEoC9b4teJcJUKpqceCUeqKzuoP_jz2ps_dV0P0nT8OTuZQ'; |
| 104 return new Future.value( |
| 105 new http.Response('', 200, headers: headers)); |
| 106 } |
| 107 |
| 108 Future<http.Response> respondContinueResumableUpload() { |
| 109 return new Future.value( |
| 110 new http.Response('', 308, headers: RESPONSE_HEADERS)); |
| 111 } |
| 112 |
| 113 Future<http.Response> respondBytes(List<int> bytes) { |
| 114 return new Future.value( |
| 115 new http.Response.bytes(bytes, 200, headers: RESPONSE_HEADERS)); |
| 116 } |
| 117 |
| 118 Future<http.Response> respondError(statusCode) { |
| 119 var error = { |
| 120 'error': { |
| 121 'code': statusCode, |
| 122 'message': 'error' |
| 123 } |
| 124 }; |
| 125 return new Future.value( |
| 126 new http.Response( |
| 127 JSON.encode(error), statusCode, headers: RESPONSE_HEADERS)); |
| 128 } |
| 129 |
| 130 Future processNormalMediaUpload(http.Request request) { |
| 131 var completer = new Completer(); |
| 132 |
| 133 var contentType = new http_parser.MediaType.parse( |
| 134 request.headers['content-type']); |
| 135 expect(contentType.mimeType, 'multipart/related'); |
| 136 var boundary = contentType.parameters['boundary']; |
| 137 |
| 138 var partCount = 0; |
| 139 var json; |
| 140 new Stream.fromIterable([request.bodyBytes, [13, 10]]) |
| 141 .transform(new mime.MimeMultipartTransformer(boundary)) |
| 142 .listen( |
| 143 ((mime.MimeMultipart mimeMultipart) { |
| 144 var contentType = mimeMultipart.headers['content-type']; |
| 145 partCount++; |
| 146 if (partCount == 1) { |
| 147 // First part in the object JSON. |
| 148 expect(contentType, 'application/json; charset=utf-8'); |
| 149 mimeMultipart |
| 150 .transform(UTF8.decoder) |
| 151 .fold('', (p, e) => '$p$e') |
| 152 .then((j) => json = j); |
| 153 } else if (partCount == 2) { |
| 154 // Second part is the base64 encoded bytes. |
| 155 mimeMultipart |
| 156 .transform(ASCII.decoder) |
| 157 .fold('', (p, e) => '$p$e') |
| 158 .then(crypto.CryptoUtils.base64StringToBytes) |
| 159 .then((bytes) { |
| 160 completer.complete( |
| 161 new NormalMediaUpload(json, bytes, contentType)); |
| 162 }); |
| 163 } else { |
| 164 // Exactly two parts expected. |
| 165 throw 'Unexpected part count'; |
| 166 } |
| 167 })); |
| 168 |
| 169 return completer.future; |
| 170 } |
| 171 } |
| 172 |
| 173 class NormalMediaUpload { |
| 174 final String json; |
| 175 final List<int> bytes; |
| 176 final String contentType; |
| 177 NormalMediaUpload(this.json, this.bytes, this.contentType); |
| 178 } |
| 179 |
| 180 // Implementation of http.Client which traces all requests and responses. |
| 181 // Mainly useful for local testing. |
| 182 class TraceClient extends http.BaseClient { |
| 183 final http.Client client; |
| 184 |
| 185 TraceClient(this.client); |
| 186 |
| 187 Future<http.StreamedResponse> send(http.BaseRequest request) { |
| 188 print(request); |
| 189 return request.finalize().toBytes().then((body) { |
| 190 print('--- START REQUEST ---'); |
| 191 print(UTF8.decode(body)); |
| 192 print('--- END REQUEST ---'); |
| 193 var r = new RequestImpl(request.method, request.url, body); |
| 194 r.headers.addAll(request.headers); |
| 195 return client.send(r).then((http.StreamedResponse rr) { |
| 196 return rr.stream.toBytes().then((body) { |
| 197 print('--- START RESPONSE ---'); |
| 198 print(UTF8.decode(body)); |
| 199 print('--- END RESPONSE ---'); |
| 200 return new http.StreamedResponse( |
| 201 new http.ByteStream.fromBytes(body), |
| 202 rr.statusCode, |
| 203 headers: rr.headers); |
| 204 |
| 205 }); |
| 206 }); |
| 207 }); |
| 208 } |
| 209 |
| 210 void close() { |
| 211 client.close(); |
| 212 } |
| 213 } |
| 214 |
| 215 // http.BaseRequest implementationn used by the TraceClient. |
| 216 class RequestImpl extends http.BaseRequest { |
| 217 final List<int> _body; |
| 218 |
| 219 RequestImpl(String method, Uri url, this._body) |
| 220 : super(method, url); |
| 221 |
| 222 http.ByteStream finalize() { |
| 223 super.finalize(); |
| 224 return new http.ByteStream.fromBytes(_body); |
| 225 } |
| 226 } |
OLD | NEW |