From 6d0acf591664a15ddf35b083b9beec63fff0f30b Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 21 Apr 2014 16:14:28 -0400 Subject: [PATCH] Initial XhrHttpImpl implementation --- src/dapi/http/XhrHttpImpl.js | 145 ++++++++++++++++++++ test/dapi/http/HttpDataApiTest.js | 2 +- test/dapi/http/XhrHttpImplTest.js | 219 ++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 src/dapi/http/XhrHttpImpl.js create mode 100644 test/dapi/http/XhrHttpImplTest.js diff --git a/src/dapi/http/XhrHttpImpl.js b/src/dapi/http/XhrHttpImpl.js new file mode 100644 index 0000000..ac9c6c7 --- /dev/null +++ b/src/dapi/http/XhrHttpImpl.js @@ -0,0 +1,145 @@ +/** + * XMLHttpRequest HTTP protocol implementation + * + * Copyright (C) 2014 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Class = require( 'easejs' ).Class, + HttpImpl = require( './HttpImpl' ); + + +/** + * An HTTP implementation using the standardized XMLHttpRequest prototype. + */ +module.exports = Class( 'XhrHttpImpl' ) + .implement( HttpImpl ) + .extend( +{ + /** + * XMLHttpRequest constructor + * @type {XMLHttpRequest} + * @constructor + */ + 'private _Xhr': null, + + + /** + * Initializes with constructor to the object through which XHRs will be + * made + * + * @param {Object} XMLHttpRequest ctor to object to perform requests + */ + __construct: function( XMLHttpRequest ) + { + this._Xhr = XMLHttpRequest; + }, + + + /** + * Perform HTTP request using the standard XMLHttpRequest + * + * @param {Object|string} data request params + * @param {function(Error, Object)} callback server response callback + * + * @return {HttpImpl} self + */ + 'public requestData': function( url, method, data, callback ) + { + var req = new this._Xhr(); + + try + { + this.openRequest( req, url, method ); + this.onLoad( req, function( err, resp ) + { + callback( err, resp ); + } ); + } + catch ( e ) + { + callback( e, null ); + } + + return this; + }, + + + /** + * Prepares a request to the given URL using the given HTTP method + * + * This method may be overridden by subtypes to set authentication data, + * modify headers, hook XHR callbacks, etc. + * + * Subtypes may throw exceptions; the caller of this method catches and + * properly forwards them to the callback. + * + * This method must be synchronous. + * + * @param {XMLHttpRequest} req request to prepare + * @param {string} url destination URL + * @param {string} method HTTP method + * + * @return {undefined} + */ + 'virtual protected openRequest': function( req, url, method ) + { + // alway async + req.open( method, url, true ); + }, + + + /** + * Hooks ready state change to handle data + * + * Subtypes may override this method to alter the ready state change + * actions taken (e.g. to display progress, handle errors, etc.) + * + * @param {XMLHttpRequest} req request to hook + * @param {function(string)} callback continuation to invoke with response + * + * @return {undefined} + * + * @throws {Error} if non-200 response received from server + */ + 'virtual protected onLoad': function( req, callback ) + { + req.onreadystatechange = function() + { + // ready state of 4 (DONE) indicates that the request is complete + if ( req.readyState !== 4 ) + { + return; + } + else if ( req.status !== 200 ) + { + callback( + Error( req.status + " error from server" ), + { + status: req.status, + data: req.responseText + } + ); + return; + } + + // successful + callback( null, req.responseText ); + }; + } +} ); + diff --git a/test/dapi/http/HttpDataApiTest.js b/test/dapi/http/HttpDataApiTest.js index eeeb7d6..748912e 100644 --- a/test/dapi/http/HttpDataApiTest.js +++ b/test/dapi/http/HttpDataApiTest.js @@ -27,7 +27,7 @@ var dapi = require( '../../../' ).dapi, dummy_url = 'http://foo', dummy_impl = Class .implement( dapi.http.HttpImpl ) - .extend( { requestData: function( _, __, ___, ____ ) {} } ), + .extend( { requestData: function( _, __, ___, ____ ) {} } )(), dummy_sut = Sut( dummy_url, 'GET', dummy_impl ); diff --git a/test/dapi/http/XhrHttpImplTest.js b/test/dapi/http/XhrHttpImplTest.js new file mode 100644 index 0000000..7e3efc6 --- /dev/null +++ b/test/dapi/http/XhrHttpImplTest.js @@ -0,0 +1,219 @@ +/** + * Test case for XMLHttpRequest HTTP protocol implementation + * + * Copyright (C) 2014 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var dapi = require( '../../../' ).dapi, + expect = require( 'chai' ).expect, + Class = require( 'easejs' ).Class, + HttpImpl = dapi.http.HttpImpl, + Sut = dapi.http.XhrHttpImpl, + + DummyXhr = function() + { + this.open = function() + { + DummyXhr.args = arguments; + }; + }; + + +describe( 'XhrHttpImpl', function() +{ + /** + * Since ECMAScript does not have return typing, we won't know if the ctor + * actually returns an XMLHttpRequest until we try. + */ + it( 'will accept any constructor', function() + { + expect( function() + { + Sut( function() {} ); + } ).to.not.throw( Error ); + } ); + + + it( 'is an HttpImpl', function() + { + var sut = Sut( function() {} ); + expect( Class.isA( HttpImpl, sut ) ).to.be.ok; + } ); + + + describe( '.requestData', function() + { + it( 'requests a connection using the given url and method', function() + { + var url = 'http://foonugget', + method = 'GET', + sut = Sut( DummyXhr ); + + sut.requestData( url, method, {}, function() {} ); + + var args = DummyXhr.args; + expect( args[ 0 ] ).to.equal( method ); + expect( args[ 1 ] ).to.equal( url ); + expect( args[ 1 ] ).to.be.ok; // async + } ); + + + /** + * Since the request is asynchronous, we should be polite and not return + * errors in two different formats; we will catch it and instead pass it + * back via the callback. + */ + it( 'returns XHR open() errors via callback', function( done ) + { + var e = Error( "Test error" ), + Xhr = function() + { + this.open = function() + { + throw e; + }; + }; + + // should not throw an exception + expect( function() + { + // should instead provide to callback + Sut( Xhr ) + .requestData( 'http://foo', 'GET', {}, function( err, data ) + { + expect( err ).to.equal( e ); + expect( data ).to.equal( null ); + done(); + } ); + } ).to.not.throw( Error ); + } ); + + + it( 'returns XHR response via callback when no error', function( done ) + { + var retdata = "foobar", + src = "moocow", + StubXhr = createStubXhr(); + + StubXhr.prototype.responseText = retdata; + StubXhr.prototype.readyState = 4; // done + StubXhr.prototype.status = 200; // OK + + StubXhr.prototype.send = function( data ) + { + expect( data ).is.equal( src ); + StubXhr.inst.onreadystatechange(); + }; + + Sut( StubXhr ) + .requestData( 'http://bar', 'GET', src, function( err, resp ) + { + expect( err ).to.equal( null ); + expect( resp ).to.equal( retdata ); + done(); + } ); + + // invoke callback + StubXhr.inst.send( src ); + } ); + + + describe( 'if return status code is not 200', function() + { + /** + * This is the default behavior, but can be changed by overriding + * the onLoad method. + */ + it( 'returns an error to the callback', function( done ) + { + var StubXhr = createStubXhr(); + StubXhr.prototype.status = 404; + + Sut( StubXhr ) + .requestData( 'http://foo', 'GET', '', function( err, _ ) + { + expect( err ).to.be.instanceOf( Error ); + expect( err.message ).to.contain( + StubXhr.prototype.status + ); + + done(); + } ); + + StubXhr.inst.send( '' ); + } ); + + + it( 'returns response text with error code', function( done ) + { + var StubXhr = createStubXhr(), + status = 404, + reply = 'foobunny'; + + StubXhr.prototype.status = status; + StubXhr.prototype.responseText = reply; + + Sut( StubXhr ) + .requestData( 'http://foo', 'GET', '', function( _, resp ) + { + expect( resp.status ).to.equal( status ); + expect( resp.data ).to.equal( reply ); + done(); + } ); + + StubXhr.inst.send( '' ); + } ); + } ); + + + it( 'returns self', function() + { + var sut = Sut( function() {} ), + ret = sut.requestData( + 'http://foo', 'GET', {}, function() {} + ); + + expect( ret ).to.equal( sut ); + } ); + } ); +} ); + + +function createStubXhr() +{ + var StubXhr = function() + { + StubXhr.inst = this; + }; + + StubXhr.prototype = { + onreadystatechange: null, + responseText: '', + readyState: 4, // don, + status: 200, // O, + + open: function() {}, + send: function( data ) + { + this.onreadystatechange(); + } + }; + + return StubXhr; +} +