'use strict'; // TODO(vojta): refactor these tests to use new inject() syntax describe('$http', function() { var $http, $browser, $exceptionHandler, // services method, url, data, headers, timeout, // passed arguments onSuccess, onError, // callback spies scope, errorLogs, respond, rawXhrObject, future; beforeEach(inject(function($injector) { $injector.get('$exceptionHandlerProvider').mode('log'); scope = $injector.get('$rootScope'); $http = $injector.get('$http'); $browser = $injector.get('$browser'); $exceptionHandler = $injector.get('$exceptionHandler'); // TODO(vojta): move this into mock browser ? respond = method = url = data = headers = null; rawXhrObject = { abort: jasmine.createSpy('request.abort'), getResponseHeader: function(h) {return h + '-val';}, getAllResponseHeaders: function() { return 'content-encoding: gzip\nserver: Apache\n'; } }; spyOn(scope, '$apply'); spyOn($browser, 'xhr').andCallFake(function(m, u, d, c, h, t) { method = m; url = u; data = d; respond = c; headers = h; timeout = t; return rawXhrObject; }); })); afterEach(function() { // expect($exceptionHandler.errors.length).toBe(0); }); function doCommonXhr(method, url) { future = $http({method: method || 'GET', url: url || '/url'}); onSuccess = jasmine.createSpy('on200'); onError = jasmine.createSpy('on400'); future.on('200', onSuccess); future.on('400', onError); return future; } it('should do basic request', function() { $http({url: '/url', method: 'GET'}); expect($browser.xhr).toHaveBeenCalledOnce(); expect(url).toBe('/url'); expect(method).toBe('GET'); }); it('should pass data if specified', function() { $http({url: '/url', method: 'POST', data: 'some-data'}); expect($browser.xhr).toHaveBeenCalledOnce(); expect(data).toBe('some-data'); }); it('should pass timeout if specified', function() { $http({url: '/url', method: 'POST', timeout: 5000}); expect($browser.xhr).toHaveBeenCalledOnce(); expect(timeout).toBe(5000); }); describe('callbacks', function() { beforeEach(doCommonXhr); it('should log exceptions', function() { onSuccess.andThrow('exception in success callback'); onError.andThrow('exception in error callback'); respond(200, 'content'); expect($exceptionHandler.errors.pop()).toContain('exception in success callback'); respond(400, ''); expect($exceptionHandler.errors.pop()).toContain('exception in error callback'); }); it('should log more exceptions', function() { onError.andThrow('exception in error callback'); future.on('500', onError).on('50x', onError); respond(500, ''); expect($exceptionHandler.errors.length).toBe(2); $exceptionHandler.errors = []; }); it('should get response as first param', function() { respond(200, 'response'); expect(onSuccess).toHaveBeenCalledOnce(); expect(onSuccess.mostRecentCall.args[0]).toBe('response'); respond(400, 'empty'); expect(onError).toHaveBeenCalledOnce(); expect(onError.mostRecentCall.args[0]).toBe('empty'); }); it('should get status code as second param', function() { respond(200, 'response'); expect(onSuccess).toHaveBeenCalledOnce(); expect(onSuccess.mostRecentCall.args[1]).toBe(200); respond(400, 'empty'); expect(onError).toHaveBeenCalledOnce(); expect(onError.mostRecentCall.args[1]).toBe(400); }); }); describe('response headers', function() { var callback; beforeEach(function() { callback = jasmine.createSpy('callback'); }); it('should return single header', function() { callback.andCallFake(function(r, s, header) { expect(header('date')).toBe('date-val'); }); $http({url: '/url', method: 'GET'}).on('200', callback); respond(200, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should return null when single header does not exist', function() { callback.andCallFake(function(r, s, header) { header(); // we need that to get headers parsed first expect(header('nothing')).toBe(null); }); $http({url: '/url', method: 'GET'}).on('200', callback); respond(200, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should return all headers as object', function() { callback.andCallFake(function(r, s, header) { expect(header()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'}); }); $http({url: '/url', method: 'GET'}).on('200', callback); respond(200, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should return empty object for jsonp request', function() { // jsonp doesn't return raw object rawXhrObject = undefined; callback.andCallFake(function(r, s, headers) { expect(headers()).toEqual({}); }); $http({url: '/some', method: 'JSONP'}).on('200', callback); respond(200, ''); expect(callback).toHaveBeenCalledOnce(); }); }); describe('response headers parser', function() { it('should parse basic', function() { var parsed = parseHeaders( 'date: Thu, 04 Aug 2011 20:23:08 GMT\n' + 'content-encoding: gzip\n' + 'transfer-encoding: chunked\n' + 'x-cache-info: not cacheable; response has already expired, not cacheable; response has already expired\n' + 'connection: Keep-Alive\n' + 'x-backend-server: pm-dekiwiki03\n' + 'pragma: no-cache\n' + 'server: Apache\n' + 'x-frame-options: DENY\n' + 'content-type: text/html; charset=utf-8\n' + 'vary: Cookie, Accept-Encoding\n' + 'keep-alive: timeout=5, max=1000\n' + 'expires: Thu: , 19 Nov 1981 08:52:00 GMT\n'); expect(parsed['date']).toBe('Thu, 04 Aug 2011 20:23:08 GMT'); expect(parsed['content-encoding']).toBe('gzip'); expect(parsed['transfer-encoding']).toBe('chunked'); expect(parsed['keep-alive']).toBe('timeout=5, max=1000'); }); it('should parse lines without space after colon', function() { expect(parseHeaders('key:value').key).toBe('value'); }); it('should trim the values', function() { expect(parseHeaders('key: value ').key).toBe('value'); }); it('should allow headers without value', function() { expect(parseHeaders('key:').key).toBe(''); }); it('should merge headers with same key', function() { expect(parseHeaders('key: a\nkey:b\n').key).toBe('a, b'); }); it('should normalize keys to lower case', function() { expect(parseHeaders('KeY: value').key).toBe('value'); }); it('should parse CRLF as delimiter', function() { // IE does use CRLF expect(parseHeaders('a: b\r\nc: d\r\n')).toEqual({a: 'b', c: 'd'}); expect(parseHeaders('a: b\r\nc: d\r\n').a).toBe('b'); }); it('should parse tab after semi-colon', function() { expect(parseHeaders('a:\tbb').a).toBe('bb'); expect(parseHeaders('a: \tbb').a).toBe('bb'); }); }); describe('request headers', function() { it('should send custom headers', function() { $http({url: '/url', method: 'GET', headers: { 'Custom': 'header', 'Content-Type': 'application/json' }}); expect(headers['Custom']).toEqual('header'); expect(headers['Content-Type']).toEqual('application/json'); }); it('should set default headers for GET request', function() { $http({url: '/url', method: 'GET', headers: {}}); expect(headers['Accept']).toBe('application/json, text/plain, */*'); expect(headers['X-Requested-With']).toBe('XMLHttpRequest'); }); it('should set default headers for POST request', function() { $http({url: '/url', method: 'POST', headers: {}}); expect(headers['Accept']).toBe('application/json, text/plain, */*'); expect(headers['X-Requested-With']).toBe('XMLHttpRequest'); expect(headers['Content-Type']).toBe('application/json'); }); it('should set default headers for PUT request', function() { $http({url: '/url', method: 'PUT', headers: {}}); expect(headers['Accept']).toBe('application/json, text/plain, */*'); expect(headers['X-Requested-With']).toBe('XMLHttpRequest'); expect(headers['Content-Type']).toBe('application/json'); }); it('should set default headers for custom HTTP method', function() { $http({url: '/url', method: 'FOO', headers: {}}); expect(headers['Accept']).toBe('application/json, text/plain, */*'); expect(headers['X-Requested-With']).toBe('XMLHttpRequest'); }); it('should override default headers with custom', function() { $http({url: '/url', method: 'POST', headers: { 'Accept': 'Rewritten', 'Content-Type': 'Rewritten' }}); expect(headers['Accept']).toBe('Rewritten'); expect(headers['X-Requested-With']).toBe('XMLHttpRequest'); expect(headers['Content-Type']).toBe('Rewritten'); }); it('should set the XSRF cookie into a XSRF header', function() { $browser.cookies('XSRF-TOKEN', 'secret'); $http({url: '/url', method: 'GET'}); expect(headers['X-XSRF-TOKEN']).toBe('secret'); $http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}}); expect(headers['X-XSRF-TOKEN']).toBe('secret'); $http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}}); expect(headers['X-XSRF-TOKEN']).toBe('secret'); $http({url: '/url', method: 'DELETE', headers: {}}); expect(headers['X-XSRF-TOKEN']).toBe('secret'); }); }); describe('short methods', function() { it('should have .get()', function() { $http.get('/url'); expect(method).toBe('GET'); expect(url).toBe('/url'); }); it('.get() should allow config param', function() { $http.get('/url', {headers: {'Custom': 'Header'}}); expect(method).toBe('GET'); expect(url).toBe('/url'); expect(headers['Custom']).toBe('Header'); }); it('should have .delete()', function() { $http['delete']('/url'); expect(method).toBe('DELETE'); expect(url).toBe('/url'); }); it('.delete() should allow config param', function() { $http['delete']('/url', {headers: {'Custom': 'Header'}}); expect(method).toBe('DELETE'); expect(url).toBe('/url'); expect(headers['Custom']).toBe('Header'); }); it('should have .head()', function() { $http.head('/url'); expect(method).toBe('HEAD'); expect(url).toBe('/url'); }); it('.head() should allow config param', function() { $http.head('/url', {headers: {'Custom': 'Header'}}); expect(method).toBe('HEAD'); expect(url).toBe('/url'); expect(headers['Custom']).toBe('Header'); }); it('should have .patch()', function() { $http.patch('/url'); expect(method).toBe('PATCH'); expect(url).toBe('/url'); }); it('.patch() should allow config param', function() { $http.patch('/url', {headers: {'Custom': 'Header'}}); expect(method).toBe('PATCH'); expect(url).toBe('/url'); expect(headers['Custom']).toBe('Header'); }); it('should have .post()', function() { $http.post('/url', 'some-data'); expect(method).toBe('POST'); expect(url).toBe('/url'); expect(data).toBe('some-data'); }); it('.post() should allow config param', function() { $http.post('/url', 'some-data', {headers: {'Custom': 'Header'}}); expect(method).toBe('POST'); expect(url).toBe('/url'); expect(data).toBe('some-data'); expect(headers['Custom']).toBe('Header'); }); it('should have .put()', function() { $http.put('/url', 'some-data'); expect(method).toBe('PUT'); expect(url).toBe('/url'); expect(data).toBe('some-data'); }); it('.put() should allow config param', function() { $http.put('/url', 'some-data', {headers: {'Custom': 'Header'}}); expect(method).toBe('PUT'); expect(url).toBe('/url'); expect(data).toBe('some-data'); expect(headers['Custom']).toBe('Header'); }); it('should have .jsonp()', function() { $http.jsonp('/url'); expect(method).toBe('JSONP'); expect(url).toBe('/url'); }); it('.jsonp() should allow config param', function() { $http.jsonp('/url', {headers: {'Custom': 'Header'}}); expect(method).toBe('JSONP'); expect(url).toBe('/url'); expect(headers['Custom']).toBe('Header'); }); }); describe('future', function() { describe('abort', function() { beforeEach(doCommonXhr); it('should return itself to allow chaining', function() { expect(future.abort()).toBe(future); }); it('should allow aborting the request', function() { future.abort(); expect(rawXhrObject.abort).toHaveBeenCalledOnce(); }); it('should not abort already finished request', function() { respond(200, 'content'); future.abort(); expect(rawXhrObject.abort).not.toHaveBeenCalled(); }); }); describe('retry', function() { it('should retry last request with same callbacks', function() { doCommonXhr('HEAD', '/url-x'); respond(200, ''); $browser.xhr.reset(); onSuccess.reset(); future.retry(); expect($browser.xhr).toHaveBeenCalledOnce(); expect(method).toBe('HEAD'); expect(url).toBe('/url-x'); respond(200, 'body'); expect(onSuccess).toHaveBeenCalledOnce(); }); it('should return itself to allow chaining', function() { doCommonXhr(); respond(200, ''); expect(future.retry()).toBe(future); }); it('should throw error when pending request', function() { doCommonXhr(); expect(future.retry).toThrow('Can not retry request. Abort pending request first.'); }); }); describe('on', function() { var callback; beforeEach(function() { future = $http({method: 'GET', url: '/url'}); callback = jasmine.createSpy('callback'); }); it('should return itself to allow chaining', function() { expect(future.on('200', noop)).toBe(future); }); it('should call exact status code callback', function() { future.on('205', callback); respond(205, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should match 2xx', function() { future.on('2xx', callback); respond(200, ''); respond(201, ''); respond(266, ''); respond(400, ''); respond(300, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(3); }); it('should match 20x', function() { future.on('20x', callback); respond(200, ''); respond(201, ''); respond(205, ''); respond(400, ''); respond(300, ''); respond(210, ''); respond(255, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(3); }); it('should match 2x1', function() { future.on('2x1', callback); respond(201, ''); respond(211, ''); respond(251, ''); respond(400, ''); respond(300, ''); respond(210, ''); respond(255, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(3); }); it('should match xxx', function() { future.on('xxx', callback); respond(201, ''); respond(211, ''); respond(251, ''); respond(404, ''); respond(501, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(5); }); it('should call all matched callbacks', function() { var no = jasmine.createSpy('wrong'); future.on('xxx', callback); future.on('2xx', callback); future.on('205', callback); future.on('3xx', no); future.on('2x1', no); future.on('4xx', no); respond(205, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(3); expect(no).not.toHaveBeenCalled(); }); it('should allow list of status patterns', function() { future.on('2xx,3xx', callback); respond(405, ''); expect(callback).not.toHaveBeenCalled(); respond(201); expect(callback).toHaveBeenCalledOnce(); respond(301); expect(callback.callCount).toBe(2); }); it('should preserve the order of listeners', function() { var log = ''; future.on('2xx', function() {log += '1';}); future.on('201', function() {log += '2';}); future.on('2xx', function() {log += '3';}); respond(201); expect(log).toBe('123'); }); it('should know "success" alias', function() { future.on('success', callback); respond(200, ''); expect(callback).toHaveBeenCalledOnce(); callback.reset(); respond(201, ''); expect(callback).toHaveBeenCalledOnce(); callback.reset(); respond(250, ''); expect(callback).toHaveBeenCalledOnce(); callback.reset(); respond(404, ''); respond(501, ''); expect(callback).not.toHaveBeenCalled(); }); it('should know "error" alias', function() { future.on('error', callback); respond(401, ''); expect(callback).toHaveBeenCalledOnce(); callback.reset(); respond(500, ''); expect(callback).toHaveBeenCalledOnce(); callback.reset(); respond(0, ''); expect(callback).toHaveBeenCalledOnce(); callback.reset(); respond(201, ''); respond(200, ''); respond(300, ''); expect(callback).not.toHaveBeenCalled(); }); it('should know "always" alias', function() { future.on('always', callback); respond(201, ''); respond(200, ''); respond(300, ''); respond(401, ''); respond(502, ''); respond(0, ''); respond(-1, ''); respond(-2, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(8); }); it('should call "xxx" when 0 status code', function() { future.on('xxx', callback); respond(0, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should not call "2xx" when 0 status code', function() { future.on('2xx', callback); respond(0, ''); expect(callback).not.toHaveBeenCalled(); }); it('should normalize internal statuses -1, -2 to 0', function() { callback.andCallFake(function(response, status) { expect(status).toBe(0); }); future.on('xxx', callback); respond(-1, ''); respond(-2, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(2); }); it('should match "timeout" when -1 internal status', function() { future.on('timeout', callback); respond(-1, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should match "abort" when 0 status', function() { future.on('abort', callback); respond(0, ''); expect(callback).toHaveBeenCalledOnce(); }); it('should match "error" when 0, -1, or -2', function() { future.on('error', callback); respond(0, ''); respond(-1, ''); respond(-2, ''); expect(callback).toHaveBeenCalled(); expect(callback.callCount).toBe(3); }); }); }); describe('scope.$apply', function() { beforeEach(doCommonXhr); it('should $apply after success callback', function() { respond(200, ''); expect(scope.$apply).toHaveBeenCalledOnce(); }); it('should $apply after error callback', function() { respond(404, ''); expect(scope.$apply).toHaveBeenCalledOnce(); }); it('should $apply even if exception thrown during callback', function() { onSuccess.andThrow('error in callback'); onError.andThrow('error in callback'); respond(200, ''); expect(scope.$apply).toHaveBeenCalledOnce(); scope.$apply.reset(); respond(400, ''); expect(scope.$apply).toHaveBeenCalledOnce(); $exceptionHandler.errors = []; }); }); describe('transform', function() { describe('request', function() { describe('default', function() { it('should transform object into json', function() { $http({method: 'POST', url: '/url', data: {one: 'two'}}); expect(data).toBe('{"one":"two"}'); }); it('should ignore strings', function() { $http({method: 'POST', url: '/url', data: 'string-data'}); expect(data).toBe('string-data'); }); }); }); describe('response', function() { describe('default', function() { it('should deserialize json objects', function() { doCommonXhr(); respond(200, '{"foo":"bar","baz":23}'); expect(onSuccess.mostRecentCall.args[0]).toEqual({foo: 'bar', baz: 23}); }); it('should deserialize json arrays', function() { doCommonXhr(); respond(200, '[1, "abc", {"foo":"bar"}]'); expect(onSuccess.mostRecentCall.args[0]).toEqual([1, 'abc', {foo: 'bar'}]); }); it('should deserialize json with security prefix', function() { doCommonXhr(); respond(200, ')]}\',\n[1, "abc", {"foo":"bar"}]'); expect(onSuccess.mostRecentCall.args[0]).toEqual([1, 'abc', {foo:'bar'}]); }); }); it('should pipeline more functions', function() { function first(d) {return d + '1';} function second(d) {return d + '2';} onSuccess = jasmine.createSpy('onSuccess'); $http({method: 'POST', url: '/url', data: '0', transformResponse: [first, second]}) .on('200', onSuccess); respond(200, '0'); expect(onSuccess).toHaveBeenCalledOnce(); expect(onSuccess.mostRecentCall.args[0]).toBe('012'); }); }); }); describe('cache', function() { function doFirstCacheRequest(method, responseStatus) { onSuccess = jasmine.createSpy('on200'); $http({method: method || 'get', url: '/url', cache: true}); respond(responseStatus || 200, 'content'); $browser.xhr.reset(); } it('should cache GET request', function() { doFirstCacheRequest(); $http({method: 'get', url: '/url', cache: true}).on('200', onSuccess); $browser.defer.flush(); expect(onSuccess).toHaveBeenCalledOnce(); expect(onSuccess.mostRecentCall.args[0]).toBe('content'); expect($browser.xhr).not.toHaveBeenCalled(); }); it('should always call callback asynchronously', function() { doFirstCacheRequest(); $http({method: 'get', url: '/url', cache: true}).on('200', onSuccess); expect(onSuccess).not.toHaveBeenCalled(); }); it('should not cache POST request', function() { doFirstCacheRequest('post'); $http({method: 'post', url: '/url', cache: true}).on('200', onSuccess); $browser.defer.flush(); expect(onSuccess).not.toHaveBeenCalled(); expect($browser.xhr).toHaveBeenCalledOnce(); }); it('should not cache PUT request', function() { doFirstCacheRequest('put'); $http({method: 'put', url: '/url', cache: true}).on('200', onSuccess); $browser.defer.flush(); expect(onSuccess).not.toHaveBeenCalled(); expect($browser.xhr).toHaveBeenCalledOnce(); }); it('should not cache DELETE request', function() { doFirstCacheRequest('delete'); $http({method: 'delete', url: '/url', cache: true}).on('200', onSuccess); $browser.defer.flush(); expect(onSuccess).not.toHaveBeenCalled(); expect($browser.xhr).toHaveBeenCalledOnce(); }); it('should not cache non 2xx responses', function() { doFirstCacheRequest('get', 404); $http({method: 'get', url: '/url', cache: true}).on('200', onSuccess); $browser.defer.flush(); expect(onSuccess).not.toHaveBeenCalled(); expect($browser.xhr).toHaveBeenCalledOnce(); }); it('should cache the headers as well', function() { doFirstCacheRequest(); onSuccess.andCallFake(function(r, s, headers) { expect(headers()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'}); expect(headers('server')).toBe('Apache'); }); $http({method: 'get', url: '/url', cache: true}).on('200', onSuccess); $browser.defer.flush(); expect(onSuccess).toHaveBeenCalledOnce(); }); it('should cache status code as well', function() { doFirstCacheRequest('get', 201); onSuccess.andCallFake(function(r, status, h) { expect(status).toBe(201); }); $http({method: 'get', url: '/url', cache: true}).on('2xx', onSuccess); $browser.defer.flush(); expect(onSuccess).toHaveBeenCalledOnce(); }); }); describe('pendingCount', function() { it('should return number of pending requests', function() { expect($http.pendingCount()).toBe(0); $http({method: 'get', url: '/some'}); expect($http.pendingCount()).toBe(1); respond(200, ''); expect($http.pendingCount()).toBe(0); }); it('should decrement the counter when request aborted', function() { future = $http({method: 'get', url: '/x'}); expect($http.pendingCount()).toBe(1); future.abort(); respond(0, ''); expect($http.pendingCount()).toBe(0); }); it('should decrement the counter when served from cache', function() { $http({method: 'get', url: '/cached', cache: true}); respond(200, 'content'); expect($http.pendingCount()).toBe(0); $http({method: 'get', url: '/cached', cache: true}); expect($http.pendingCount()).toBe(1); $browser.defer.flush(); expect($http.pendingCount()).toBe(0); }); it('should decrement the counter before firing callbacks', function() { $http({method: 'get', url: '/cached'}).on('xxx', function() { expect($http.pendingCount()).toBe(0); }); expect($http.pendingCount()).toBe(1); respond(200, 'content'); }); }); });