From 9ff1bc321cd201bee09f8420c42e61632481ffb7 Mon Sep 17 00:00:00 2001 From: Marko Vuksanovic Date: Fri, 9 May 2014 21:20:22 +1000 Subject: [PATCH] feat(Http): Http service can make cross-site requests (get, post, put, etc.) which use credentials (such as cookies or authorization headers). Closes #945 Closes #1026 --- lib/core_dom/http.dart | 60 +++++++++++++++++++------------- lib/mock/http_backend.dart | 52 +++++++++++++++++---------- test/core_dom/http_spec.dart | 9 +++++ test/mock/http_backend_spec.dart | 15 ++++++++ 4 files changed, 93 insertions(+), 43 deletions(-) diff --git a/lib/core_dom/http.dart b/lib/core_dom/http.dart index 72c9e7875..40b98437d 100644 --- a/lib/core_dom/http.dart +++ b/lib/core_dom/http.dart @@ -410,6 +410,8 @@ class Http { * - headers: Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function * is null, the header will not be sent. + * - withCredentials: True if cross-site requests should use credentials such as cookies or + * authorization headers; false otherwise. If not specified, defaults to false. * - xsrfHeaderName: TBI * - xsrfCookieName: TBI * - interceptors: Either a [HttpInterceptor] or a [HttpInterceptors] @@ -422,6 +424,7 @@ class Http { data, Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, @@ -481,7 +484,8 @@ class Http { var result = _backend.request(url, method: method, requestHeaders: config.headers, - sendData: config.data).then((dom.HttpRequest value) { + sendData: config.data, + withCredentials: withCredentials).then((dom.HttpRequest value) { // TODO: Uncomment after apps migrate off of this class. // assert(value.status >= 200 && value.status < 300); @@ -535,15 +539,16 @@ class Http { String data, Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, cache, timeout - }) => call(method: 'GET', url: url, data: data, params: params, - headers: headers, xsrfHeaderName: xsrfHeaderName, - xsrfCookieName: xsrfCookieName, interceptors: interceptors, - cache: cache, timeout: timeout); + }) => call(method: 'GET', url: url, data: data, params: params, headers: headers, + withCredentials: withCredentials, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, + timeout: timeout); /** * Shortcut method for DELETE requests. See [call] for a complete description @@ -553,15 +558,16 @@ class Http { String data, Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, cache, timeout - }) => call(method: 'DELETE', url: url, data: data, params: params, - headers: headers, xsrfHeaderName: xsrfHeaderName, - xsrfCookieName: xsrfCookieName, interceptors: interceptors, - cache: cache, timeout: timeout); + }) => call(method: 'DELETE', url: url, data: data, params: params, headers: headers, + withCredentials: withCredentials, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, + timeout: timeout); /** * Shortcut method for HEAD requests. See [call] for a complete description @@ -571,15 +577,16 @@ class Http { String data, Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, cache, timeout - }) => call(method: 'HEAD', url: url, data: data, params: params, - headers: headers, xsrfHeaderName: xsrfHeaderName, - xsrfCookieName: xsrfCookieName, interceptors: interceptors, - cache: cache, timeout: timeout); + }) => call(method: 'HEAD', url: url, data: data, params: params, headers: headers, + withCredentials: withCredentials, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, + timeout: timeout); /** * Shortcut method for PUT requests. See [call] for a complete description @@ -588,15 +595,16 @@ class Http { async.Future put(String url, String data, { Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, cache, timeout - }) => call(method: 'PUT', url: url, data: data, params: params, - headers: headers, xsrfHeaderName: xsrfHeaderName, - xsrfCookieName: xsrfCookieName, interceptors: interceptors, - cache: cache, timeout: timeout); + }) => call(method: 'PUT', url: url, data: data, params: params, headers: headers, + withCredentials: withCredentials, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, + timeout: timeout); /** * Shortcut method for POST requests. See [call] for a complete description @@ -605,15 +613,16 @@ class Http { async.Future post(String url, String data, { Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, cache, timeout - }) => call(method: 'POST', url: url, data: data, params: params, - headers: headers, xsrfHeaderName: xsrfHeaderName, - xsrfCookieName: xsrfCookieName, interceptors: interceptors, - cache: cache, timeout: timeout); + }) => call(method: 'POST', url: url, data: data, params: params, headers: headers, + withCredentials: withCredentials, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, + timeout: timeout); /** * Shortcut method for JSONP requests. See [call] for a complete description @@ -623,15 +632,16 @@ class Http { String data, Map params, Map headers, + bool withCredentials: false, xsrfHeaderName, xsrfCookieName, interceptors, cache, timeout - }) => call(method: 'JSONP', url: url, data: data, params: params, - headers: headers, xsrfHeaderName: xsrfHeaderName, - xsrfCookieName: xsrfCookieName, interceptors: interceptors, - cache: cache, timeout: timeout); + }) => call(method: 'JSONP', url: url, data: data, params: params, headers: headers, + withCredentials: withCredentials, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, + timeout: timeout); /** * Parse raw headers into key-value object diff --git a/lib/mock/http_backend.dart b/lib/mock/http_backend.dart index a626bc59f..2d472ccd8 100644 --- a/lib/mock/http_backend.dart +++ b/lib/mock/http_backend.dart @@ -64,20 +64,23 @@ class _MockXhr { * An internal class used by [MockHttpBackend]. */ class MockHttpExpectation { - final method; - final url; + final String method; + final /*String or RegExp*/ url; final data; final headers; + final bool withCredentials; var response; - MockHttpExpectation(this.method, this.url, [this.data, this.headers]); + MockHttpExpectation(this.method, this.url, [this.data, this.headers, withCredentials]) : + this.withCredentials = withCredentials == true; - bool match(method, url, [data, headers]) { + bool match(method, url, [data, headers, withCredentials]) { if (method != method) return false; if (!matchUrl(url)) return false; if (data != null && !matchData(data)) return false; if (headers != null && !matchHeaders(headers)) return false; + if (withCredentials != null && !matchWithCredentials(withCredentials)) return false; return true; } @@ -102,6 +105,8 @@ class MockHttpExpectation { return JSON.encode(data) == JSON.encode(d); } + bool matchWithCredentials(withCredentials) => this.withCredentials == withCredentials; + String toString() => "$method $url"; } @@ -124,7 +129,7 @@ class MockHttpBackend implements HttpBackend { * This function is called from [Http] and designed to mimic the Dart APIs. */ dart_async.Future request(String url, - {String method, bool withCredentials, String responseType, + {String method, bool withCredentials: false, String responseType, String mimeType, Map requestHeaders, sendData, void onProgress(ProgressEvent e)}) { dart_async.Completer c = new dart_async.Completer(); @@ -137,7 +142,7 @@ class MockHttpBackend implements HttpBackend { } }; call(method == null ? 'GET' : method, url, callback, - data: sendData, headers: requestHeaders); + data: sendData, headers: requestHeaders, withCredentials: withCredentials); return c.future; } @@ -163,7 +168,7 @@ class MockHttpBackend implements HttpBackend { * A callback oriented API. This function takes a callback with * will be called with (status, data, headers) */ - void call(method, url, callback, {data, headers, timeout}) { + void call(method, url, callback, {data, headers, timeout, withCredentials: false}) { var xhr = new _MockXhr(), expectation = expectations.isEmpty ? null : expectations[0], wasExpected = false; @@ -206,6 +211,11 @@ class MockHttpBackend implements HttpBackend { 'EXPECTED: ${prettyPrint(expectation.headers)}\n' 'GOT: ${prettyPrint(headers)}']; + if (!expectation.matchWithCredentials(withCredentials)) + throw ['Expected $expectation with different withCredentials\n' + 'EXPECTED: ${prettyPrint(expectation.withCredentials)}\n' + 'GOT: ${prettyPrint(withCredentials)}']; + expectations.removeAt(0); if (expectation.response != null) { @@ -216,7 +226,7 @@ class MockHttpBackend implements HttpBackend { } for (var definition in definitions) { - if (definition.match(method, url, data, headers != null ? headers : {})) { + if (definition.match(method, url, data, headers != null ? headers : {}, withCredentials)) { if (definition.response != null) { // if $browser specified, we do auto flush all requests responses.add(wrapResponse(definition)); @@ -248,8 +258,8 @@ class MockHttpBackend implements HttpBackend { * an array containing response status (number), response data (string) and response headers * (Object). */ - _Chain when(method, [url, data, headers]) { - var definition = new MockHttpExpectation(method, url, data, headers), + _Chain when(method, [url, data, headers, withCredentials = false]) { + var definition = new MockHttpExpectation(method, url, data, headers, withCredentials), chain = new _Chain(respond: (status, data, headers) { definition.response = _createResponse(status, data, headers); }); @@ -364,8 +374,8 @@ class MockHttpBackend implements HttpBackend { * an array containing response status (number), response data (string) and response headers * (Object). */ - _Chain expect(method, [url, data, headers]) { - var expectation = new MockHttpExpectation(method, url, data, headers); + _Chain expect(method, [url, data, headers, withCredentials = false]) { + var expectation = new MockHttpExpectation(method, url, data, headers, withCredentials); expectations.add(expectation); return new _Chain(respond: (status, data, headers) { expectation.response = _createResponse(status, data, headers); @@ -385,7 +395,8 @@ class MockHttpBackend implements HttpBackend { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. See #expect for more info. */ - _Chain expectGET(url, [headers]) => expect('GET', url, null, headers); + _Chain expectGET(url, [headers, withCredentials = false]) => expect('GET', url, null, headers, + withCredentials); /** * @ngdoc method @@ -399,7 +410,8 @@ class MockHttpBackend implements HttpBackend { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ - _Chain expectDELETE(url, [headers]) => expect('DELETE', url, null, headers); + _Chain expectDELETE(url, [headers, withCredentials = false]) => expect('DELETE', url, null, + headers, withCredentials); /** * @ngdoc method @@ -412,7 +424,8 @@ class MockHttpBackend implements HttpBackend { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ - _Chain expectJSONP(url, [headers]) => expect('JSONP', url, null, headers); + _Chain expectJSONP(url, [headers, withCredentials = false]) => expect('JSONP', url, null, headers, + withCredentials); /** * @ngdoc method @@ -427,7 +440,8 @@ class MockHttpBackend implements HttpBackend { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ - _Chain expectPUT(url, [data, headers]) => expect('PUT', url, data, headers); + _Chain expectPUT(url, [data, headers, withCredentials = false]) => expect('PUT', url, data, + headers, withCredentials); /** * @ngdoc method @@ -442,7 +456,8 @@ class MockHttpBackend implements HttpBackend { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ - _Chain expectPOST(url, [data, headers]) => expect('POST', url, data, headers); + _Chain expectPOST(url, [data, headers, withCredentials = false]) => expect('POST', url, data, + headers, withCredentials); /** * @ngdoc method @@ -457,7 +472,8 @@ class MockHttpBackend implements HttpBackend { * @returns {requestHandler} Returns an object with `respond` method that control how a matched * request is handled. */ - _Chain expectPATCH(url, [data, headers]) => expect('PATCH', url, data, headers); + _Chain expectPATCH(url, [data, headers, withCredentials = false]) => expect('PATCH', url, data, + headers, withCredentials); /** * @ngdoc method diff --git a/test/core_dom/http_spec.dart b/test/core_dom/http_spec.dart index b15f7a856..e31d75565 100644 --- a/test/core_dom/http_spec.dart +++ b/test/core_dom/http_spec.dart @@ -98,6 +98,15 @@ void main() { flush(); })); + describe('backend', () { + it('should pass on withCredentials to backend and use GET as default method', + async(() { + backend.expect('GET', '/url', null, null, true).respond(''); + http(url: '/url', method: 'GET', withCredentials: true); + flush(); + })); + }); + describe('params', () { it('should do basic request with params and encode', async(() { diff --git a/test/mock/http_backend_spec.dart b/test/mock/http_backend_spec.dart index 95c3339e7..e8842f2b3 100644 --- a/test/mock/http_backend_spec.dart +++ b/test/mock/http_backend_spec.dart @@ -43,6 +43,21 @@ void main() { expect(callback).toHaveBeenCalledOnce(); }); + it('should match when with credentials is set', () { + hb.when('GET', '/url1').respond(200, 'content', {}); + hb.when('GET', '/url1', null, null, true).respond(201, 'another', {}); + + callback.andCallFake((status, response, _) { + expect(status).toBe(201); + expect(response).toBe('another'); + }); + + hb('GET', '/url1', callback, withCredentials: true); + expect(callback).not.toHaveBeenCalled(); + hb.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + it('should respond with JSON', (Logger logger) { hb.when('GET', '/url1').respond(200, ['abc'], {});