├── test ├── fixtures │ ├── bad_food │ │ ├── broken_xml.xml │ │ ├── unknown_tags.xml │ │ ├── just_params.xml │ │ ├── illegal_i4_response.xml │ │ ├── illegal_int_response.xml │ │ ├── illegal_double_response.xml │ │ ├── illegal_i8_response.xml │ │ ├── illegal_boolean_response.xml │ │ └── illegal_datetime_response.xml │ └── good_food │ │ ├── fault_empty.xml │ │ ├── undefined_response.xml │ │ ├── unspecified_type_response.xml │ │ ├── fault_explicit_empty.xml │ │ ├── array_empty_response.xml │ │ ├── i4_zero_response.xml │ │ ├── i8_zero_response.xml │ │ ├── i4_negative_response.xml │ │ ├── i4_positive_response.xml │ │ ├── int_positive_response.xml │ │ ├── int_zero_response.xml │ │ ├── string_empty_response.xml │ │ ├── undefined_call.xml │ │ ├── int_negative_response.xml │ │ ├── base64_response.xml │ │ ├── boolean_false_response.xml │ │ ├── boolean_true_response.xml │ │ ├── nil_call.xml │ │ ├── string_emoji_response.xml │ │ ├── string_response.xml │ │ ├── double_negative_response.xml │ │ ├── i8_positive_response.xml │ │ ├── double_positive_response.xml │ │ ├── i8_negative_response.xml │ │ ├── int_zero_call.xml │ │ ├── string_empty_call.xml │ │ ├── customtype_response.xml │ │ ├── int_negative_call.xml │ │ ├── int_positive_call.xml │ │ ├── string_emoji.xml │ │ ├── boolean_true_call.xml │ │ ├── base64_call.xml │ │ ├── boolean_false_call.xml │ │ ├── double_positive_call.xml │ │ ├── string_call.xml │ │ ├── string_cdata_response.xml │ │ ├── datetime_response.xml │ │ ├── double_negative_call.xml │ │ ├── encoded_call.xml │ │ ├── customtype_call.xml │ │ ├── struct_implicit_string_response.xml │ │ ├── customtype_extended_response.xml │ │ ├── datetime_call.xml │ │ ├── customtype_extended_call.xml │ │ ├── string_cdata_call.xml │ │ ├── struct_response.xml │ │ ├── array_response.xml │ │ ├── struct_with_whitespace_response.xml │ │ ├── array_call.xml │ │ ├── fault.xml │ │ ├── string_multiline_cdata_call.xml │ │ ├── struct_call.xml │ │ ├── struct_empty_property_call.xml │ │ ├── array_nested_response.xml │ │ ├── struct_nested_call.xml │ │ ├── array_nested_with_trailing_values_response.xml │ │ ├── struct_nested_response.xml │ │ ├── grinder.xml │ │ ├── very_large_response_results.json │ │ └── very_large_response.xml ├── server_test.js ├── cookies_test.js ├── date_formatter.js ├── deserializer_test.js ├── client_test.js └── serializer_test.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── lib ├── customtype.js ├── xmlrpc.js ├── server.js ├── cookies.js ├── serializer.js ├── date_formatter.js ├── client.js └── deserializer.js ├── package.json ├── LICENSE ├── HISTORY.md ├── README.md └── example └── client_server.js /test/fixtures/bad_food/broken_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "4" 6 | - "6" 7 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/just_params.xml: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/fault_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/illegal_i4_response.xml: -------------------------------------------------------------------------------- 1 | four 2 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/illegal_int_response.xml: -------------------------------------------------------------------------------- 1 | four 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/undefined_response.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/unspecified_type_response.xml: -------------------------------------------------------------------------------- 1 | testString 2 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/illegal_double_response.xml: -------------------------------------------------------------------------------- 1 | i 2 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/illegal_i8_response.xml: -------------------------------------------------------------------------------- 1 | gazillion 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/fault_explicit_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/array_empty_response.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/i4_zero_response.xml: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/i8_zero_response.xml: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/illegal_boolean_response.xml: -------------------------------------------------------------------------------- 1 | not a boolean 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/i4_negative_response.xml: -------------------------------------------------------------------------------- 1 | -4 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/i4_positive_response.xml: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/int_positive_response.xml: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/int_zero_response.xml: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_empty_response.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/undefined_call.xml: -------------------------------------------------------------------------------- 1 | testMethod 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/int_negative_response.xml: -------------------------------------------------------------------------------- 1 | -4 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/base64_response.xml: -------------------------------------------------------------------------------- 1 | dGVzdGluZw== 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/boolean_false_response.xml: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/boolean_true_response.xml: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/nil_call.xml: -------------------------------------------------------------------------------- 1 | testMethod 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_emoji_response.xml: -------------------------------------------------------------------------------- 1 | 😁 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_response.xml: -------------------------------------------------------------------------------- 1 | testString 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/double_negative_response.xml: -------------------------------------------------------------------------------- 1 | -1.41421 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/i8_positive_response.xml: -------------------------------------------------------------------------------- 1 | 4611686018427387904 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/double_positive_response.xml: -------------------------------------------------------------------------------- 1 | 3.141592654 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/i8_negative_response.xml: -------------------------------------------------------------------------------- 1 | -4611686018427387904 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/int_zero_call.xml: -------------------------------------------------------------------------------- 1 | testMethod0 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_empty_call.xml: -------------------------------------------------------------------------------- 1 | testMethod 2 | -------------------------------------------------------------------------------- /test/fixtures/bad_food/illegal_datetime_response.xml: -------------------------------------------------------------------------------- 1 | illegal datetime 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/customtype_response.xml: -------------------------------------------------------------------------------- 1 | testCustomType 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/int_negative_call.xml: -------------------------------------------------------------------------------- 1 | testMethod-32 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/int_positive_call.xml: -------------------------------------------------------------------------------- 1 | testMethod17 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_emoji.xml: -------------------------------------------------------------------------------- 1 | testMethod😁 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/boolean_true_call.xml: -------------------------------------------------------------------------------- 1 | testMethod1 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/base64_call.xml: -------------------------------------------------------------------------------- 1 | testMethoddGVzdGluZw== 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/boolean_false_call.xml: -------------------------------------------------------------------------------- 1 | testMethod0 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/double_positive_call.xml: -------------------------------------------------------------------------------- 1 | testMethod17.5 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_call.xml: -------------------------------------------------------------------------------- 1 | testMethodtestString 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_cdata_response.xml: -------------------------------------------------------------------------------- 1 | ]]> 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/datetime_response.xml: -------------------------------------------------------------------------------- 1 | 20120608T11:35:10 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/double_negative_call.xml: -------------------------------------------------------------------------------- 1 | testMethod-32.7777 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/encoded_call.xml: -------------------------------------------------------------------------------- 1 | testMethodFoo -------------------------------------------------------------------------------- /test/fixtures/good_food/customtype_call.xml: -------------------------------------------------------------------------------- 1 | testMethodtestCustomType 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_implicit_string_response.xml: -------------------------------------------------------------------------------- 1 | the-NametestValue 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/customtype_extended_response.xml: -------------------------------------------------------------------------------- 1 | extendedTestCustomType 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/datetime_call.xml: -------------------------------------------------------------------------------- 1 | testMethod20120607T11:35:10 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/customtype_extended_call.xml: -------------------------------------------------------------------------------- 1 | testMethodextendedTestCustomType 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_cdata_call.xml: -------------------------------------------------------------------------------- 1 | testCDATAMethodCongrats]]> 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_response.xml: -------------------------------------------------------------------------------- 1 | the-NametestValue 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/array_response.xml: -------------------------------------------------------------------------------- 1 | 178testString 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_with_whitespace_response.xml: -------------------------------------------------------------------------------- 1 | the-Name 2 | testValue 3 | -------------------------------------------------------------------------------- /test/fixtures/good_food/array_call.xml: -------------------------------------------------------------------------------- 1 | testMethodstring13 2 | -------------------------------------------------------------------------------- /lib/customtype.js: -------------------------------------------------------------------------------- 1 | var CustomType = module.exports = function(raw) { 2 | this.raw = raw 3 | } 4 | 5 | CustomType.prototype.serialize = function(xml) { 6 | return xml.ele(this.tagName).txt(this.raw) 7 | } 8 | 9 | CustomType.prototype.tagName = 'customType' 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/good_food/fault.xml: -------------------------------------------------------------------------------- 1 | faultCode4faultStringToo many parameters. 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/string_multiline_cdata_call.xml: -------------------------------------------------------------------------------- 1 | testCDATAMethod 2 | Go testing! 3 | Congrats 4 | ]]> 5 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_call.xml: -------------------------------------------------------------------------------- 1 | testMethodstringNamestring1intName3 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_empty_property_call.xml: -------------------------------------------------------------------------------- 1 | testMethodstringNameintName3 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/array_nested_response.xml: -------------------------------------------------------------------------------- 1 | 178testLevel1StringtestString64 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_nested_call.xml: -------------------------------------------------------------------------------- 1 | testMethodstringNamestring1objectNameintName4 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/array_nested_with_trailing_values_response.xml: -------------------------------------------------------------------------------- 1 | 178testLevel1StringtestString64testLevel1StringAfter 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/struct_nested_response.xml: -------------------------------------------------------------------------------- 1 | theNametestValueanotherNamenestedNamenestedValuelastNameSmith 2 | -------------------------------------------------------------------------------- /test/fixtures/good_food/grinder.xml: -------------------------------------------------------------------------------- 1 | theNametestValueanotherNamenestedNamenestedValuelastNameSmithyetAnotherName1999.26moreNested 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "xmlrpc" 2 | , "description" : "A pure JavaScript XML-RPC client and server." 3 | , "keywords" : [ "xml-rpc", "xmlrpc", "xml", "rpc" ] 4 | , "version" : "1.3.2" 5 | , "preferGlobal" : false 6 | , "homepage" : "https://github.com/baalexander/node-xmlrpc" 7 | , "author" : "Brandon Alexander (https://github.com/baalexander)" 8 | , "repository" : { 9 | "type" : "git" 10 | , "url" : "git://github.com/baalexander/node-xmlrpc.git" 11 | } 12 | , "bugs" : { 13 | "url" : "https://github.com/baalexander/node-xmlrpc/issues" 14 | } 15 | , "directories" : { 16 | "lib" : "./lib" 17 | } 18 | , "main" : "./lib/xmlrpc.js" 19 | , "dependencies" : { 20 | "sax" : "1.2.x" 21 | , "xmlbuilder" : "8.2.x" 22 | } 23 | , "devDependencies" : { 24 | "vows" : "0.7.x" 25 | } 26 | , "scripts" : { 27 | "test" : "vows 'test/*.js'" 28 | } 29 | , "engines" : { 30 | "node" : ">=0.8", 31 | "npm" : ">=1.0.0" 32 | } 33 | , "license" : "MIT" 34 | } 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2016 Brandon Ace Alexander 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /lib/xmlrpc.js: -------------------------------------------------------------------------------- 1 | var Client = require('./client') 2 | , Server = require('./server') 3 | , CustomType = require('./customtype') 4 | , dateFormatter = require('./date_formatter') 5 | 6 | var xmlrpc = exports 7 | 8 | /** 9 | * Creates an XML-RPC client. 10 | * 11 | * @param {Object} options - server options to make the HTTP request to 12 | * - {String} host 13 | * - {Number} port 14 | * - {String} url 15 | * - {Boolean} cookies 16 | * @return {Client} 17 | * @see Client 18 | */ 19 | xmlrpc.createClient = function(options) { 20 | return new Client(options, false) 21 | } 22 | 23 | /** 24 | * Creates an XML-RPC client that makes calls using HTTPS. 25 | * 26 | * @param {Object} options - server options to make the HTTP request to 27 | * - {String} host 28 | * - {Number} port 29 | * - {String} url 30 | * - {Boolean} cookies 31 | * @return {Client} 32 | * @see Client 33 | */ 34 | xmlrpc.createSecureClient = function(options) { 35 | return new Client(options, true) 36 | } 37 | 38 | /** 39 | * Creates an XML-RPC server. 40 | * 41 | * @param {Object}options - the HTTP server options 42 | * - {String} host 43 | * - {Number} port 44 | * @return {Server} 45 | * @see Server 46 | */ 47 | xmlrpc.createServer = function(options, callback) { 48 | return new Server(options, false, callback) 49 | } 50 | 51 | /** 52 | * Creates an XML-RPC server that uses HTTPS. 53 | * 54 | * @param {Object}options - the HTTP server options 55 | * - {String} host 56 | * - {Number} port 57 | * @return {Server} 58 | * @see Server 59 | */ 60 | xmlrpc.createSecureServer = function(options, callback) { 61 | return new Server(options, true, callback) 62 | } 63 | 64 | xmlrpc.CustomType = CustomType 65 | xmlrpc.dateFormatter = dateFormatter 66 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , https = require('https') 3 | , url = require('url') 4 | , EventEmitter = require('events').EventEmitter 5 | , Serializer = require('./serializer') 6 | , Deserializer = require('./deserializer') 7 | 8 | /** 9 | * Creates a new Server object. Also creates an HTTP server to start listening 10 | * for XML-RPC method calls. Will emit an event with the XML-RPC call's method 11 | * name when receiving a method call. 12 | * 13 | * @constructor 14 | * @param {Object|String} options - The HTTP server options. Either a URI string 15 | * (e.g. 'http://localhost:9090') or an object 16 | * with fields: 17 | * - {String} host - (optional) 18 | * - {Number} port 19 | * @param {Boolean} isSecure - True if using https for making calls, 20 | * otherwise false. 21 | * @return {Server} 22 | */ 23 | function Server(options, isSecure, onListening) { 24 | 25 | if (false === (this instanceof Server)) { 26 | return new Server(options, isSecure) 27 | } 28 | onListening = onListening || function() {} 29 | var that = this 30 | 31 | // If a string URI is passed in, converts to URI fields 32 | if (typeof options === 'string') { 33 | options = url.parse(options) 34 | options.host = options.hostname 35 | options.path = options.pathname 36 | } 37 | 38 | function handleMethodCall(request, response) { 39 | var deserializer = new Deserializer() 40 | deserializer.deserializeMethodCall(request, function(error, methodName, params) { 41 | if (Object.prototype.hasOwnProperty.call(that._events, methodName)) { 42 | that.emit(methodName, null, params, function(error, value) { 43 | var xml = null 44 | if (error !== null) { 45 | xml = Serializer.serializeFault(error) 46 | } 47 | else { 48 | xml = Serializer.serializeMethodResponse(value) 49 | } 50 | response.writeHead(200, {'Content-Type': 'text/xml'}) 51 | response.end(xml) 52 | }) 53 | } 54 | else { 55 | that.emit('NotFound', methodName, params) 56 | response.writeHead(404) 57 | response.end() 58 | } 59 | }) 60 | } 61 | 62 | this.httpServer = isSecure ? https.createServer(options, handleMethodCall) 63 | : http.createServer(handleMethodCall) 64 | 65 | process.nextTick(function() { 66 | this.httpServer.listen(options.port, options.host, onListening) 67 | }.bind(this)) 68 | this.close = function(callback) { 69 | this.httpServer.once('close', callback) 70 | this.httpServer.close() 71 | }.bind(this) 72 | } 73 | 74 | // Inherit from EventEmitter to emit and listen 75 | Server.prototype.__proto__ = EventEmitter.prototype 76 | 77 | module.exports = Server 78 | 79 | -------------------------------------------------------------------------------- /lib/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates object for cookies manipulation on client side. 3 | * Allows to parse server's response in order to get cookies and compose http request to transfer cookies to the server 4 | * @constructor 5 | */ 6 | function Cookies() { 7 | this.cookies = {} 8 | } 9 | 10 | Cookies.prototype = { 11 | /** 12 | * Obtains value of the cookie with specified name. 13 | * This call checks expiration dates and does not return expired cookies. 14 | * @param {String} name cookie name 15 | * @return {String} cookie value or null 16 | */ 17 | get: function(name) { 18 | var cookie = this.cookies[name] 19 | if (cookie && this.checkNotExpired(name)) { 20 | return this.cookies[name].value 21 | } 22 | return null 23 | }, 24 | 25 | /** 26 | * Sets cookie's value and optional options 27 | * @param {String} name cookie's name 28 | * @param {String} value value 29 | * @param {Object} options with the following fields: 30 | * - {Boolean} secure - is cookie secure or not (does not mean anything for now) 31 | * - {Date} expires - cookie's expiration date. If specified then cookie will disappear after that date 32 | */ 33 | set: function(name, value, options) { 34 | var cookie = typeof options == 'object' 35 | ? {value: value, expires: options.expires, secure: options.secure || false, new: options.new || false} 36 | : {value: value} 37 | if (this.checkNotExpired(name, cookie)) { 38 | this.cookies[name] = cookie 39 | } 40 | }, 41 | 42 | // For testing purposes 43 | getExpirationDate: function(name) { 44 | return this.cookies[name] ? this.cookies[name].expires : null 45 | }, 46 | 47 | // Internal function 48 | checkNotExpired: function(name, cookie) { 49 | if (typeof cookie === 'undefined') { 50 | cookie = this.cookies[name] 51 | } 52 | var now = new Date() 53 | if (cookie && cookie.expires && now > cookie.expires) { 54 | delete this.cookies[name] 55 | return false 56 | } 57 | return true 58 | }, 59 | 60 | 61 | /** 62 | * Parses headers from server's response for 'set-cookie' header and store cookie's values. 63 | * Also parses expiration date 64 | * @param headers 65 | */ 66 | parseResponse: function(headers) { 67 | var cookies = headers['set-cookie'] 68 | if (cookies) { 69 | cookies.forEach(function(c) { 70 | var cookiesParams = c.split(';') 71 | var cookiePair = cookiesParams.shift().split('=') 72 | var options = {} 73 | cookiesParams.forEach(function(param) { 74 | param = param.trim() 75 | if (param.toLowerCase().indexOf('expires') == 0) { 76 | var date = param.split('=')[1].trim() 77 | options.expires = new Date(date) 78 | } 79 | }) 80 | this.set(cookiePair[0].trim(), cookiePair[1].trim(), options) 81 | }.bind(this)) 82 | } 83 | }, 84 | 85 | /** 86 | * Adds cookies to the provided headers as array. Does nothing if there are no cookies stored. 87 | * This call checks expiration dates and does not add expired cookies. 88 | * @param headers 89 | */ 90 | composeRequest: function(headers) { 91 | if (Object.keys(this.cookies).length == 0) { 92 | return 93 | } 94 | headers['Cookie'] = this.toString() 95 | }, 96 | 97 | 98 | /** 99 | * 100 | * @return {String} cookies as 'name=value' pairs joined by semicolon 101 | */ 102 | toString: function() { 103 | return Object.keys(this.cookies) 104 | .filter(this.checkNotExpired.bind(this)) 105 | .map(function(name) { 106 | return name + '=' + this.cookies[name].value 107 | }.bind(this)).join(';') 108 | } 109 | } 110 | 111 | module.exports = Cookies 112 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 1.3.2 / 2016-06-16 2 | 3 | * Drops support CI/Support for node v0.8 4 | * Adds support for node v6.x 5 | * Adds support for emojies 6 | * Removes Makefile in favor of npm test 7 | * Updates XMLBuilder to 8.2 8 | * Updates Sax to 1.2 9 | 10 | ## 1.3.1 / 2015-05-23 11 | 12 | * Adds option to specify xml encoding. 13 | * Updates XMLBuilder to 2.6. 14 | * Adds support for Node v0.12. 15 | 16 | ## 1.3.0 / 2015-01-09 17 | 18 | * Reworked DateFormatter to improve ISO-8601 implementation. 19 | * Updates XMLBuilder to 2.4. 20 | * Updates sax to 0.6. 21 | * Adds more detail to Client request errors to ease debugging. 22 | 23 | ## 1.2.0 / 2014-01-12 24 | 25 | * Adds (tested) support for Node v0.10 and v0.11. 26 | * Drops support for Node v0.6. 27 | * Adds a Custom Type serializer for non-standard XML-RPC types. 28 | 29 | ## 1.1.1 / 2013-09-22 30 | 31 | * Adds CDATA deserialization. 32 | * Updates XMLBuilder to get rid of warnings at installation time. 33 | * Cleans up unformatted code. 34 | * Removes vestigial code from ancient v0.4 days. 35 | 36 | ## 1.1.0 / 2012-11-13 37 | 38 | * Supports optional listening handler in Server. 39 | * Adds close function to server. 40 | * Adds cookie support to client. 41 | * Changes minimum Node engine to v0.6. 42 | 43 | ## 1.0.2 / 2012-07-29 44 | 45 | * Server responds with a 404 if it does not handle the method. 46 | * Client returns an error if the server returns a 404. 47 | 48 | ## 1.0.1 / 2012-04-29 49 | 50 | * Fixes content-length header value when sending multi-byte characters. 51 | 52 | ## 1.0.0 / 2012-03-28 53 | 54 | * Replaces builder/parser logic with marshaller/unmarshaller. 55 | * Uses XML-RPC's nil for JavaScript null values and vice versa. 56 | * Client and parser are now re-entrant safe. 57 | * Moves test strings to fixture files. 58 | * Adds 30+ test cases. 59 | * Special thanks to @agnat for his work on the (un)marshaller and tests. 60 | 61 | ## 0.9.4 / 2012-03-03 62 | 63 | * Handles chunked method calls. 64 | * Supports ISO-8859-1 encodings. 65 | 66 | ## 0.9.3 / 2012-01-21 67 | 68 | * Values with no type now default to String as per XML-RPC spec. 69 | 70 | ## 0.9.2 / 2011-12-10 71 | 72 | * Replaces the XML parser with the Sax.js module. 73 | * Uses Travic CI for Continuous Integration. 74 | * Fixes accidental global variable. 75 | 76 | ## 0.9.1 / 2011-11-29 77 | 78 | * Returns an Error on invalid XML-RPC. 79 | * Updates to latest version of xmlbuilder to fix install warning. 80 | 81 | ## 0.9.0 / 2011-11-01 82 | 83 | * Supports the Base64 datatype. 84 | * Supports the i8 datatype, treating it as a float. 85 | * Fixes issue where not returning for faults containing no params. 86 | * Standardizes how to handle empty params. Last failing test now passes. 87 | * Enforces null value for error param in callbacks. 88 | * Updates to latest version of xmlbuilder. 89 | 90 | ## 0.8.1 / 2011-09-01 91 | 92 | * Supports passing the URI as a string to client or server. 93 | * Host is now an optional parameter for client or server. 94 | * Fixes bug when performing a method call multiple times. 95 | * Removes node_modules directory. Use `npm install .` if cloning. 96 | 97 | ## 0.8.0 / 2011-08-14 98 | 99 | * Supports HTTPS server and client. 100 | * Improves Basic Auth support. 101 | * Errors returned are now an instance of Error, not a String. 102 | * Fixes bug with structs and whitespace. 103 | * Fixes bug with empty arrays responses. 104 | 105 | ## 0.7.1 / 2011-08-02 106 | 107 | * Handles chunked method responses. 108 | * Fixes parsing multi-line strings values in the String parameter. 109 | * Allows for custom headers in the HTTP request. 110 | 111 | ## 0.7.0 / 2011-07-12 112 | 113 | * Renames Client.call() to Client.methodCall(). 114 | * Adds better support for sending and parsing empty String parameters. 115 | * Client handles errors on http request. Includes handling of invalid URLs. 116 | * Updates documentation. 117 | 118 | ## 0.6.2 / 2011-06-15 119 | 120 | * Fixes issue with parsing non-value whitespace in method calls. 121 | 122 | ## 0.6.1 / 2011-06-03 123 | 124 | * Supports CDATA when generating XML calls or responses. 125 | 126 | ## 0.6.0 / 2011-05-18 127 | 128 | * Initial release to NPM. Considered stable enough for public use. 129 | 130 | -------------------------------------------------------------------------------- /test/server_test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , http = require('http') 4 | , fs = require('fs') 5 | , Server = require('../lib/server') 6 | , Client = require('../lib/client') 7 | 8 | vows.describe('Server').addBatch({ 9 | 'A constructor' : { 10 | // Test string parameter for options 11 | 'with a string URI for options' : { 12 | topic: function () { 13 | var server = new Server('http://localhost:9005', false) 14 | server.on('testMethod', this.callback) 15 | 16 | // Waits briefly to give the server time to start up and start listening 17 | setTimeout(function () { 18 | var options = { host: 'localhost', port: 9005, path: '/' } 19 | var client = new Client(options, false) 20 | client.methodCall('testMethod', null, function() { }) 21 | }, 500) 22 | } 23 | , 'still responds' : function (error, value) { 24 | assert.isNull(error) 25 | assert.deepEqual(value, []) 26 | } 27 | } 28 | // Test default host 29 | , 'with no host specified' : { 30 | topic: function () { 31 | var server = new Server({ port: 9999, path: '/'}, false) 32 | server.on('testMethod', this.callback) 33 | 34 | // Waits briefly to give the server time to start up and start listening 35 | setTimeout(function () { 36 | var options = { host: 'localhost', port: 9999, path: '/' } 37 | var client = new Client(options, false) 38 | client.methodCall('testMethod', null, function() { }) 39 | }, 500) 40 | } 41 | , 'still responds' : function (error, value) { 42 | assert.isNull(error) 43 | assert.deepEqual(value, []) 44 | } 45 | } 46 | } 47 | ////////////////////////////////////////////////////////////////////// 48 | // Test method call functionality 49 | ////////////////////////////////////////////////////////////////////// 50 | , 'A method call' : { 51 | // Test chunked request 52 | 'with a chunked request' : { 53 | topic: function () { 54 | var server = new Server({ port: 9998, path: '/'}, false) 55 | server.on('testMethod', this.callback) 56 | 57 | // Waits briefly to give the server time to start up and start listening 58 | setTimeout(function () { 59 | var options = { host: 'localhost', port: 9998, path: '/', 60 | method: 'POST' } 61 | var req = http.request(options, function() {}) 62 | var chunk1 = '' 63 | + '' 64 | + 'testMethod' 65 | + '' 66 | + '' 67 | + 'Param A' 68 | + '' 69 | + '' 70 | var chunk2 = 'Param B' 71 | + '' 72 | + '' 73 | + '' 74 | req.on('error', function(e) { 75 | assert.isNull(e) 76 | }) 77 | req.write(chunk1) 78 | req.write(chunk2) 79 | req.end() 80 | }, 500) 81 | } 82 | , 'contains all the parameters' : function (error, value) { 83 | assert.isNull(error) 84 | assert.deepEqual(value, ['Param A', 'Param B']) 85 | } 86 | } 87 | } 88 | , 'Another call' :{ 89 | 'with an unknown method': { 90 | topic: function() { 91 | var server = new Server({ port: 9996, path: '/'}, false) 92 | 93 | server.on('NotFound', this.callback) 94 | setTimeout(function () { 95 | var options = { host: 'localhost', port: 9996, path: '/' } 96 | var client = new Client(options, false) 97 | client.methodCall('testMethod', null, function() { }) 98 | }, 500) 99 | } 100 | , 'return 404' : function (method, params) { 101 | assert.equal(method, 'testMethod') 102 | assert.deepEqual(params, []) 103 | } 104 | } 105 | } 106 | , 'close()': { 107 | topic: function() { 108 | console.log() 109 | var that = this 110 | var server = new Server({ port: 9995, path: '/'}, false, function() { 111 | server.close(function() { 112 | var server2 = new Server({ port: 9995, path: '/'}, false, that.callback) 113 | }) 114 | }) 115 | } 116 | , 'allows new connections on same port': function (error) { 117 | assert.ifError(error) 118 | } 119 | } 120 | }).export(module) 121 | -------------------------------------------------------------------------------- /lib/serializer.js: -------------------------------------------------------------------------------- 1 | var xmlBuilder = require('xmlbuilder') 2 | , dateFormatter = require('./date_formatter') 3 | , CustomType = require('./customtype') 4 | 5 | /** 6 | * Creates the XML for an XML-RPC method call. 7 | * 8 | * @param {String} method - The method name. 9 | * @param {Array} params - Params to pass in the call. 10 | * @param {Function} callback - function (error, xml) { ... } 11 | * - {Object|null} error - Any errors that occurred while building the XML, 12 | * otherwise null. 13 | * - {String} xml - The method call XML. 14 | */ 15 | exports.serializeMethodCall = function(method, params, encoding) { 16 | var params = params || [] 17 | 18 | var options = { version: '1.0', allowSurrogateChars: true } 19 | 20 | if (encoding) { 21 | options.encoding = encoding 22 | } 23 | 24 | var xml = xmlBuilder.create('methodCall', options) 25 | .ele('methodName') 26 | .txt(method) 27 | .up() 28 | .ele('params') 29 | 30 | params.forEach(function(param) { 31 | serializeValue(param, xml.ele('param')) 32 | }) 33 | 34 | // Includes the declaration 35 | return xml.doc().toString() 36 | } 37 | 38 | /** 39 | * Creates the XML for an XML-RPC method response. 40 | * 41 | * @param {mixed} value - The value to pass in the response. 42 | * @param {Function} callback - function (error, xml) { ... } 43 | * - {Object|null} error - Any errors that occurred while building the XML, 44 | * otherwise null. 45 | * - {String} xml - The method response XML. 46 | */ 47 | exports.serializeMethodResponse = function(result) { 48 | var xml = xmlBuilder.create('methodResponse', { version: '1.0', allowSurrogateChars: true }) 49 | .ele('params') 50 | .ele('param') 51 | 52 | serializeValue(result, xml) 53 | 54 | // Includes the declaration 55 | return xml.doc().toString() 56 | } 57 | 58 | exports.serializeFault = function(fault) { 59 | var xml = xmlBuilder.create('methodResponse', { version: '1.0', allowSurrogateChars: true }) 60 | .ele('fault') 61 | 62 | serializeValue(fault, xml) 63 | 64 | // Includes the declaration 65 | return xml.doc().toString() 66 | } 67 | 68 | function serializeValue(value, xml) { 69 | var stack = [ { value: value, xml: xml } ] 70 | , current = null 71 | , valueNode = null 72 | , next = null 73 | 74 | while (stack.length > 0) { 75 | current = stack[stack.length - 1] 76 | 77 | if (current.index !== undefined) { 78 | // Iterating a compound 79 | next = getNextItemsFrame(current) 80 | if (next) { 81 | stack.push(next) 82 | } 83 | else { 84 | stack.pop() 85 | } 86 | } 87 | else { 88 | // we're about to add a new value (compound or simple) 89 | valueNode = current.xml.ele('value') 90 | switch(typeof current.value) { 91 | case 'boolean': 92 | appendBoolean(current.value, valueNode) 93 | stack.pop() 94 | break 95 | case 'string': 96 | appendString(current.value, valueNode) 97 | stack.pop() 98 | break 99 | case 'number': 100 | appendNumber(current.value, valueNode) 101 | stack.pop() 102 | break 103 | case 'object': 104 | if (current.value === null) { 105 | valueNode.ele('nil') 106 | stack.pop() 107 | } 108 | else if (current.value instanceof Date) { 109 | appendDatetime(current.value, valueNode) 110 | stack.pop() 111 | } 112 | else if (Buffer.isBuffer(current.value)) { 113 | appendBuffer(current.value, valueNode) 114 | stack.pop() 115 | } 116 | else if (current.value instanceof CustomType) { 117 | current.value.serialize(valueNode) 118 | stack.pop() 119 | } 120 | else { 121 | if (Array.isArray(current.value)) { 122 | current.xml = valueNode.ele('array').ele('data') 123 | } 124 | else { 125 | current.xml = valueNode.ele('struct') 126 | current.keys = Object.keys(current.value) 127 | } 128 | current.index = 0 129 | next = getNextItemsFrame(current) 130 | if (next) { 131 | stack.push(next) 132 | } 133 | else { 134 | stack.pop() 135 | } 136 | } 137 | break 138 | default: 139 | stack.pop() 140 | break 141 | } 142 | } 143 | } 144 | } 145 | 146 | function getNextItemsFrame(frame) { 147 | var nextFrame = null 148 | 149 | if (frame.keys) { 150 | if (frame.index < frame.keys.length) { 151 | var key = frame.keys[frame.index++] 152 | , member = frame.xml.ele('member').ele('name').text(key).up() 153 | nextFrame = { 154 | value: frame.value[key] 155 | , xml: member 156 | } 157 | } 158 | } 159 | else if (frame.index < frame.value.length) { 160 | nextFrame = { 161 | value: frame.value[frame.index] 162 | , xml: frame.xml 163 | } 164 | frame.index++ 165 | } 166 | 167 | return nextFrame 168 | } 169 | 170 | function appendBoolean(value, xml) { 171 | xml.ele('boolean').txt(value ? 1 : 0) 172 | } 173 | 174 | var illegalChars = /^(?![^<&]*]]>[^<&]*)[^<&]*$/ 175 | function appendString(value, xml) { 176 | if (value.length === 0) { 177 | xml.ele('string') 178 | } 179 | else if (!illegalChars.test(value)) { 180 | xml.ele('string').d(value) 181 | } 182 | else { 183 | xml.ele('string').txt(value) 184 | } 185 | } 186 | 187 | function appendNumber(value, xml) { 188 | if (value % 1 == 0) { 189 | xml.ele('int').txt(value) 190 | } 191 | else { 192 | xml.ele('double').txt(value) 193 | } 194 | } 195 | 196 | function appendDatetime(value, xml) { 197 | xml.ele('dateTime.iso8601').txt(dateFormatter.encodeIso8601(value)) 198 | } 199 | 200 | function appendBuffer(value, xml) { 201 | xml.ele('base64').txt(value.toString('base64')) 202 | } 203 | -------------------------------------------------------------------------------- /test/cookies_test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , http = require('http') 4 | , Cookies = require('../lib/cookies') 5 | 6 | vows.describe('Cookies').addBatch({ 7 | 'Set/Get functionality': { 8 | 'set cookie with value only': { 9 | topic: function() { 10 | var cookies = new Cookies() 11 | cookies.set('a', 'b') 12 | return cookies.get('a') 13 | }, 14 | 'it shall be possible to get this value': function(value) { 15 | assert.equal(value, 'b') 16 | } 17 | }, 18 | 'get non-existent cookie': { 19 | topic: function() { 20 | var cookies = new Cookies() 21 | this.callback(null, cookies.get('c')) 22 | }, 23 | 'it shall return undefined': function(value) { 24 | assert.isNull(value) 25 | } 26 | }, 27 | 'set cookie with expiration date in the future': { 28 | topic: function() { 29 | var cookies = new Cookies() 30 | var date = new Date() 31 | date.setFullYear(date.getFullYear()+5) 32 | cookies.set('a', 'b', {expires: date}) 33 | return cookies.get('a') 34 | }, 35 | 'it shall be possible to get this value': function(value) { 36 | assert.equal(value, 'b') 37 | } 38 | }, 39 | 'set cookie with expiration date in the past': { 40 | topic: function() { 41 | var cookies = new Cookies() 42 | var date = new Date() 43 | date.setDate(date.getDate()-1) 44 | cookies.set('a', 'b', {expires: date}) 45 | this.callback(null, cookies.get('a')) 46 | }, 47 | 'it shall not be possible to get this value': function(value) { 48 | assert.isNull(value) 49 | } 50 | } 51 | }, 52 | 'parseResponse': { 53 | 'parsing response without cookies': { 54 | topic: function() { 55 | var cookies = new Cookies() 56 | cookies.parseResponse({}) 57 | cookies.parseResponse({ 58 | 'set-cookie': [] 59 | }) 60 | return 'ok' 61 | }, 62 | 'shall not process any error': function(value) { 63 | //ok 64 | } 65 | }, 66 | 'parsing response with cookie without expiration date': { 67 | topic: function() { 68 | var cookies = new Cookies() 69 | cookies.parseResponse({ 70 | 'set-cookie': [' name=value '] 71 | }) 72 | return cookies.get('name') 73 | }, 74 | 'shall get value of the cookie': function(value) { 75 | assert.equal(value, 'value') 76 | } 77 | }, 78 | 'response with cookie with expiration date': { 79 | topic: function() { 80 | var cookies = new Cookies() 81 | cookies.parseResponse({ 82 | 'set-cookie': [' name=value ;Expires=Wed, 01 Jan 2070 00:00:01 GMT '] 83 | }) 84 | return cookies 85 | }, 86 | 'shall get value of the cookie': function(cookies) { 87 | assert.equal(cookies.get('name'), 'value') 88 | }, 89 | 'shall get expiration date of the cookie': function(cookies) { 90 | assert.equal(cookies.getExpirationDate('name').toUTCString(), 'Wed, 01 Jan 2070 00:00:01 GMT') 91 | } 92 | }, 93 | 'response with cookie with expiration date and other fields': { 94 | topic: function() { 95 | var cookies = new Cookies() 96 | cookies.parseResponse({ 97 | 'set-cookie': [' name=value ;some=thing;Expires=Wed, 01 Jan 2070 00:00:01 GMT ;any=thing '] 98 | }) 99 | return cookies 100 | }, 101 | 'shall get value of the cookie': function(cookies) { 102 | assert.equal(cookies.get('name'), 'value') 103 | }, 104 | 'shall get expiration date of the cookie': function(cookies) { 105 | assert.equal(cookies.getExpirationDate('name').toUTCString(), 'Wed, 01 Jan 2070 00:00:01 GMT') 106 | } 107 | }, 108 | 'response with several cookies': { 109 | topic: function() { 110 | var cookies = new Cookies() 111 | cookies.parseResponse({ 112 | 'set-cookie': [' name=value ', 'name2=value2'] 113 | }) 114 | return cookies 115 | }, 116 | 'shall get value of all the cookies': function(cookies) { 117 | assert.equal(cookies.get('name'), 'value') 118 | assert.equal(cookies.get('name2'), 'value2') 119 | } 120 | } 121 | }, 122 | 'composeRequest': { 123 | 'when there are not cookies set': { 124 | topic: new Cookies(), 125 | 'shall not set cookie header': function(topic) { 126 | var headers = {} 127 | topic.composeRequest(headers) 128 | assert.isUndefined(headers['Cookie']) 129 | } 130 | }, 131 | 'when there is one cookie': { 132 | topic: function() { 133 | var cookies = new Cookies() 134 | cookies.set('a', 'b') 135 | return cookies 136 | }, 137 | 'shall set cookie header with name/value pair': function(topic) { 138 | var headers = {} 139 | topic.composeRequest(headers) 140 | assert.equal(headers['Cookie'], 'a=b') 141 | } 142 | }, 143 | 'when there are two cookies': { 144 | topic: function() { 145 | var cookies = new Cookies() 146 | cookies.set('a', 'b') 147 | cookies.set('c', 'd') 148 | return cookies 149 | }, 150 | 'shall set cookies header with name/value pairs separated by semicolon': function(topic) { 151 | var headers = {} 152 | topic.composeRequest(headers) 153 | assert.equal(headers['Cookie'], 'a=b;c=d') 154 | } 155 | }, 156 | 'when some cookie is expired': { 157 | topic: function() { 158 | var cookies = new Cookies() 159 | cookies.set('new', 'one') 160 | var date = new Date() 161 | date.setFullYear(date.getFullYear()-1) 162 | cookies.set('expired', 'value', {expires: date}) 163 | return cookies 164 | }, 165 | 'shall set cookies header with non-expired cookies only': function(topic) { 166 | var headers = {} 167 | topic.composeRequest(headers) 168 | assert.equal(headers['Cookie'], 'new=one') 169 | } 170 | } 171 | } 172 | }).export(module) -------------------------------------------------------------------------------- /test/date_formatter.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , date_formatter = require('../lib/date_formatter') 4 | 5 | var LOCAL_DATE = new Date(2014,0,20,14,25,25); 6 | 7 | vows.describe('Date Formatter').addBatch({ 8 | "A local date": { 9 | topic: LOCAL_DATE 10 | , "when encoded": { 11 | "without hyphens": encodeCase({hyphens: false}, '^\\d{8}T') 12 | , "with hyphens": encodeCase({hyphens: true}, '^\\d{4}[-]\\d{2}[-]\\d{2}T') 13 | , "without colons": encodeCase({colons: false}, 'T\\d{6}') 14 | , "with colons": encodeCase({colons: true}, 'T\\d{2}:\\d{2}:\\d{2}') 15 | , "without offset": encodeCase({offset: false}, 'T\\d{2}[:]?\\d{2}[:]?' 16 | + '\\d{2}(\\.\\d{3})?$') 17 | , "with offset": encodeCase({offset: true}, '([+-]\\d{2}[:]\\d{2}|Z)$') 18 | , "with milliseconds": encodeCase({ms: true}, '\\.\\d{3}([+-]\\d{2}[:]' 19 | + '\\d{2}|Z)?$') 20 | , "without milliseconds": encodeCase({ms: false}, 'T\\d{2}[:]?\\d{2}[:]?' 21 | + '\\d{2}([+-]\\d{2}[:]\\d{2}|Z)?$') 22 | , "to local representation": { 23 | topic: function (d) { 24 | date_formatter.setOpts() 25 | date_formatter.setOpts({local: true}) 26 | return date_formatter.encodeIso8601(d) 27 | } 28 | , "must return a properly formatted string": function (e, str) { 29 | var reStr = '^\\d{4}[-]?\\d{2}[-]?\\d{2}T\\d{2}[:]?\\d{2}[:]?\\d{2}' 30 | + '(\\.\\d{3})?([+-]\\d{2}[:]\\d{2})?$' 31 | assert.isNull(e) 32 | assert.isString(str) 33 | assert.match(str, new RegExp(reStr)) 34 | } 35 | , "must match the correct time": function (str) { 36 | var reStr = '^T14[:]?25[:]?25(\\.000)?([+-]\\d{2}[:]\\d{2})?$' 37 | assert.isString(str) 38 | assert.match(str, new RegExp()) 39 | } 40 | , teardown: function () { date_formatter.setOpts() } 41 | } 42 | , "to utc representation": { 43 | topic: function (d) { 44 | date_formatter.setOpts() 45 | date_formatter.setOpts({local: false}) 46 | return date_formatter.encodeIso8601(d) 47 | } 48 | , "must return a properly formatted string": function (e, str) { 49 | var reStr = '^\\d{4}[-]?\\d{2}[-]?\\d{2}T\\d{2}[:]?\\d{2}[:]?\\d{2}' 50 | + '(\\.\\d{3})?Z$' 51 | assert.isNull(e) 52 | assert.isString(str) 53 | assert.match(str, new RegExp(reStr)) 54 | } 55 | , "must match the correct time": function (str) { 56 | var offset = LOCAL_DATE.getTimezoneOffset() 57 | var round = (offset < 0) ? 'ceil' : 'floor' 58 | var reStr = [ 59 | 'T[0]?' 60 | , 14+Math[round](offset/60) 61 | , '[:]?[0]?' 62 | , 25+(offset%60) 63 | , '[:]?25(\\.000)?Z' 64 | ].join('') 65 | assert.isString(str) 66 | assert.match(str, new RegExp(reStr)) 67 | } 68 | , teardown: function () { date_formatter.setOpts() } 69 | } 70 | } 71 | } 72 | }).addBatch({ 73 | "When decoding": { 74 | "YYYY-MM-DDTHH:mm:ss.mss": decodeCase('2014-01-20T14:25:25.050' 75 | , localDate(2014,0,20,14,25,25,50)) 76 | , "YYYY-MM-DDTHH:mm:ss": decodeCase('2014-01-20T14:25:25' 77 | , localDate(2014,0,20,14,25,25)) 78 | , "YYYY-MM-DDTHH:mm": decodeCase('2014-01-20T14:25' 79 | , localDate(2014,0,20,14,25)) 80 | , "YYYY-MM-DDTHH": decodeCase('2014-01-20T14' 81 | , localDate(2014,0,20,14)) 82 | , "YYYY-MM-DD": decodeCase('2014-01-20' 83 | , localDate(2014,0,20)) 84 | , "YYYYMMDDTHH:mm:ss": decodeCase('20140120T14:25:25' 85 | , localDate(2014,0,20,14,25,25)) 86 | , "YYYYMMDDTHHmmss": decodeCase('20140120T142525' 87 | , localDate(2014,0,20,14,25,25)) 88 | , "YYYYMMDDTHHmm": decodeCase('20140120T1425', localDate(2014,0,20,14,25)) 89 | , "YYYYMMDD": decodeCase('20140120', localDate(2014,0,20)) 90 | , "YYYY-MM-DDTHH:mm:ss.mssZ": decodeCase('2014-01-20T14:25:25.050Z' 91 | , '2014-01-20T14:25:25.050Z') 92 | , "YYYY-MM-DDTHH:mm:ssZ": decodeCase('2014-01-20T14:25:25Z' 93 | , '2014-01-20T14:25:25.000Z') 94 | , "YYYY-MM-DDTHHZ": decodeCase('2014-01-20T14Z', '2014-01-20T14:00:00.000Z') 95 | , "YYYY-MM-DDTHH:mm:ss.mss+hh:mm": decodeCase('2014-01-20T14:25:25.000+09:30' 96 | , function () { 97 | var d = new Date('2014-01-20T14:25:25.000Z') 98 | d.setUTCHours(d.getUTCHours() - 9) 99 | d.setUTCMinutes(d.getUTCMinutes() - 30) 100 | return d.toISOString() 101 | } 102 | ) 103 | , "YYYY-MM-DDTHH:mm:ss.mss+hhmm": decodeCase('2014-01-20T14:25:25.000+0930' 104 | , function () { 105 | var d = new Date('2014-01-20T14:25:25.000Z') 106 | d.setUTCHours(d.getUTCHours() - 9) 107 | d.setUTCMinutes(d.getUTCMinutes() - 30) 108 | return d.toISOString() 109 | } 110 | ) 111 | , "YYYY-MM-DDTHH:mm:ss.mss+hh": decodeCase('2014-01-20T14:25:25.000+09' 112 | , function () { 113 | var d = new Date('2014-01-20T14:25:25.000Z') 114 | d.setUTCHours(d.getUTCHours() - 9) 115 | return d.toISOString() 116 | } 117 | ) 118 | } 119 | }).export(module) 120 | 121 | // HELPERS 122 | function encodeCase (opts, reStr) { 123 | return { 124 | topic: function (d) { 125 | date_formatter.setOpts(opts) 126 | return date_formatter.encodeIso8601(d) 127 | } 128 | , "must return a properly formatted string": function (e, str) { 129 | assert.isNull(e) 130 | assert.isString(str) 131 | assert.match(str, new RegExp(reStr)) 132 | } 133 | , teardown: function () { date_formatter.setOpts() } 134 | } 135 | } 136 | 137 | function decodeCase(str, check) { 138 | if (typeof(check) === 'function') check = check() 139 | return { 140 | topic: function () { 141 | return date_formatter.decodeIso8601(str) 142 | } 143 | , "must return the right Date": function (e, date) { 144 | assert.isNull(e) 145 | assert.instanceOf(date, Date) 146 | assert.equal(check, date.toISOString()) 147 | } 148 | } 149 | } 150 | 151 | function localDate (y,M,d,h,m,s,ms) { 152 | return (new Date(y||0,M||0,d||0,h||0,m||0,s||0,ms||0)).toISOString() 153 | } 154 | -------------------------------------------------------------------------------- /lib/date_formatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class DateFormatter 3 | * The DateFormatter supports decoding from and encoding to 4 | * ISO8601 formatted strings. Accepts formats with and without 5 | * hyphen/colon separators and correctly parses zoning info. 6 | */ 7 | var DateFormatter = function (opts) { 8 | this.opts = {} 9 | this.setOpts(opts) 10 | } 11 | 12 | /** 13 | * Default options for DateFormatter 14 | * @static 15 | * @see DateFormatter#setOpts 16 | */ 17 | DateFormatter.DEFAULT_OPTIONS = { 18 | colons: true 19 | , hyphens: false 20 | , local: true 21 | , ms: false 22 | , offset: false 23 | } 24 | 25 | /** 26 | * Regular Expression that disects ISO 8601 formatted strings into 27 | * an array of parts. 28 | * @static 29 | */ 30 | DateFormatter.ISO8601 = new RegExp( 31 | '([0-9]{4})([-]?([0-9]{2}))([-]?([0-9]{2}))' 32 | + '(T([0-9]{2})(((:?([0-9]{2}))?((:?([0-9]{2}))?(\.([0-9]+))?))?)' 33 | + '(Z|([+-]([0-9]{2}(:?([0-9]{2}))?)))?)?' 34 | ) 35 | 36 | /** 37 | * Sets options for encoding Date objects to ISO8601 strings. 38 | * Omitting the 'opts' argument will reset all options to the default. 39 | * 40 | * @param {Object} opts - Options (optional) 41 | * @param {Boolean} opts.colons - Enable/disable formatting the time portion 42 | * with a colon as separator (default: true) 43 | * @param {Boolean} opts.hyphens - Enable/disable formatting the date portion 44 | * with a hyphen as separator (default: false) 45 | * @param {Boolean} opts.local - Encode as local time instead of UTC 46 | * (default: true) 47 | * @param {Boolean} opts.ms - Enable/Disable output of milliseconds 48 | * (default: false) 49 | * @param {Boolean} opts.offset - Enable/Disable output of UTC offset 50 | * (default: false) 51 | */ 52 | DateFormatter.prototype.setOpts = function (opts) { 53 | if (!opts) opts = DateFormatter.DEFAULT_OPTIONS 54 | 55 | var ctx = this 56 | Object.keys(DateFormatter.DEFAULT_OPTIONS).forEach(function (k) { 57 | ctx.opts[k] = opts.hasOwnProperty(k) ? 58 | opts[k] : DateFormatter.DEFAULT_OPTIONS[k] 59 | }) 60 | } 61 | 62 | /** 63 | * Converts a date time stamp following the ISO8601 format to a JavaScript Date 64 | * object. 65 | * 66 | * @param {String} time - String representation of timestamp. 67 | * @return {Date} - Date object from timestamp. 68 | */ 69 | DateFormatter.prototype.decodeIso8601 = function(time) { 70 | var dateParts = time.toString().match(DateFormatter.ISO8601) 71 | if (!dateParts) { 72 | throw new Error('Expected a ISO8601 datetime but got \'' + time + '\'') 73 | } 74 | 75 | var date = [ 76 | [dateParts[1], dateParts[3] || '01', dateParts[5] || '01'].join('-') 77 | , 'T' 78 | , [ 79 | dateParts[7] || '00' 80 | , dateParts[11] || '00' 81 | , dateParts[14] || '00' 82 | ].join(':') 83 | , '.' 84 | , dateParts[16] || '000' 85 | ].join('') 86 | 87 | date += (dateParts[17] !== undefined) ? 88 | dateParts[17] + 89 | ((dateParts[19] && dateParts[20] === undefined) ? '00' : '') : 90 | DateFormatter.formatCurrentOffset(new Date(date)) 91 | 92 | return new Date(date) 93 | } 94 | 95 | /** 96 | * Converts a JavaScript Date object to an ISO8601 timestamp. 97 | * 98 | * @param {Date} date - Date object. 99 | * @return {String} - String representation of timestamp. 100 | */ 101 | DateFormatter.prototype.encodeIso8601 = function(date) { 102 | var parts = this.opts.local ? 103 | DateFormatter.getLocalDateParts(date) : 104 | DateFormatter.getUTCDateParts(date) 105 | 106 | return [ 107 | [parts[0],parts[1],parts[2]].join(this.opts.hyphens ? '-' : '') 108 | , 'T' 109 | , [parts[3],parts[4],parts[5]].join(this.opts.colons ? ':' : '') 110 | , (this.opts.ms) ? '.' + parts[6] : '' 111 | , (this.opts.local) ? ((this.opts.offset) ? 112 | DateFormatter.formatCurrentOffset(date) : '') : 'Z' 113 | ].join('') 114 | } 115 | 116 | /** 117 | * Helper function to get an array of zero-padded date parts, 118 | * in UTC 119 | * 120 | * @param {Date} date - Date Object 121 | * @return {String[]} 122 | */ 123 | DateFormatter.getUTCDateParts = function (date) { 124 | return [ 125 | date.getUTCFullYear() 126 | , DateFormatter.zeroPad(date.getUTCMonth()+1,2) 127 | , DateFormatter.zeroPad(date.getUTCDate(),2) 128 | , DateFormatter.zeroPad(date.getUTCHours(), 2) 129 | , DateFormatter.zeroPad(date.getUTCMinutes(), 2) 130 | , DateFormatter.zeroPad(date.getUTCSeconds(), 2) 131 | , DateFormatter.zeroPad(date.getUTCMilliseconds(), 3)] 132 | } 133 | 134 | 135 | /** 136 | * Helper function to get an array of zero-padded date parts, 137 | * in the local time zone 138 | * 139 | * @param {Date} date - Date Object 140 | * @return {String[]} 141 | */ 142 | DateFormatter.getLocalDateParts = function (date) { 143 | return [ 144 | date.getFullYear() 145 | , DateFormatter.zeroPad(date.getMonth()+1,2) 146 | , DateFormatter.zeroPad(date.getDate(),2) 147 | , DateFormatter.zeroPad(date.getHours(), 2) 148 | , DateFormatter.zeroPad(date.getMinutes(), 2) 149 | , DateFormatter.zeroPad(date.getSeconds(), 2) 150 | , DateFormatter.zeroPad(date.getMilliseconds(), 3)] 151 | } 152 | 153 | /** 154 | * Helper function to pad the digits with 0s to meet date formatting 155 | * requirements. 156 | * 157 | * @param {Number} digit - The number to pad. 158 | * @param {Number} length - Length of digit string, prefix with 0s if not 159 | * already length. 160 | * @return {String} - String with the padded digit 161 | */ 162 | DateFormatter.zeroPad = function (digit, length) { 163 | var padded = '' + digit 164 | while (padded.length < length) { 165 | padded = '0' + padded 166 | } 167 | 168 | return padded 169 | } 170 | 171 | /** 172 | * Helper function to get the current timezone to default decoding to 173 | * rather than UTC. (for backward compatibility) 174 | * 175 | * @return {String} - in the format /Z|[+-]\d{2}:\d{2}/ 176 | */ 177 | DateFormatter.formatCurrentOffset = function (d) { 178 | var offset = (d || new Date()).getTimezoneOffset() 179 | return (offset === 0) ? 'Z' : [ 180 | (offset < 0) ? '+' : '-' 181 | , DateFormatter.zeroPad(Math.abs(Math.floor(offset/60)),2) 182 | , ':' 183 | , DateFormatter.zeroPad(Math.abs(offset%60),2) 184 | ].join('') 185 | } 186 | 187 | // export an instance of DateFormatter only. 188 | module.exports = new DateFormatter() 189 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , https = require('https') 3 | , url = require('url') 4 | , Serializer = require('./serializer') 5 | , Deserializer = require('./deserializer') 6 | , Cookies = require('./cookies') 7 | 8 | /** 9 | * Creates a Client object for making XML-RPC method calls. 10 | * 11 | * @constructor 12 | * @param {Object|String} options - Server options to make the HTTP request to. 13 | * Either a URI string 14 | * (e.g. 'http://localhost:9090') or an object 15 | * with fields: 16 | * - {String} host - (optional) 17 | * - {Number} port 18 | * - {String} url - (optional) - may be used instead of host/port pair 19 | * - {Boolean} cookies - (optional) - if true then cookies returned by server will be stored and sent back on the next calls. 20 | * Also it will be possible to access/manipulate cookies via #setCookie/#getCookie methods 21 | * @param {Boolean} isSecure - True if using https for making calls, 22 | * otherwise false. 23 | * @return {Client} 24 | */ 25 | function Client(options, isSecure) { 26 | 27 | // Invokes with new if called without 28 | if (false === (this instanceof Client)) { 29 | return new Client(options, isSecure) 30 | } 31 | 32 | // If a string URI is passed in, converts to URI fields 33 | if (typeof options === 'string') { 34 | options = url.parse(options) 35 | options.host = options.hostname 36 | options.path = options.pathname 37 | } 38 | 39 | if (typeof options.url !== 'undefined') { 40 | var parsedUrl = url.parse(options.url); 41 | options.host = parsedUrl.hostname; 42 | options.path = parsedUrl.pathname; 43 | options.port = parsedUrl.port; 44 | } 45 | 46 | // Set the HTTP request headers 47 | var headers = { 48 | 'User-Agent' : 'NodeJS XML-RPC Client' 49 | , 'Content-Type' : 'text/xml' 50 | , 'Accept' : 'text/xml' 51 | , 'Accept-Charset' : 'UTF8' 52 | , 'Connection' : 'Keep-Alive' 53 | } 54 | options.headers = options.headers || {} 55 | 56 | if (options.headers.Authorization == null && 57 | options.basic_auth != null && 58 | options.basic_auth.user != null && 59 | options.basic_auth.pass != null) 60 | { 61 | var auth = options.basic_auth.user + ':' + options.basic_auth.pass 62 | options.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64') 63 | } 64 | 65 | for (var attribute in headers) { 66 | if (options.headers[attribute] === undefined) { 67 | options.headers[attribute] = headers[attribute] 68 | } 69 | } 70 | 71 | options.method = 'POST' 72 | this.options = options 73 | 74 | this.isSecure = isSecure 75 | this.headersProcessors = { 76 | processors: [], 77 | composeRequest: function(headers) { 78 | this.processors.forEach(function(p) {p.composeRequest(headers);}) 79 | }, 80 | parseResponse: function(headers) { 81 | this.processors.forEach(function(p) {p.parseResponse(headers);}) 82 | } 83 | }; 84 | if (options.cookies) { 85 | this.cookies = new Cookies(); 86 | this.headersProcessors.processors.unshift(this.cookies); 87 | } 88 | } 89 | 90 | /** 91 | * Makes an XML-RPC call to the server specified by the constructor's options. 92 | * 93 | * @param {String} method - The method name. 94 | * @param {Array} params - Params to send in the call. 95 | * @param {Function} callback - function(error, value) { ... } 96 | * - {Object|null} error - Any errors when making the call, otherwise null. 97 | * - {mixed} value - The value returned in the method response. 98 | */ 99 | Client.prototype.methodCall = function methodCall(method, params, callback) { 100 | var options = this.options 101 | var xml = Serializer.serializeMethodCall(method, params, options.encoding) 102 | var transport = this.isSecure ? https : http 103 | 104 | options.headers['Content-Length'] = Buffer.byteLength(xml, 'utf8') 105 | this.headersProcessors.composeRequest(options.headers) 106 | var request = transport.request(options, function(response) { 107 | 108 | var body = [] 109 | response.on('data', function (chunk) { body.push(chunk) }) 110 | 111 | function __enrichError (err) { 112 | Object.defineProperty(err, 'req', { value: request }) 113 | Object.defineProperty(err, 'res', { value: response }) 114 | Object.defineProperty(err, 'body', { value: body.join('') }) 115 | return err 116 | } 117 | 118 | if (response.statusCode == 404) { 119 | callback(__enrichError(new Error('Not Found'))) 120 | } 121 | else { 122 | this.headersProcessors.parseResponse(response.headers) 123 | 124 | var deserializer = new Deserializer(options.responseEncoding) 125 | 126 | deserializer.deserializeMethodResponse(response, function(err, result) { 127 | if (err) { 128 | err = __enrichError(err) 129 | } 130 | callback(err, result) 131 | }) 132 | } 133 | }.bind(this)) 134 | 135 | request.on('error', callback) 136 | request.write(xml, 'utf8') 137 | request.end() 138 | } 139 | 140 | /** 141 | * Gets the cookie value by its name. The latest value received from servr with 'Set-Cookie' header is returned 142 | * Note that method throws an error if cookies were not turned on during client creation (see comments for constructor) 143 | * 144 | * @param {String} name name of the cookie to be obtained or changed 145 | * @return {*} cookie's value 146 | */ 147 | Client.prototype.getCookie = function getCookie(name) { 148 | if (!this.cookies) { 149 | throw 'Cookies support is not turned on for this client instance'; 150 | } 151 | return this.cookies.get(name); 152 | } 153 | 154 | /** 155 | * Sets the cookie value by its name. The cookie will be sent to the server during the next xml-rpc call. 156 | * The method returns client itself, so it is possible to chain calls like the following: 157 | * 158 | * 159 | * client.cookie('login', 'alex').cookie('password', '123'); 160 | * 161 | * 162 | * Note that method throws an error if cookies were not turned on during client creation (see comments for constructor) 163 | * 164 | * @param {String} name name of the cookie to be changed 165 | * @param {String} value value to be set. 166 | * @return {*} client object itself 167 | */ 168 | Client.prototype.setCookie = function setCookie(name, value) { 169 | if (!this.cookies) { 170 | throw 'Cookies support is not turned on for this client instance'; 171 | } 172 | this.cookies.set(name, value); 173 | return this; 174 | } 175 | 176 | module.exports = Client 177 | 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The What 2 | 3 | The xmlrpc module is a pure JavaScript XML-RPC server and client for node.js. 4 | 5 | Pure JavaScript means that the [XML parsing](https://github.com/isaacs/sax-js) 6 | and [XML building](https://github.com/robrighter/node-xml) use pure JavaScript 7 | libraries, so no extra C dependencies or build requirements. The xmlrpc module 8 | can be used as an XML-RPC server, receiving method calls and responding with 9 | method responses, or as an XML-RPC client, making method calls and receiving 10 | method responses, or as both. 11 | 12 | 13 | ## The How 14 | 15 | ### To Install 16 | 17 | ```bash 18 | npm install xmlrpc 19 | ``` 20 | 21 | ### To Use 22 | 23 | The file client_server.js in the example directory has a nicely commented 24 | example of using xmlrpc as an XML-RPC server and client (they even talk to each 25 | other!). 26 | 27 | A brief example: 28 | 29 | ```javascript 30 | var xmlrpc = require('xmlrpc') 31 | 32 | // Creates an XML-RPC server to listen to XML-RPC method calls 33 | var server = xmlrpc.createServer({ host: 'localhost', port: 9090 }) 34 | // Handle methods not found 35 | server.on('NotFound', function(method, params) { 36 | console.log('Method ' + method + ' does not exist'); 37 | }) 38 | // Handle method calls by listening for events with the method call name 39 | server.on('anAction', function (err, params, callback) { 40 | console.log('Method call params for \'anAction\': ' + params) 41 | 42 | // ...perform an action... 43 | 44 | // Send a method response with a value 45 | callback(null, 'aResult') 46 | }) 47 | console.log('XML-RPC server listening on port 9091') 48 | 49 | // Waits briefly to give the XML-RPC server time to start up and start 50 | // listening 51 | setTimeout(function () { 52 | // Creates an XML-RPC client. Passes the host information on where to 53 | // make the XML-RPC calls. 54 | var client = xmlrpc.createClient({ host: 'localhost', port: 9090, path: '/'}) 55 | 56 | // Sends a method call to the XML-RPC server 57 | client.methodCall('anAction', ['aParam'], function (error, value) { 58 | // Results of the method response 59 | console.log('Method response for \'anAction\': ' + value) 60 | }) 61 | 62 | }, 1000) 63 | ``` 64 | 65 | Output from the example: 66 | 67 | ``` 68 | XML-RPC server listening on port 9090 69 | Method call params for 'anAction': aParam 70 | Method response for 'anAction': aResult 71 | ``` 72 | 73 | ### Date/Time Formatting 74 | 75 | XML-RPC dates are formatted according to ISO 8601. There are a number of 76 | formatting options within the boundaries of the standard. The decoder detects 77 | those formats and parses them automatically, but for encoding dates to ISO 78 | 8601 some options can be specified to match your specific implementation. 79 | 80 | 81 | The formatting options can be set through 82 | ```xmlrpc.dateFormatter.setOpts(options);```, where the ```options``` 83 | parameter is an object, with the following (optional) boolean members: 84 | 85 | * ```colons``` - enables/disables formatting the time portion with a colon as 86 | separator (default: ```true```) 87 | * ```hyphens``` - enables/disables formatting the date portion with a hyphen 88 | as separator (default: ```false```) 89 | * ```local``` - encode as local time instead of UTC (```true``` = local, 90 | ```false``` = utc, default: ```true```) 91 | * ```ms``` - enables/disables output of milliseconds (default: ```false```) 92 | * ```offset``` - enables/disables output of UTC offset in case of local time 93 | (default: ```false```) 94 | 95 | 96 | Default format: 20140101T11:20:00 97 | 98 | 99 | UTC Example: 100 | ```javascript 101 | xmlrpc.dateFormatter.setOpts({ 102 | colons: true 103 | , hyphens: true 104 | , local: false 105 | , ms: true 106 | }) // encoding output: '2014-01-01T16:20:00.000Z' 107 | ``` 108 | 109 | Local date + offset example: 110 | ```javascript 111 | xmlrpc.dateFormatter.setOpts({ 112 | colons: true 113 | , hyphens: true 114 | , local: true 115 | , ms: false 116 | , offset: true 117 | }) // encoding output: '2014-01-01T11:20:00-05:00' 118 | ``` 119 | 120 | ### Cookies support 121 | 122 | It is possible to turn on cookies support for XML-RPC client by special options 123 | flag. If turned on then all the cookies received from server will be bounced 124 | back with subsequent calls to the server. You also may manipulate cookies 125 | manually by the setCookie/getCookie call. 126 | 127 | ```javascript 128 | var client = xmlrpc.createClient({ 129 | host: 'localhost', 130 | port: 9090, 131 | cookies: true 132 | }); 133 | 134 | client.setCookie('login', 'bilbo'); 135 | 136 | //This call will send provided cookie to the server 137 | client.methodCall('someAction', [], function(error, value) { 138 | //Here we may get cookie received from server if we know its name 139 | console.log(client.getCookie('session')); 140 | }); 141 | 142 | ``` 143 | 144 | ### Custom Types 145 | If you need to parse to a specific format or need to handle custom data types 146 | that are not supported by default, it is possible to extend the serializer 147 | with a user-defined type for your specific needs. 148 | 149 | A custom type can be defined as follows: 150 | ```javascript 151 | var xmlrpc = require('xmlrpc'); 152 | var util = require('util'); 153 | 154 | // create your custom class 155 | var YourType = function (raw) { 156 | xmlrpc.CustomType.call(this, raw); 157 | }; 158 | 159 | // inherit everything 160 | util.inherits(YourType, xmlrpc.CustomType); 161 | 162 | // set a custom tagName (defaults to 'customType') 163 | YourType.prototype.tagName = 'yourType'; 164 | 165 | // optionally, override the serializer 166 | YourType.prototype.serialize = function (xml) { 167 | var value = somefunction(this.raw); 168 | return xml.ele(this.tagName).txt(value); 169 | } 170 | ``` 171 | 172 | and then make your method calls, wrapping your variables inside your new type 173 | definition: 174 | 175 | ```javascript 176 | var client = xmlrpc.createClient('YOUR_ENDPOINT'); 177 | client.methodCall('YOUR_METHOD', [new YourType(yourVariable)], yourCallback); 178 | ``` 179 | 180 | ### To Debug (client-side) 181 | 182 | Error callbacks on the client are enriched with request and response 183 | information and the returned body as long as a http connection was made, 184 | to aide with request debugging. Example: 185 | 186 | ```javascript 187 | var client = xmlrpc.createClient({ host: 'example.com', port: 80 }); 188 | client.methodCall('FAULTY_METHOD', [], function (error, value) { 189 | if (error) { 190 | console.log('error:', error); 191 | console.log('req headers:', error.req && error.req._header); 192 | console.log('res code:', error.res && error.res.statusCode); 193 | console.log('res body:', error.body); 194 | } else { 195 | console.log('value:', value); 196 | } 197 | }); 198 | 199 | // error: [Error: Unknown XML-RPC tag 'TITLE'] 200 | // req headers: POST / HTTP/1.1 201 | // User-Agent: NodeJS XML-RPC Client 202 | // ... 203 | // res code: 200 204 | // res body: 205 | // ... 206 | ``` 207 | 208 | ### To Test 209 | 210 | [![Build 211 | Status](https://secure.travis-ci.org/baalexander/node-xmlrpc.png)](http://travis-ci.org/baalexander/node-xmlrpc) 212 | 213 | XML-RPC must be precise so there are an extensive set of test cases in the test 214 | directory. [Vows](http://vowsjs.org/) is the testing framework and [Travis 215 | CI](http://travis-ci.org/baalexander/node-xmlrpc) is used for Continuous 216 | Integration. 217 | 218 | To run the test suite: 219 | 220 | `npm test` 221 | 222 | If submitting a bug fix, please update the appropriate test file too. 223 | 224 | 225 | ## The License (MIT) 226 | 227 | Released under the MIT license. See the LICENSE file for the complete wording. 228 | 229 | 230 | ## Contributors 231 | 232 | Thank you to all [the 233 | authors](https://github.com/baalexander/node-xmlrpc/graphs/contributors) and 234 | everyone who has filed an issue to help make xmlrpc better. 235 | 236 | -------------------------------------------------------------------------------- /example/client_server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The purpose of this example is to demonstrate an XML-RPC client interacting 3 | * with an XML-RPC server. Both client and server are using node-xmlrpc. 4 | * 5 | * The XML-RPC server is a basic parameter server. It exposes getter and setter 6 | * methods to get and set different data types. The XML-RPC client can then set 7 | * and get these values using method calls. 8 | */ 9 | 10 | var fs = require('fs') 11 | , xmlrpc = require('../lib/xmlrpc.js') 12 | 13 | 14 | //////////////////////////////////////////////////////////////////////// 15 | // The XML-RPC Server 16 | //////////////////////////////////////////////////////////////////////// 17 | 18 | // To simulate a simple parameter server, where values can be setted and getted 19 | // through XML-RPC, the contents of the calls are stored in an object 20 | var serverContents = { 21 | calls: [] 22 | , arrayValue: null 23 | , booleanValue: null 24 | , dateTimeValue: null 25 | , doubleValue: null 26 | , integerValue: null 27 | , stringValue: null 28 | , structValue: null 29 | } 30 | 31 | // Creates an XML-RPC server to listen for XML-RPC method calls 32 | var serverOptions = { 33 | host: 'localhost' 34 | , port: 9090 35 | } 36 | // Can use a String too: 37 | // var serverOptions = 'http://localhost:9090' 38 | var server = xmlrpc.createServer(serverOptions) 39 | 40 | // To use an HTTPS server instead, use createSecureServer: 41 | /* 42 | var secureServerOptions = { 43 | host: 'localhost' 44 | , port: 443 45 | , key: fs.readFileSync('./test-key.pem') 46 | , cert: fs.readFileSync('./test-cert.pem') 47 | } 48 | var server = xmlrpc.createSecureServer(secureServerOptions) 49 | */ 50 | 51 | // Handle method calls by listening for events with the method call name 52 | // Array handling 53 | // 'setArray' is the method call to listen for 54 | 55 | server.on('NotFound', function (methodeName, params) { 56 | console.log(methodeName+ ' is not found'); 57 | }) 58 | server.on('setArray', function (err, params, callback) { 59 | serverContents.calls.push('setArray') 60 | serverContents.arrayValue = params[0] 61 | callback() 62 | }) 63 | server.on('getArray', function (err, params, callback) { 64 | serverContents.calls.push('getArray') 65 | callback(null, serverContents.arrayValue) 66 | }) 67 | // Boolean handling 68 | server.on('setBoolean', function (err, params, callback) { 69 | serverContents.calls.push('setBoolean') 70 | serverContents.booleanValue = params[0] 71 | callback() 72 | }) 73 | server.on('getBoolean', function (err, params, callback) { 74 | serverContents.calls.push('getBoolean') 75 | callback(null, serverContents.booleanValue) 76 | }) 77 | // Date handling 78 | server.on('setDate', function (err, params, callback) { 79 | serverContents.calls.push('setDate') 80 | serverContents.dateValue = params[0] 81 | callback() 82 | }) 83 | server.on('getDate', function (err, params, callback) { 84 | serverContents.calls.push('getDate') 85 | callback(null, serverContents.dateValue) 86 | }) 87 | // Double handling 88 | server.on('setDouble', function (err, params, callback) { 89 | serverContents.calls.push('setDouble') 90 | serverContents.doubleValue = params[0] 91 | callback() 92 | }) 93 | server.on('getDouble', function (err, params, callback) { 94 | serverContents.calls.push('getDouble') 95 | callback(null, serverContents.doubleValue) 96 | }) 97 | // Integer handling 98 | server.on('setInteger', function (err, params, callback) { 99 | serverContents.calls.push('setInteger') 100 | serverContents.integerValue = params[0] 101 | callback() 102 | }) 103 | server.on('getInteger', function (err, params, callback) { 104 | serverContents.calls.push('getInteger') 105 | callback(null, serverContents.integerValue) 106 | }) 107 | // String handling 108 | server.on('setString', function (err, params, callback) { 109 | serverContents.calls.push('setString') 110 | serverContents.stringValue = params[0] 111 | callback() 112 | }) 113 | server.on('getString', function (err, params, callback) { 114 | serverContents.calls.push('getString') 115 | callback(null, serverContents.stringValue) 116 | }) 117 | // Struct handling 118 | server.on('setStruct', function (err, params, callback) { 119 | serverContents.calls.push('setStruct') 120 | serverContents.structValue = params[0] 121 | callback() 122 | }) 123 | server.on('getStruct', function (err, params, callback) { 124 | serverContents.calls.push('getStruct') 125 | callback(null, serverContents.structValue) 126 | }) 127 | // Call log handling 128 | server.on('getCallLog', function (err, params, callback) { 129 | serverContents.calls.push('getCallLog') 130 | callback(null, serverContents.calls) 131 | }) 132 | // Return a fault message 133 | server.on('fakeFault', function (error, params, callback) { 134 | serverContents.calls.push('fakeFault') 135 | callback({ faultCode: 2, faultString: 'Uh oh.'}, null) 136 | }) 137 | 138 | 139 | //////////////////////////////////////////////////////////////////////// 140 | // The XML-RPC Client 141 | //////////////////////////////////////////////////////////////////////// 142 | 143 | // Waits briefly to give the XML-RPC server time to start up and start listening 144 | setTimeout(function () { 145 | // Creates an XML-RPC client. Passes the host information on where to make the 146 | // XML-RPC calls. 147 | var clientOptions = { 148 | host: 'localhost' 149 | , port: 9090 150 | , path: '/' 151 | } 152 | // Can use a String too: 153 | // var clientOptions = 'http://localhost:9090' 154 | var client = xmlrpc.createClient(clientOptions) 155 | 156 | // To use HTTPS to make the call, use createSecureClient instead: 157 | /* 158 | var secureClientOptions = { 159 | host: 'localhost' 160 | , port: 443 161 | , path: '/' 162 | } 163 | var client = xmlrpc.createSecureClient(secureClientOptions) 164 | */ 165 | 166 | client.methodCall('setArray', [['value1', 'value2']], function(error, value) { 167 | client.methodCall('getArray', null, function (error, value) { 168 | console.log('Get Array Response: ' + value) 169 | }) 170 | }) 171 | 172 | client.methodCall('setBoolean', [true], function (error, value) { 173 | client.methodCall('getBoolean', null, function (error, value) { 174 | console.log('Get Boolean Response: ' + value) 175 | }) 176 | }) 177 | 178 | client.methodCall('setDate', [new Date(2016, 05, 08, 11, 35, 10)], function (error, value) { 179 | client.methodCall('getDate', null, function (error, value) { 180 | console.log('Get Date Response: ' + value) 181 | }) 182 | }) 183 | 184 | client.methodCall('setDouble', [24.99], function (error, value) { 185 | client.methodCall('getDouble', null, function (error, value) { 186 | console.log('Get Double Response: ' + value) 187 | }) 188 | }) 189 | 190 | client.methodCall('setInteger', [23], function (error, value) { 191 | client.methodCall('getInteger', null, function (error, value) { 192 | console.log('Get Integer Response: ' + value) 193 | }) 194 | }) 195 | 196 | client.methodCall('setString', ['testString1'], function (error, value) { 197 | client.methodCall('getString', null, function (error, value) { 198 | console.log('Get String Response: ' + value) 199 | }) 200 | }) 201 | 202 | client.methodCall('setStruct', [{ nameOfValue: 'Go 1998!' }], function (error, value) { 203 | client.methodCall('getStruct', null, function (error, value) { 204 | console.log('Get Struct Response (on next line): ') 205 | console.log(value) 206 | }) 207 | }) 208 | 209 | client.methodCall('fakeFault', null, function (error, value) { 210 | console.log('Fake Fault Response as Error (on next line): ') 211 | console.log(error) 212 | }) 213 | 214 | client.methodCall('getCallLog', null, function (error, value) { 215 | console.log('Get Call Log Response: ' + value) 216 | }) 217 | 218 | client.methodCall('notFound', null, function (error, value) { 219 | console.log('notFound: '+error); 220 | }); 221 | }, 1000) 222 | 223 | -------------------------------------------------------------------------------- /lib/deserializer.js: -------------------------------------------------------------------------------- 1 | var sax = require('sax') 2 | , dateFormatter = require('./date_formatter') 3 | 4 | var Deserializer = function(encoding) { 5 | this.type = null 6 | this.responseType = null 7 | this.stack = [] 8 | this.marks = [] 9 | this.data = [] 10 | this.methodname = null 11 | this.encoding = encoding || 'utf8' 12 | this.value = false 13 | this.callback = null 14 | this.error = null 15 | 16 | this.parser = sax.createStream() 17 | this.parser.on('opentag', this.onOpentag.bind(this)) 18 | this.parser.on('closetag', this.onClosetag.bind(this)) 19 | this.parser.on('text', this.onText.bind(this)) 20 | this.parser.on('cdata', this.onCDATA.bind(this)) 21 | this.parser.on('end', this.onDone.bind(this)) 22 | this.parser.on('error', this.onError.bind(this)) 23 | } 24 | 25 | Deserializer.prototype.deserializeMethodResponse = function(stream, callback) { 26 | var that = this 27 | 28 | this.callback = function(error, result) { 29 | if (error) { 30 | callback(error) 31 | } 32 | else if (result.length > 1) { 33 | callback(new Error('Response has more than one param')) 34 | } 35 | else if (that.type !== 'methodresponse') { 36 | callback(new Error('Not a method response')) 37 | } 38 | else if (!that.responseType) { 39 | callback(new Error('Invalid method response')) 40 | } 41 | else { 42 | callback(null, result[0]) 43 | } 44 | } 45 | 46 | stream.setEncoding(this.encoding) 47 | stream.on('error', this.onError.bind(this)) 48 | stream.pipe(this.parser) 49 | } 50 | 51 | Deserializer.prototype.deserializeMethodCall = function(stream, callback) { 52 | var that = this 53 | 54 | this.callback = function(error, result) { 55 | if (error) { 56 | callback(error) 57 | } 58 | else if (that.type !== 'methodcall') { 59 | callback(new Error('Not a method call')) 60 | } 61 | else if (!that.methodname) { 62 | callback(new Error('Method call did not contain a method name')) 63 | } 64 | else { 65 | callback(null, that.methodname, result) 66 | } 67 | } 68 | 69 | stream.setEncoding(this.encoding) 70 | stream.on('error', this.onError.bind(this)) 71 | stream.pipe(this.parser) 72 | } 73 | 74 | Deserializer.prototype.onDone = function() { 75 | var that = this 76 | 77 | if (!this.error) { 78 | if (this.type === null || this.marks.length) { 79 | this.callback(new Error('Invalid XML-RPC message')) 80 | } 81 | else if (this.responseType === 'fault') { 82 | var createFault = function(fault) { 83 | var error = new Error('XML-RPC fault' + (fault.faultString ? ': ' + fault.faultString : '')) 84 | error.code = fault.faultCode 85 | error.faultCode = fault.faultCode 86 | error.faultString = fault.faultString 87 | return error 88 | } 89 | this.callback(createFault(this.stack[0])) 90 | } 91 | else { 92 | this.callback(undefined, this.stack) 93 | } 94 | } 95 | } 96 | 97 | // TODO: 98 | // Error handling needs a little thinking. There are two different kinds of 99 | // errors: 100 | // 1. Low level errors like network, stream or xml errors. These don't 101 | // require special treatment. They only need to be forwarded. The IO 102 | // is already stopped in these cases. 103 | // 2. Protocol errors: Invalid tags, invalid values &c. These happen in 104 | // our code and we should tear down the IO and stop parsing. 105 | // Currently all errors end here. Guess I'll split it up. 106 | Deserializer.prototype.onError = function(msg) { 107 | if (!this.error) { 108 | if (typeof msg === 'string') { 109 | this.error = new Error(msg) 110 | } 111 | else { 112 | this.error = msg 113 | } 114 | this.callback(this.error) 115 | } 116 | } 117 | 118 | Deserializer.prototype.push = function(value) { 119 | this.stack.push(value) 120 | } 121 | 122 | //============================================================================== 123 | // SAX Handlers 124 | //============================================================================== 125 | 126 | Deserializer.prototype.onOpentag = function(node) { 127 | if (node.name === 'ARRAY' || node.name === 'STRUCT') { 128 | this.marks.push(this.stack.length) 129 | } 130 | this.data = [] 131 | this.value = (node.name === 'VALUE') 132 | } 133 | 134 | Deserializer.prototype.onText = function(text) { 135 | this.data.push(text) 136 | } 137 | 138 | Deserializer.prototype.onCDATA = function(cdata) { 139 | this.data.push(cdata) 140 | } 141 | 142 | Deserializer.prototype.onClosetag = function(el) { 143 | var data = this.data.join('') 144 | try { 145 | switch(el) { 146 | case 'BOOLEAN': 147 | this.endBoolean(data) 148 | break 149 | case 'INT': 150 | case 'I4': 151 | this.endInt(data) 152 | break 153 | case 'I8': 154 | this.endI8(data) 155 | break 156 | case 'DOUBLE': 157 | this.endDouble(data) 158 | break 159 | case 'STRING': 160 | case 'NAME': 161 | this.endString(data) 162 | break 163 | case 'ARRAY': 164 | this.endArray(data) 165 | break 166 | case 'STRUCT': 167 | this.endStruct(data) 168 | break 169 | case 'BASE64': 170 | this.endBase64(data) 171 | break 172 | case 'DATETIME.ISO8601': 173 | this.endDateTime(data) 174 | break 175 | case 'VALUE': 176 | this.endValue(data) 177 | break 178 | case 'PARAMS': 179 | this.endParams(data) 180 | break 181 | case 'FAULT': 182 | this.endFault(data) 183 | break 184 | case 'METHODRESPONSE': 185 | this.endMethodResponse(data) 186 | break 187 | case 'METHODNAME': 188 | this.endMethodName(data) 189 | break 190 | case 'METHODCALL': 191 | this.endMethodCall(data) 192 | break 193 | case 'NIL': 194 | this.endNil(data) 195 | break 196 | case 'DATA': 197 | case 'PARAM': 198 | case 'MEMBER': 199 | // Ignored by design 200 | break 201 | default: 202 | this.onError('Unknown XML-RPC tag \'' + el + '\'') 203 | break 204 | } 205 | } 206 | catch (e) { 207 | this.onError(e) 208 | } 209 | } 210 | 211 | Deserializer.prototype.endNil = function(data) { 212 | this.push(null) 213 | this.value = false 214 | } 215 | 216 | Deserializer.prototype.endBoolean = function(data) { 217 | if (data === '1') { 218 | this.push(true) 219 | } 220 | else if (data === '0') { 221 | this.push(false) 222 | } 223 | else { 224 | throw new Error('Illegal boolean value \'' + data + '\'') 225 | } 226 | this.value = false 227 | } 228 | 229 | Deserializer.prototype.endInt = function(data) { 230 | var value = parseInt(data, 10) 231 | if (isNaN(value)) { 232 | throw new Error('Expected an integer but got \'' + data + '\'') 233 | } 234 | else { 235 | this.push(value) 236 | this.value = false 237 | } 238 | } 239 | 240 | Deserializer.prototype.endDouble = function(data) { 241 | var value = parseFloat(data) 242 | if (isNaN(value)) { 243 | throw new Error('Expected a double but got \'' + data + '\'') 244 | } 245 | else { 246 | this.push(value) 247 | this.value = false 248 | } 249 | } 250 | 251 | Deserializer.prototype.endString = function(data) { 252 | this.push(data) 253 | this.value = false 254 | } 255 | 256 | Deserializer.prototype.endArray = function(data) { 257 | var mark = this.marks.pop() 258 | this.stack.splice(mark, this.stack.length - mark, this.stack.slice(mark)) 259 | this.value = false 260 | } 261 | 262 | Deserializer.prototype.endStruct = function(data) { 263 | var mark = this.marks.pop() 264 | , struct = {} 265 | , items = this.stack.slice(mark) 266 | , i = 0 267 | 268 | for (; i < items.length; i += 2) { 269 | struct[items[i]] = items[i + 1] 270 | } 271 | this.stack.splice(mark, this.stack.length - mark, struct) 272 | this.value = false 273 | } 274 | 275 | Deserializer.prototype.endBase64 = function(data) { 276 | var buffer = new Buffer(data, 'base64') 277 | this.push(buffer) 278 | this.value = false 279 | } 280 | 281 | Deserializer.prototype.endDateTime = function(data) { 282 | var date = dateFormatter.decodeIso8601(data) 283 | this.push(date) 284 | this.value = false 285 | } 286 | 287 | var isInteger = /^-?\d+$/ 288 | Deserializer.prototype.endI8 = function(data) { 289 | if (!isInteger.test(data)) { 290 | throw new Error('Expected integer (I8) value but got \'' + data + '\'') 291 | } 292 | else { 293 | this.endString(data) 294 | } 295 | } 296 | 297 | Deserializer.prototype.endValue = function(data) { 298 | if (this.value) { 299 | this.endString(data) 300 | } 301 | } 302 | 303 | Deserializer.prototype.endParams = function(data) { 304 | this.responseType = 'params' 305 | } 306 | 307 | Deserializer.prototype.endFault = function(data) { 308 | this.responseType = 'fault' 309 | } 310 | 311 | Deserializer.prototype.endMethodResponse = function(data) { 312 | this.type = 'methodresponse' 313 | } 314 | 315 | Deserializer.prototype.endMethodName = function(data) { 316 | this.methodname = data 317 | } 318 | 319 | Deserializer.prototype.endMethodCall = function(data) { 320 | this.type = 'methodcall' 321 | } 322 | 323 | module.exports = Deserializer 324 | 325 | -------------------------------------------------------------------------------- /test/deserializer_test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , path = require('path') 3 | , fs = require('fs') 4 | , assert = require('assert') 5 | , Deserializer = require('../lib/deserializer') 6 | , error_gallery = process.env.XMLRPC_ERROR_GALLERY 7 | 8 | 9 | vows.describe('Deserializer').addBatch({ 10 | 11 | 'deserializeMethodResponse() called with': { 12 | 13 | 'bad input containing': { 14 | 15 | 'broken xml': { 16 | topic: deserializeMethodResponseFixture('bad_food/broken_xml.xml') 17 | , 'results in an error': assertError 18 | } 19 | 20 | , 'non-xmlrpc xml tags': { 21 | topic: deserializeMethodResponseFixture('bad_food/unknown_tags.xml') 22 | , 'results in an error': assertError 23 | } 24 | 25 | , 'a bare sequence of params': { 26 | topic: deserializeMethodResponseFixture('bad_food/just_params.xml') 27 | , 'results in an error': assertError 28 | } 29 | 30 | } 31 | 32 | , 'type': { 33 | 34 | 'boolean': { 35 | 'set to a true value': { 36 | topic: deserializeMethodResponseFixture('good_food/boolean_true_response.xml') 37 | , 'does not return an error': assertOk 38 | , 'results in a true value': assertResponse(true) 39 | } 40 | , 'set to a false value': { 41 | topic: deserializeMethodResponseFixture('good_food/boolean_false_response.xml') 42 | , 'does not return an error': assertOk 43 | , 'results in a false value': assertResponse(false) 44 | } 45 | , 'containing an illegal value': { 46 | topic: deserializeMethodResponseFixture('bad_food/illegal_boolean_response.xml') 47 | , 'results in an error': assertError 48 | } 49 | } 50 | 51 | , 'datetime': { 52 | 'set to valid ISO8601 date': { 53 | topic: deserializeMethodResponseFixture('good_food/datetime_response.xml') 54 | , 'does not return an error': assertOk 55 | , 'results in a matching Date object': assertResponse(new Date(2012, 5, 8, 11, 35, 10)) 56 | } 57 | , 'containing an illegal value': { 58 | topic: deserializeMethodResponseFixture('bad_food/illegal_datetime_response.xml') 59 | , 'results in an error': assertError 60 | } 61 | } 62 | 63 | , 'base64': { 64 | topic: deserializeMethodResponseFixture('good_food/base64_response.xml') 65 | , 'does not return an error': assertOk 66 | , 'results in the correct buffer': assertResponse(new Buffer('dGVzdGluZw==', 'base64')) 67 | } 68 | // No illegal base64 test. node just skips illegal chars, which is RFC conform. 69 | 70 | , 'double': { 71 | 'set to ~\u03c0': { 72 | topic: deserializeMethodResponseFixture('good_food/double_positive_response.xml') 73 | , 'does not return an error': assertOk 74 | , 'results in the correct number': assertResponse(3.141592654) 75 | } 76 | , 'set to -\u221a2': { 77 | topic: deserializeMethodResponseFixture('good_food/double_negative_response.xml') 78 | , 'does not return an error': assertOk 79 | , 'results in the correct number': assertResponse(-1.41421) 80 | } 81 | , 'set to an illegal value': { 82 | topic: deserializeMethodResponseFixture('bad_food/illegal_double_response.xml') 83 | , 'results in an error': assertError 84 | } 85 | } 86 | 87 | , 'int': { 88 | 'set to a positive value': { 89 | topic: deserializeMethodResponseFixture('good_food/int_positive_response.xml') 90 | , 'does not return an error': assertOk 91 | , 'results in the correct number': assertResponse(4) 92 | } 93 | , 'set to a negative value': { 94 | topic: deserializeMethodResponseFixture('good_food/int_negative_response.xml') 95 | , 'does not return an error': assertOk 96 | , 'results in the correct number': assertResponse(-4) 97 | } 98 | , 'set to zero': { 99 | topic: deserializeMethodResponseFixture('good_food/int_zero_response.xml') 100 | , 'does not return an error': assertOk 101 | , 'results in the correct number': assertResponse(0) 102 | } 103 | , 'set to an illegal value': { 104 | topic: deserializeMethodResponseFixture('bad_food/illegal_int_response.xml') 105 | , 'results in an error': assertError 106 | } 107 | } 108 | 109 | , 'i4': { 110 | 'set to a positive value': { 111 | topic: deserializeMethodResponseFixture('good_food/i4_positive_response.xml') 112 | , 'does not return an error': assertOk 113 | , 'results in the correct number': assertResponse(4) 114 | } 115 | , 'set to a negative value': { 116 | topic: deserializeMethodResponseFixture('good_food/i4_negative_response.xml') 117 | , 'does not return an error': assertOk 118 | , 'results in the correct number': assertResponse(-4) 119 | } 120 | , 'set to zero': { 121 | topic: deserializeMethodResponseFixture('good_food/i4_zero_response.xml') 122 | , 'does not return an error': assertOk 123 | , 'results in the correct number': assertResponse(0) 124 | } 125 | , 'set to an illegal value': { 126 | topic: deserializeMethodResponseFixture('bad_food/illegal_i4_response.xml') 127 | , 'results in an error': assertError 128 | } 129 | } 130 | 131 | , 'i8': { 132 | 'set to a positive value': { 133 | topic: deserializeMethodResponseFixture('good_food/i8_positive_response.xml') 134 | , 'does not return an error': assertOk 135 | , 'results in the correct string': assertResponse('4611686018427387904') 136 | } 137 | , 'set to a negative value': { 138 | topic: deserializeMethodResponseFixture('good_food/i8_negative_response.xml') 139 | , 'does not return an error': assertOk 140 | , 'results in the correct string': assertResponse('-4611686018427387904') 141 | } 142 | , 'set to zero': { 143 | topic: deserializeMethodResponseFixture('good_food/i8_zero_response.xml') 144 | , 'does not return an error': assertOk 145 | , 'results in the correct string': assertResponse('0') 146 | } 147 | , 'set to an illegal value': { 148 | topic: deserializeMethodResponseFixture('bad_food/illegal_i8_response.xml') 149 | , 'results in an error': assertError 150 | } 151 | } 152 | 153 | , 'string': { 154 | 'containing characters': { 155 | topic: deserializeMethodResponseFixture('good_food/string_response.xml') 156 | , 'does not return an error': assertOk 157 | , 'results in the right string': assertResponse('testString') 158 | } 159 | , 'without content': { 160 | topic: deserializeMethodResponseFixture('good_food/string_empty_response.xml') 161 | , 'does not return an error': assertOk 162 | , 'results in an empty string': assertResponse('') 163 | } 164 | , 'containing CDATA': { 165 | topic: deserializeMethodResponseFixture('good_food/string_cdata_response.xml') 166 | , 'does not return an error': assertOk 167 | , 'results in the right string': assertResponse('') 168 | } 169 | } 170 | } 171 | 172 | , 'a param of unspecified type': { 173 | topic: deserializeMethodResponseFixture('good_food/unspecified_type_response.xml') 174 | , 'does not return an error': assertOk 175 | , 'results in a string': assertResponse('testString') 176 | } 177 | 178 | , 'compound': { 179 | 180 | 'array': { 181 | 'containing simple values': { 182 | topic: deserializeMethodResponseFixture('good_food/array_response.xml') 183 | , 'does not return an error': assertOk 184 | , 'results in the correct array': assertResponse([178, 'testString']) 185 | } 186 | , 'containing no values': { 187 | topic: deserializeMethodResponseFixture('good_food/array_empty_response.xml') 188 | , 'does not return an error': assertOk 189 | , 'results in an empty array': assertResponse([]) 190 | } 191 | , 'that has one nested ARRAY': { 192 | topic: deserializeMethodResponseFixture('good_food/array_nested_response.xml') 193 | , 'does not return an error': assertOk 194 | , 'results in an array containing another array': 195 | assertResponse([178, 'testLevel1String', ['testString', 64]]) 196 | } 197 | , 'that has a nested ARRAY followed by more simple values': { 198 | topic: deserializeMethodResponseFixture('good_food/array_nested_with_trailing_values_response.xml') 199 | , 'does not return an error': assertOk 200 | , 'results in an array containing another array and the trailing values': 201 | assertResponse([178, 'testLevel1String', ['testString', 64], 'testLevel1StringAfter']) 202 | } 203 | } 204 | 205 | , 'struct': { 206 | 'containing simple values': { 207 | topic: deserializeMethodResponseFixture('good_food/struct_response.xml') 208 | , 'does not return an error': assertOk 209 | , 'results in a matching object': assertResponse({'the-Name': 'testValue'}) 210 | } 211 | , 'containing an implicit string': { 212 | topic: deserializeMethodResponseFixture('good_food/struct_implicit_string_response.xml') 213 | , 'does not return an error': assertOk 214 | , 'results in a matching object': assertResponse({'the-Name': 'testValue'}) 215 | } 216 | , 'that has whitespace after the name element': { 217 | topic: deserializeMethodResponseFixture('good_food/struct_with_whitespace_response.xml') 218 | , 'does not return an error': assertOk 219 | , 'results in a matching object': assertResponse({'the-Name': 'testValue'}) 220 | } 221 | , 'containing another struct': { 222 | topic: deserializeMethodResponseFixture('good_food/struct_nested_response.xml') 223 | , 'does not return an error': assertOk 224 | , 'results in a matching object': 225 | assertResponse( { theName: 'testValue' 226 | , anotherName: { nestedName: 'nestedValue' } 227 | , lastName: 'Smith' 228 | }) 229 | } 230 | } 231 | 232 | , 'fault': { 233 | 'which includes error information': { 234 | topic: deserializeMethodResponseFixture('good_food/fault.xml') 235 | , 'results in an error': assertError 236 | , 'which has all properties of a proper xmlrpc fault': function(error, r) { 237 | assert.strictEqual(error.message, 'XML-RPC fault: Too many parameters.') 238 | assert.strictEqual(error.faultString, 'Too many parameters.') 239 | assert.strictEqual(error.faultCode, 4) 240 | } 241 | } 242 | , 'which does not include error information': { 243 | topic: deserializeMethodResponseFixture('good_food/fault_empty.xml') 244 | , 'results in an error': assertError 245 | } 246 | , 'that contains an empty string': { 247 | topic: deserializeMethodResponseFixture('good_food/fault_explicit_empty.xml') 248 | , 'results in an error': assertError 249 | } 250 | } 251 | , 'a mix of everything': { 252 | topic: deserializeMethodResponseFixture('good_food/grinder.xml') 253 | , 'does not return an error': assertOk 254 | , 'results in a matching object': 255 | assertResponse([ { theName: 'testValue' 256 | , anotherName: {nestedName: 'nestedValue' } 257 | , lastName: 'Smith' 258 | } 259 | , [ { yetAnotherName: 1999.26} , 'moreNested' ] 260 | ]) 261 | } 262 | , 'a very large response': { 263 | topic: deserializeMethodResponseFixture('good_food/very_large_response.xml') 264 | , 'does not return an error': assertOk 265 | , 'results in a matching object': function(error, result) { 266 | var resultsFile = path.join(__dirname, 'fixtures', 'good_food', 'very_large_response_results.json') 267 | var jsonResult = fs.readFileSync(resultsFile, 'utf8') 268 | assert.equal(JSON.stringify(result), jsonResult) 269 | } 270 | } 271 | } 272 | } 273 | }).export(module) 274 | 275 | //============================================================================== 276 | // Macros & Utilities 277 | //============================================================================== 278 | 279 | function fixtureStream(f) { 280 | return fs.createReadStream(path.join(__dirname, 'fixtures', f)) 281 | } 282 | 283 | function deserializeMethodResponseFixture(f) { 284 | return function() { 285 | var deserializer = new Deserializer() 286 | deserializer.deserializeMethodResponse(fixtureStream(f), this.callback) 287 | } 288 | } 289 | 290 | function assertError(error, result) { 291 | assert.instanceOf(error, Error) 292 | assert.isUndefined(result) 293 | if (error_gallery) { 294 | console.log('' + error) 295 | } 296 | } 297 | 298 | function assertOk(error, result) { 299 | assert.isTrue( ! error) 300 | } 301 | 302 | function assertResponse(what) { 303 | return function(error, result) { 304 | assert.deepEqual(result, what) 305 | } 306 | } 307 | 308 | -------------------------------------------------------------------------------- /test/client_test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , http = require('http') 4 | , Client = require('../lib/client') 5 | , fs = require('fs') 6 | 7 | const VALID_RESPONSE = fs.readFileSync(__dirname + '/fixtures/good_food/string_response.xml') 8 | const BROKEN_XML = fs.readFileSync(__dirname + '/fixtures/bad_food/broken_xml.xml') 9 | 10 | vows.describe('Client').addBatch({ 11 | ////////////////////////////////////////////////////////////////////// 12 | // Test Constructor functionality 13 | ////////////////////////////////////////////////////////////////////// 14 | 'A constructor' : { 15 | // Test standard Client initialization 16 | 'with URI options only' : { 17 | topic: function () { 18 | var client = new Client({ host: 'localhost', port: 9999, path: '/'}, false) 19 | return client.options 20 | } 21 | , 'contains the standard headers' : function (topic) { 22 | var headers = { 23 | 'User-Agent': 'NodeJS XML-RPC Client' 24 | , 'Content-Type': 'text/xml' 25 | , 'Accept': 'text/xml' 26 | , 'Accept-Charset': 'UTF8' 27 | , 'Connection': 'Keep-Alive' 28 | } 29 | assert.deepEqual(topic, { host: 'localhost', port: 9999, path: '/', method: 'POST', headers: headers }) 30 | } 31 | } 32 | // Test passing string URI for options 33 | , 'with a string URI for options' : { 34 | topic: function () { 35 | var client = new Client('http://localhost:9999', false) 36 | return client.options 37 | } 38 | , 'parses the string URI into URI fields' : function (topic) { 39 | assert.strictEqual(topic.host, 'localhost') 40 | assert.strictEqual(topic.path, '/') 41 | assert.equal(topic.port, 9999) 42 | } 43 | } 44 | // Test passing custom headers to the Client 45 | , 'with options containing header params' : { 46 | topic: function () { 47 | var client = new Client({ host: 'localhost', port: 9999, path: '/', headers: { 'User-Agent': 'Testaroo', 'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' }}, false) 48 | return client.options 49 | } 50 | , 'does not overwrite the custom headers' : function (topic) { 51 | var headers = { 52 | 'User-Agent': 'Testaroo' 53 | , 'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' 54 | , 'Content-Type': 'text/xml' 55 | , 'Accept': 'text/xml' 56 | , 'Accept-Charset' : 'UTF8' 57 | , 'Connection': 'Keep-Alive' 58 | } 59 | assert.deepEqual(topic, { host: 'localhost', port: 9999, path: '/', method: 'POST', headers: headers }) 60 | } 61 | } 62 | // Test passing HTTP Basic authentication credentials 63 | , 'with basic auth passed' : { 64 | topic: function () { 65 | var client = new Client({ basic_auth: { user: 'john', pass: '12345' } }, false) 66 | return client.options.headers 67 | } 68 | , 'correctly encodes and sets the \'Authorization\' header' : function (topic) { 69 | assert.isNotNull(topic.Authorization) 70 | assert.equal(topic.Authorization, 'Basic am9objoxMjM0NQ==') 71 | } 72 | } 73 | , 'with a string URI inside options' : { 74 | topic: function () { 75 | var client = new Client({url:'http://localhost:9999'}, false) 76 | return client.options 77 | } 78 | , 'parses the string URI into URI fields' : function (topic) { 79 | assert.strictEqual(topic.host, 'localhost') 80 | assert.strictEqual(topic.path, '/') 81 | assert.equal(topic.port, 9999) 82 | } 83 | } 84 | // Test passing encoding 85 | , 'with an encoding passed': { 86 | topic: function () { 87 | var client = new Client({ url:'http://localhost:9999', encoding: 'utf-8' }, false) 88 | return client.options 89 | } 90 | , 'caches the encoding option' : function (topic) { 91 | assert.strictEqual(topic.encoding, 'utf-8') 92 | } 93 | } 94 | } 95 | ////////////////////////////////////////////////////////////////////// 96 | // Test method call functionality 97 | ////////////////////////////////////////////////////////////////////// 98 | , 'A method call' : { 99 | // Test invalid internal URI to send method call to 100 | 'with an invalid internal URI' : { 101 | topic: function () { 102 | var client = new Client({ host: 'localhost', port: 9999, path: '/'}, false) 103 | client.methodCall('getArray', null, this.callback) 104 | } 105 | , 'contains the error' : function (error, value) { 106 | assert.isObject(error) 107 | } 108 | } 109 | , 'with a string URI for options' : { 110 | topic: function () { 111 | var that = this 112 | // Basic http server that sends a chunked XML response 113 | http.createServer(function (request, response) { 114 | response.writeHead(200, {'Content-Type': 'text/xml'}) 115 | var data = '' 116 | + '' 117 | + '' 118 | + 'more.listMethods' 119 | + '' 120 | + '' 121 | response.write(data) 122 | response.end() 123 | }).listen(9090, 'localhost', function() { 124 | var client = new Client('http://localhost:9090', false) 125 | client.methodCall('listMethods', null, that.callback) 126 | }) 127 | } 128 | , 'contains the string' : function (error, value) { 129 | assert.isNull(error) 130 | assert.deepEqual(value, 'more.listMethods') 131 | } 132 | } 133 | , 'with no host specified' : { 134 | topic: function () { 135 | var that = this 136 | // Basic http server that sends a chunked XML response 137 | http.createServer(function (request, response) { 138 | response.writeHead(200, {'Content-Type': 'text/xml'}) 139 | var data = '' 140 | + '' 141 | + '' 142 | + 'system.listMethods' 143 | + '' 144 | + '' 145 | response.write(data) 146 | response.end() 147 | }).listen(9091, 'localhost', function() { 148 | var client = new Client({ port: 9091, path: '/'}, false) 149 | client.methodCall('listMethods', null, that.callback) 150 | }) 151 | } 152 | , 'contains the string' : function (error, value) { 153 | assert.isNull(error) 154 | assert.deepEqual(value, 'system.listMethods') 155 | } 156 | } 157 | , 'with a chunked response' : { 158 | topic: function () { 159 | var that = this 160 | // Basic http server that sends a chunked XML response 161 | http.createServer(function (request, response) { 162 | response.writeHead(200, {'Content-Type': 'text/xml'}) 163 | var chunk1 = '' 164 | + '' 165 | + '' 166 | + '' 167 | + 'system.listMethods' 168 | + 'system.methodSignature' 169 | var chunk2 = 'xmlrpc_dialect' 170 | + '' 171 | + '' 172 | + '' 173 | response.write(chunk1) 174 | response.write(chunk2) 175 | response.end() 176 | }).listen(9092, 'localhost', function() { 177 | var client = new Client({ host: 'localhost', port: 9092, path: '/'}, false) 178 | client.methodCall('listMethods', null, that.callback) 179 | }) 180 | } 181 | , 'contains the array' : function (error, value) { 182 | assert.isNull(error) 183 | assert.deepEqual(value, ['system.listMethods', 'system.methodSignature', 'xmlrpc_dialect']) 184 | } 185 | } 186 | , 'with a utf-8 encoding' : { 187 | topic: function () { 188 | var that = this 189 | http.createServer(function (request, response) { 190 | response.writeHead(200, {'Content-Type': 'text/xml'}) 191 | var data = '' 192 | + '' 193 | + '' 194 | + 'here is mr. Snowman: ☃' 195 | + '' 196 | + '' 197 | response.write(data) 198 | response.end() 199 | }).listen(9093, 'localhost', function() { 200 | var client = new Client('http://localhost:9093', false) 201 | client.methodCall('listMethods', null, that.callback) 202 | }) 203 | } 204 | , 'contains the correct string' : function (error, value) { 205 | assert.isNull(error) 206 | assert.deepEqual(value, 'here is mr. Snowman: ☃') 207 | } 208 | } 209 | , 'with a ISO-8859-1 encoding' : { 210 | topic: function () { 211 | var that = this 212 | http.createServer(function (request, response) { 213 | response.writeHead(200, {'Content-Type': 'text/xml'}) 214 | // To prevent including a npm package that needs to compile (iconv): 215 | // The following iso 8859-1 text below in hex 216 | // 217 | // 218 | // 219 | // äè12 220 | // 221 | // 222 | var hex = '3c3f786d6c2076657273696f6e3d22312e302220656e636' 223 | + 'f64696e673d2249534f2d383835392d31223f3e3c6d6574686f64' 224 | + '526573706f6e73653e3c706172616d733e3c706172616d3e3c766' 225 | + '16c75653e3c737472696e673ee4e831323c2f737472696e673e3c' 226 | + '2f76616c75653e3c2f706172616d3e3c2f706172616d733e3c2f6' 227 | + 'd6574686f64526573706f6e73653e' 228 | var hexData = new Buffer(hex, 'hex') 229 | response.write(hexData) 230 | response.end() 231 | }).listen(9094, 'localhost', function() { 232 | var client = new Client({ host: 'localhost', port: 9094, path: '/', responseEncoding : 'binary'}, false) 233 | client.methodCall('listMethods', null, that.callback) 234 | }) 235 | } 236 | , 'contains the correct string' : function (error, value) { 237 | assert.isNull(error) 238 | assert.deepEqual(value, 'äè12') 239 | } 240 | } 241 | , 'with a multi-byte character in request' : { 242 | topic: function () { 243 | var that = this 244 | , requestBody = '' 245 | http.createServer(function (request, response) { 246 | request.setEncoding('utf8') 247 | request.on('data', function (chunk) { 248 | requestBody += chunk 249 | }) 250 | request.on('end', function () { 251 | response.writeHead(200, {'Content-Type': 'text/xml'}) 252 | var data = '' 253 | + '' 254 | + '' 255 | + 'ok' 256 | + '' 257 | + '' 258 | response.write(data) 259 | response.end() 260 | }) 261 | }).listen(9095, 'localhost', function() { 262 | var client = new Client({ host: 'localhost', port: 9095, path: '/'}, false) 263 | client.methodCall('multiByte', ['ö'], function (error) { 264 | that.callback(error, requestBody) 265 | }) 266 | }) 267 | } 268 | , 'contains full request' : function (error, value) { 269 | var data = '' 270 | + '' 271 | + 'multiByte' 272 | + 'ö' 273 | + '' 274 | assert.isNull(error) 275 | assert.deepEqual(value, data) 276 | } 277 | } 278 | , 'with an unknown request' : { 279 | topic: function () { 280 | var that = this 281 | http.createServer(function(request, response) { 282 | response.writeHead(404) 283 | response.end() 284 | }).listen(9099, 'localhost', function() { 285 | var client = new Client({ host: 'localhost', port: 9099, path: '/'}, false) 286 | client.methodCall('unknown', null, function (error) {that.callback(error)}) 287 | }) 288 | } 289 | , 'return NotFound Error' : function (error, value) { 290 | assert.isObject(error) 291 | } 292 | } 293 | , 'with cookies in response' : { 294 | topic: function (){ 295 | var that = this 296 | var invokeCount = 0 297 | http.createServer(function(request, response) { 298 | response.writeHead(200, {'Content-Type': 'text/xml', 'Set-Cookie': 'a=b'}) 299 | response.write(VALID_RESPONSE) 300 | response.end() 301 | invokeCount++ 302 | if (invokeCount == 2) { 303 | that.callback(undefined, request.headers['cookie']) 304 | } 305 | }).listen(9096, 'localhost', function() { 306 | var client = new Client({ host: 'localhost', port: 9096, path: '/', cookies: true}, false) 307 | function cbIfError(err, result) { 308 | if (err) that.callback(err, result) 309 | } 310 | client.methodCall('1', null, function(err, result) { 311 | cbIfError(err, result) 312 | client.methodCall('2', null, cbIfError) 313 | 314 | }) 315 | }) 316 | 317 | }, 318 | 'sends them back to the server' : function(error, value) { 319 | assert.isNull(error, 'Error was received but not expected') 320 | assert.equal(value, 'a=b') 321 | } 322 | } 323 | , 'that responds with a malformed xml': { 324 | topic: function () { 325 | var that = this 326 | http.createServer(function(request, response) { 327 | response.writeHead(500, {'Content-Type': 'text/html'}) 328 | response.write(BROKEN_XML) 329 | response.end() 330 | }).listen(9097, 'localhost', function () { 331 | var client = new Client({ host: 'localhost', port: 9097, path: '/' }, false) 332 | client.methodCall('broken', null, that.callback) 333 | }) 334 | } 335 | , 'returns an error': function (error, value) { 336 | assert.instanceOf(error, Error) 337 | assert.match(error.message, /^Unexpected end/) 338 | } 339 | , 'returns the request object with the error': function (error, value) { 340 | assert.instanceOf(error, Error) 341 | assert.isObject(error.req) 342 | assert.isObject(error.req.connection) 343 | assert.isString(error.req._header) 344 | } 345 | , 'returns the response object with the error': function (error, value) { 346 | assert.instanceOf(error, Error) 347 | assert.isObject(error.res) 348 | assert.strictEqual(error.res.statusCode, 500) 349 | } 350 | , 'returns the body with the error': function (error, value) { 351 | assert.strictEqual(error.body, BROKEN_XML.toString()) 352 | } 353 | } 354 | } 355 | }).export(module) 356 | -------------------------------------------------------------------------------- /test/serializer_test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , path = require('path') 3 | , fs = require('fs') 4 | , assert = require('assert') 5 | , Serializer = require('../lib/serializer') 6 | , CustomType = require('../lib/customtype') 7 | , util = require('util') 8 | 9 | vows.describe('Serializer').addBatch({ 10 | 11 | 'serializeMethodCall() called with': { 12 | 13 | 'type': { 14 | 15 | 'boolean' : { 16 | 'with a true boolean param' : { 17 | topic: function () { 18 | var value = true 19 | return Serializer.serializeMethodCall('testMethod', [value]) 20 | } 21 | , 'contains the value 1': assertXml('good_food/boolean_true_call.xml') 22 | } 23 | , 'with a false boolean param' : { 24 | topic: function () { 25 | var value = false 26 | return Serializer.serializeMethodCall('testMethod', [value]) 27 | } 28 | , 'contains the value 0': assertXml('good_food/boolean_false_call.xml') 29 | } 30 | } 31 | 32 | , 'datetime' : { 33 | 'with a regular datetime param' : { 34 | topic: function () { 35 | var value = new Date(2012, 05, 07, 11, 35, 10) 36 | return Serializer.serializeMethodCall('testMethod', [value]) 37 | } 38 | , 'contains the timestamp': assertXml('good_food/datetime_call.xml') 39 | } 40 | } 41 | 42 | , 'base64' : { 43 | 'with a base64 param' : { 44 | topic: function () { 45 | var value = new Buffer('dGVzdGluZw==', 'base64') 46 | return Serializer.serializeMethodCall('testMethod', [value]) 47 | } 48 | , 'contains the base64 string': assertXml('good_food/base64_call.xml') 49 | } 50 | } 51 | 52 | , 'double' : { 53 | 'with a positive double param' : { 54 | topic: function () { 55 | var value = 17.5 56 | return Serializer.serializeMethodCall('testMethod', [value]) 57 | } 58 | , 'contains the positive double': assertXml('good_food/double_positive_call.xml') 59 | } 60 | , 'with a negative double param' : { 61 | topic: function () { 62 | var value = -32.7777 63 | return Serializer.serializeMethodCall('testMethod', [value]) 64 | } 65 | , 'contains the negative double': assertXml('good_food/double_negative_call.xml') 66 | } 67 | } 68 | 69 | , 'integer' : { 70 | 'with a positive integer param' : { 71 | topic: function () { 72 | var value = 17 73 | return Serializer.serializeMethodCall('testMethod', [value]) 74 | } 75 | , 'contains the positive integer': assertXml('good_food/int_positive_call.xml') 76 | } 77 | , 'with a negative integer param' : { 78 | topic: function () { 79 | var value = -32 80 | return Serializer.serializeMethodCall('testMethod', [value]) 81 | } 82 | , 'contains the negative integer': assertXml('good_food/int_negative_call.xml') 83 | } 84 | , 'with an integer param of 0' : { 85 | topic: function () { 86 | var value = 0 87 | return Serializer.serializeMethodCall('testMethod', [value]) 88 | } 89 | , 'contains 0': assertXml('good_food/int_zero_call.xml') 90 | } 91 | } 92 | 93 | , 'nil' : { 94 | 'with a null param' : { 95 | topic: function () { 96 | var value = null 97 | return Serializer.serializeMethodCall('testMethod', [value]) 98 | } 99 | , 'contains the nil': assertXml('good_food/nil_call.xml') 100 | } 101 | } 102 | 103 | , 'string' : { 104 | 'with a regular string param' : { 105 | topic: function () { 106 | var value = 'testString' 107 | return Serializer.serializeMethodCall('testMethod', [value]) 108 | } 109 | , 'contains the string': assertXml('good_food/string_call.xml') 110 | } 111 | , 'with a string param that requires CDATA' : { 112 | topic: function () { 113 | var value = 'Congrats' 114 | return Serializer.serializeMethodCall('testCDATAMethod', [value]) 115 | } 116 | , 'contains the CDATA-wrapped string': assertXml('good_food/string_cdata_call.xml') 117 | } 118 | , 'with a multiline string param that requires CDATA' : { 119 | topic: function () { 120 | var value = '\nGo testing!\nCongrats\n' 121 | return Serializer.serializeMethodCall('testCDATAMethod', [value]) 122 | } 123 | , 'contains the CDATA-wrapped string': assertXml('good_food/string_multiline_cdata_call.xml') 124 | } 125 | , 'with an empty string' : { 126 | topic: function () { 127 | var value = '' 128 | return Serializer.serializeMethodCall('testMethod', [value]) 129 | } 130 | , 'contains the empty string': assertXml('good_food/string_empty_call.xml') 131 | } 132 | , 'with a string contains emoji': { 133 | topic: function () { 134 | var value = new Buffer('f09f9881', 'hex').toString('utf-8') 135 | return Serializer.serializeMethodCall('testMethod', [value]) 136 | } 137 | , 'contains a smiley' : assertXml('good_food/string_emoji.xml') 138 | } 139 | } 140 | 141 | , 'undefined' : { 142 | 'with an undefined param' : { 143 | topic: function () { 144 | var value = undefined 145 | return Serializer.serializeMethodCall('testMethod', [value]) 146 | } 147 | , 'contains the empty value': assertXml('good_food/undefined_call.xml') 148 | } 149 | } 150 | 151 | } 152 | 153 | , 'compound': { 154 | 155 | 'array' : { 156 | 'with a simple array' : { 157 | topic: function () { 158 | var value = ['string1', 3] 159 | return Serializer.serializeMethodCall('testMethod', [value]) 160 | } 161 | , 'contains the array': assertXml('good_food/array_call.xml') 162 | } 163 | } 164 | 165 | , 'struct' : { 166 | 'with a one-level struct' : { 167 | topic: function () { 168 | var value = { 169 | stringName: 'string1' 170 | , intName: 3 171 | } 172 | return Serializer.serializeMethodCall('testMethod', [value]) 173 | } 174 | , 'contains the struct': assertXml('good_food/struct_call.xml') 175 | } 176 | , 'with a one-level struct and an empty property name' : { 177 | topic: function () { 178 | var value = { 179 | stringName: '' 180 | , intName: 3 181 | } 182 | return Serializer.serializeMethodCall('testMethod', [value]) 183 | } 184 | , 'contains the struct': assertXml('good_food/struct_empty_property_call.xml') 185 | } 186 | , 'with a two-level struct' : { 187 | topic: function () { 188 | var value = { 189 | stringName: 'string1' 190 | , objectName: { 191 | intName: 4 192 | } 193 | } 194 | return Serializer.serializeMethodCall('testMethod', [value]) 195 | } 196 | , 'contains the struct': assertXml('good_food/struct_nested_call.xml') 197 | } 198 | } 199 | 200 | } 201 | , 'CustomType': { 202 | 'default' : { 203 | topic: function () { 204 | var value = new CustomType('testCustomType') 205 | return Serializer.serializeMethodCall('testMethod', [value]) 206 | } 207 | , 'contains the customType': assertXml('good_food/customtype_call.xml') 208 | } 209 | , 'extended' : { 210 | topic: function () { 211 | var ExtendedCustomType = function (raw) { 212 | raw = 'extended' + raw 213 | CustomType.call(this, raw) 214 | } 215 | util.inherits(ExtendedCustomType, CustomType) 216 | ExtendedCustomType.prototype.tagName = 'extendedCustomType' 217 | var value = new ExtendedCustomType('TestCustomType') 218 | return Serializer.serializeMethodCall('testMethod', [value]) 219 | } 220 | , 'contains the customType': assertXml('good_food/customtype_extended_call.xml') 221 | } 222 | } 223 | , 'utf-8 encoding': { 224 | topic: function () { 225 | var value = "\x46\x6F\x6F" 226 | return Serializer.serializeMethodCall('testMethod', [value], 'utf-8') 227 | } 228 | , 'contains the encoding attribute': assertXml('good_food/encoded_call.xml') 229 | } 230 | } 231 | 232 | , 'serializeMethodResponse() called with': { 233 | 234 | 'type': { 235 | 236 | 'boolean' : { 237 | 'with a true boolean param' : { 238 | topic: function () { 239 | var value = true 240 | return Serializer.serializeMethodResponse(value) 241 | } 242 | , 'contains the value 1': assertXml('good_food/boolean_true_response.xml') 243 | } 244 | , 'with a false boolean param' : { 245 | topic: function () { 246 | var value = false 247 | return Serializer.serializeMethodResponse(value) 248 | } 249 | , 'contains the value 0': assertXml('good_food/boolean_false_response.xml') 250 | } 251 | } 252 | 253 | , 'datetime' : { 254 | 'with a regular datetime param' : { 255 | topic: function () { 256 | var value = new Date(2012, 5, 8, 11, 35, 10) 257 | return Serializer.serializeMethodResponse(value) 258 | } 259 | , 'contains the timestamp': assertXml('good_food/datetime_response.xml') 260 | } 261 | } 262 | 263 | , 'base64' : { 264 | 'with a base64 param' : { 265 | topic: function () { 266 | var value = new Buffer('dGVzdGluZw==', 'base64') 267 | return Serializer.serializeMethodResponse(value) 268 | } 269 | , 'contains the base64 string': assertXml('good_food/base64_response.xml') 270 | } 271 | } 272 | 273 | , 'double' : { 274 | 'with a positive double param' : { 275 | topic: function () { 276 | var value = 3.141592654 277 | return Serializer.serializeMethodResponse(value) 278 | } 279 | , 'contains the positive double': assertXml('good_food/double_positive_response.xml') 280 | } 281 | , 'with a negative double param' : { 282 | topic: function () { 283 | var value = -1.41421 284 | return Serializer.serializeMethodResponse(value) 285 | } 286 | , 'contains the negative double': assertXml('good_food/double_negative_response.xml') 287 | } 288 | } 289 | 290 | , 'integer' : { 291 | 'with a positive integer param' : { 292 | topic: function () { 293 | var value = 4 294 | return Serializer.serializeMethodResponse(value) 295 | } 296 | , 'contains the positive integer': assertXml('good_food/int_positive_response.xml') 297 | } 298 | , 'with a negative integer param' : { 299 | topic: function () { 300 | var value = -4 301 | return Serializer.serializeMethodResponse(value) 302 | } 303 | , 'contains the negative integer': assertXml('good_food/int_negative_response.xml') 304 | } 305 | , 'with an integer param of 0' : { 306 | topic: function () { 307 | var value = 0 308 | return Serializer.serializeMethodResponse(value) 309 | } 310 | , 'contains 0': assertXml('good_food/int_zero_response.xml') 311 | } 312 | } 313 | 314 | , 'string' : { 315 | 'with a regular string param' : { 316 | topic: function () { 317 | var value = 'testString' 318 | return Serializer.serializeMethodResponse(value) 319 | } 320 | , 'contains the string': assertXml('good_food/string_response.xml') 321 | } 322 | , 'with an empty string' : { 323 | topic: function () { 324 | var value = '' 325 | return Serializer.serializeMethodResponse(value) 326 | } 327 | , 'contains the empty string': assertXml('good_food/string_empty_response.xml') 328 | } 329 | , 'with string contains emoji' : { 330 | topic: function () { 331 | var value = new Buffer('f09f9881', 'hex').toString('utf-8') 332 | return Serializer.serializeMethodResponse(value) 333 | } 334 | , 'contains emoji': assertXml('good_food/string_emoji_response.xml') 335 | } 336 | } 337 | 338 | , 'undefined' : { 339 | 'with an undefined param' : { 340 | topic: function () { 341 | var value = undefined 342 | return Serializer.serializeMethodResponse(value) 343 | } 344 | , 'contains the empty value': assertXml('good_food/undefined_response.xml') 345 | } 346 | } 347 | 348 | } 349 | 350 | , 'compound': { 351 | 352 | 'array' : { 353 | 'with a simple array' : { 354 | topic: function () { 355 | var value = [178, 'testString'] 356 | return Serializer.serializeMethodResponse(value) 357 | } 358 | , 'contains the array': assertXml('good_food/array_response.xml') 359 | } 360 | } 361 | 362 | , 'struct' : { 363 | 'with a one-level struct' : { 364 | topic: function () { 365 | var value = { 366 | 'the-Name': 'testValue' 367 | } 368 | return Serializer.serializeMethodResponse(value) 369 | } 370 | , 'contains the struct': assertXml('good_food/struct_response.xml') 371 | } 372 | , 'with a two-level struct' : { 373 | topic: function () { 374 | var value = { 375 | theName: 'testValue' 376 | , anotherName: { 377 | nestedName: 'nestedValue' 378 | } 379 | , lastName: 'Smith' 380 | } 381 | return Serializer.serializeMethodResponse(value) 382 | } 383 | , 'contains the struct': assertXml('good_food/struct_nested_response.xml') 384 | } 385 | } 386 | 387 | , 'fault' : { 388 | 'with a fault' : { 389 | topic: function () { 390 | var value = { 391 | faultCode: 4 392 | , faultString: 'Too many parameters.' 393 | } 394 | return Serializer.serializeFault(value) 395 | } 396 | , 'contains the fault': assertXml('good_food/fault.xml') 397 | } 398 | } 399 | 400 | } 401 | 402 | , 'CustomType': { 403 | 'default' : { 404 | topic: function () { 405 | var value = new CustomType('testCustomType') 406 | return Serializer.serializeMethodResponse(value) 407 | } 408 | , 'contains the customType': assertXml('good_food/customtype_response.xml') 409 | } 410 | , 'extended' : { 411 | topic: function () { 412 | var ExtendedCustomType = function (raw) { 413 | raw = 'extended' + raw 414 | CustomType.call(this, raw) 415 | } 416 | util.inherits(ExtendedCustomType, CustomType) 417 | ExtendedCustomType.prototype.tagName = 'extendedCustomType' 418 | var value = new ExtendedCustomType('TestCustomType') 419 | return Serializer.serializeMethodResponse(value) 420 | } 421 | , 'contains the customType': assertXml('good_food/customtype_extended_response.xml') 422 | } 423 | } 424 | } 425 | }).export(module) 426 | 427 | 428 | //============================================================================== 429 | // Utilities 430 | //============================================================================== 431 | 432 | function assertXml(fileName) { 433 | return function(result) { 434 | var file = path.join(__dirname, 'fixtures', fileName) 435 | var xml = fs.readFileSync(file, 'utf8').trim() 436 | assert.strictEqual(result, xml) 437 | } 438 | } 439 | 440 | -------------------------------------------------------------------------------- /test/fixtures/good_food/very_large_response_results.json: -------------------------------------------------------------------------------- 1 | [{"pinned":false,"stages":[{"progress":-1,"name":"node_head-macosx-gyp","startTime":"2012-02-17T13:32:02.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:32:02.000Z","warningCount":0,"endTimeMillis":"1329460332036","status":"success","completed":true,"endTime":"2012-02-17T13:32:12.000Z","properties":{},"startTimeMillis":"1329460322151","errorCount":0,"succeeded":true},{"progress":-1,"name":"npm_link","startTime":"2012-02-17T13:32:12.000Z","warningCount":0,"endTimeMillis":"1329460347608","status":"success","completed":true,"endTime":"2012-02-17T13:32:27.000Z","properties":{"exit code":"0","command line":"/Users/david/projects/node.js/node_install/bin/npm link --no-unicode","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979639/base"},"startTimeMillis":"1329460332065","errorCount":0,"succeeded":true},{"progress":-1,"name":"remove_waf_build","startTime":"2012-02-17T13:32:27.000Z","warningCount":0,"endTimeMillis":"1329460347699","status":"success","completed":true,"endTime":"2012-02-17T13:32:27.000Z","properties":{"exit code":"0","command line":"/bin/rm -rf out","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979639/base"},"startTimeMillis":"1329460347645","errorCount":0,"succeeded":true},{"progress":-1,"name":"gyp","startTime":"2012-02-17T13:32:27.000Z","warningCount":0,"endTimeMillis":"1329460348019","status":"success","completed":true,"endTime":"2012-02-17T13:32:28.000Z","properties":{"exit code":"0","command line":"/usr/bin/python /Users/david/projects/node.js/node/tools/gyp_addon --generator-output out -Goutput_dir=. -f make","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979639/base"},"startTimeMillis":"1329460347765","errorCount":0,"succeeded":true},{"progress":-1,"name":"make","startTime":"2012-02-17T13:32:28.000Z","warningCount":0,"endTimeMillis":"1329460362546","status":"success","completed":true,"endTime":"2012-02-17T13:32:42.000Z","properties":{"exit code":"0","command line":"/usr/bin/make","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979639/base"},"startTimeMillis":"1329460348090","errorCount":0,"succeeded":true},{"progress":-1,"name":"make_test","startTime":"2012-02-17T13:32:42.000Z","warningCount":0,"endTimeMillis":"1329460368172","status":"success","completed":true,"endTime":"2012-02-17T13:32:48.000Z","properties":{"targets":"test","exit code":"0","command line":"/usr/bin/make test","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979639/base"},"startTimeMillis":"1329460362621","errorCount":0,"succeeded":true},{"progress":-1,"name":"test_coverage","startTime":"2012-02-17T13:32:48.000Z","warningCount":0,"endTimeMillis":"1329460393367","status":"success","completed":true,"endTime":"2012-02-17T13:33:13.000Z","properties":{"targets":"coverage","exit code":"0","command line":"/usr/bin/make coverage","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979639/base"},"startTimeMillis":"1329460368182","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":2,"failures":0,"total":184,"errors":0,"passed":182},"warningCount":0,"endTimeMillis":"1329460393455","agent":"macosx","status":"success","completed":true,"endTime":"2012-02-17T13:33:13.000Z","startTimeMillis":"1329460322149","errorCount":0,"recipe":"gyp","succeeded":true},{"progress":-1,"name":"node_v0_7-macosx-waf","startTime":"2012-02-17T13:33:14.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:33:14.000Z","warningCount":0,"endTimeMillis":"1329460404598","status":"success","completed":true,"endTime":"2012-02-17T13:33:24.000Z","properties":{},"startTimeMillis":"1329460394466","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:33:24.000Z","warningCount":0,"endTimeMillis":"1329460421512","status":"success","completed":true,"endTime":"2012-02-17T13:33:41.000Z","properties":{"exit code":"0","command line":"/Users/david/.nvm/v0.7.1/bin/npm link --no-unicode","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979640/base"},"startTimeMillis":"1329460404615","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:33:41.000Z","warningCount":0,"endTimeMillis":"1329460426886","status":"success","completed":true,"endTime":"2012-02-17T13:33:46.000Z","properties":{"exit code":"0","command line":"/Users/david/.pulse2/data/agents/33/recipes/6979640/base/utils/testrun --verbose --ascii","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979640/base"},"startTimeMillis":"1329460421526","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":1,"failures":0,"total":184,"errors":0,"passed":183},"warningCount":0,"endTimeMillis":"1329460426934","agent":"macosx","status":"success","completed":true,"endTime":"2012-02-17T13:33:46.000Z","startTimeMillis":"1329460394464","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_v0_6-macosx-waf","startTime":"2012-02-17T13:33:47.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:33:47.000Z","warningCount":0,"endTimeMillis":"1329460436980","status":"success","completed":true,"endTime":"2012-02-17T13:33:56.000Z","properties":{},"startTimeMillis":"1329460427602","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:33:57.000Z","warningCount":0,"endTimeMillis":"1329460453074","status":"success","completed":true,"endTime":"2012-02-17T13:34:13.000Z","properties":{"exit code":"0","command line":"/Users/david/.nvm/v0.6.9/bin/npm link --no-unicode","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979641/base"},"startTimeMillis":"1329460437006","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:34:13.000Z","warningCount":0,"endTimeMillis":"1329460458650","status":"success","completed":true,"endTime":"2012-02-17T13:34:18.000Z","properties":{"exit code":"0","command line":"/Users/david/.pulse2/data/agents/33/recipes/6979641/base/utils/testrun --verbose --ascii","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979641/base"},"startTimeMillis":"1329460453087","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":1,"failures":0,"total":184,"errors":0,"passed":183},"warningCount":0,"endTimeMillis":"1329460458704","agent":"macosx","status":"success","completed":true,"endTime":"2012-02-17T13:34:18.000Z","startTimeMillis":"1329460427590","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_v0_4-macosx-waf","startTime":"2012-02-17T13:34:19.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:34:19.000Z","warningCount":0,"endTimeMillis":"1329460468260","status":"success","completed":true,"endTime":"2012-02-17T13:34:28.000Z","properties":{},"startTimeMillis":"1329460459512","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:34:28.000Z","warningCount":0,"endTimeMillis":"1329460499648","status":"success","completed":true,"endTime":"2012-02-17T13:34:59.000Z","properties":{"exit code":"0","command line":"/Users/david/.nvm/v0.4.9/bin/npm link --no-unicode","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979642/base"},"startTimeMillis":"1329460468279","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:34:59.000Z","warningCount":0,"endTimeMillis":"1329460505032","status":"success","completed":true,"endTime":"2012-02-17T13:35:05.000Z","properties":{"exit code":"0","command line":"/Users/david/.pulse2/data/agents/33/recipes/6979642/base/utils/testrun --verbose --ascii","working directory":"/Users/david/.pulse2/data/agents/33/recipes/6979642/base"},"startTimeMillis":"1329460499659","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":1,"failures":0,"total":184,"errors":0,"passed":183},"warningCount":0,"endTimeMillis":"1329460505101","agent":"macosx","status":"success","completed":true,"endTime":"2012-02-17T13:35:05.000Z","startTimeMillis":"1329460459511","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_head-linux-gyp","startTime":"2012-02-17T13:32:02.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:32:02.000Z","warningCount":0,"endTimeMillis":"1329460323599","status":"success","completed":true,"endTime":"2012-02-17T13:32:03.000Z","properties":{},"startTimeMillis":"1329460322339","errorCount":0,"succeeded":true},{"progress":-1,"name":"npm_link","startTime":"2012-02-17T13:32:03.000Z","warningCount":0,"endTimeMillis":"1329460342146","status":"success","completed":true,"endTime":"2012-02-17T13:32:22.000Z","properties":{"exit code":"0","command line":"/home/david/node_install/bin/npm link --no-unicode","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979643/base"},"startTimeMillis":"1329460323836","errorCount":0,"succeeded":true},{"progress":-1,"name":"remove_waf_build","startTime":"2012-02-17T13:32:22.000Z","warningCount":0,"endTimeMillis":"1329460342180","status":"success","completed":true,"endTime":"2012-02-17T13:32:22.000Z","properties":{"exit code":"0","command line":"/bin/rm -rf out","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979643/base"},"startTimeMillis":"1329460342173","errorCount":0,"succeeded":true},{"progress":-1,"name":"gyp","startTime":"2012-02-17T13:32:22.000Z","warningCount":0,"endTimeMillis":"1329460342448","status":"success","completed":true,"endTime":"2012-02-17T13:32:22.000Z","properties":{"exit code":"0","command line":"/usr/bin/python /home/david/node/tools/gyp_addon --generator-output out -Goutput_dir=. -f make","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979643/base"},"startTimeMillis":"1329460342196","errorCount":0,"succeeded":true},{"progress":-1,"name":"make","startTime":"2012-02-17T13:32:22.000Z","warningCount":0,"endTimeMillis":"1329460353675","status":"success","completed":true,"endTime":"2012-02-17T13:32:33.000Z","properties":{"exit code":"0","command line":"/usr/bin/make","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979643/base"},"startTimeMillis":"1329460342467","errorCount":0,"succeeded":true},{"progress":-1,"name":"make_test","startTime":"2012-02-17T13:32:33.000Z","warningCount":0,"endTimeMillis":"1329460370055","status":"success","completed":true,"endTime":"2012-02-17T13:32:50.000Z","properties":{"targets":"test","exit code":"0","command line":"/usr/bin/make test","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979643/base"},"startTimeMillis":"1329460353685","errorCount":0,"succeeded":true},{"progress":-1,"name":"test_coverage","startTime":"2012-02-17T13:32:50.000Z","warningCount":0,"endTimeMillis":"1329460398892","status":"success","completed":true,"endTime":"2012-02-17T13:33:18.000Z","properties":{"targets":"coverage","exit code":"0","command line":"/usr/bin/make coverage","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979643/base"},"startTimeMillis":"1329460370064","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":5,"failures":0,"total":183,"errors":0,"passed":178},"warningCount":0,"endTimeMillis":"1329460399063","agent":"ubuntu","status":"success","completed":true,"endTime":"2012-02-17T13:33:19.000Z","startTimeMillis":"1329460322310","errorCount":0,"recipe":"gyp","succeeded":true},{"progress":-1,"name":"node_v0_7-linux-waf","startTime":"2012-02-17T13:33:20.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:33:20.000Z","warningCount":0,"endTimeMillis":"1329460402019","status":"success","completed":true,"endTime":"2012-02-17T13:33:22.000Z","properties":{},"startTimeMillis":"1329460400633","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:33:22.000Z","warningCount":0,"endTimeMillis":"1329460421750","status":"success","completed":true,"endTime":"2012-02-17T13:33:41.000Z","properties":{"exit code":"0","command line":"/home/david/.nvm/v0.7.1/bin/npm link --no-unicode","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979644/base"},"startTimeMillis":"1329460402101","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:33:41.000Z","warningCount":0,"endTimeMillis":"1329460438028","status":"success","completed":true,"endTime":"2012-02-17T13:33:58.000Z","properties":{"exit code":"0","command line":"/home/david/.pulse2-agent/data/agents/354/recipes/6979644/base/utils/testrun --verbose --ascii","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979644/base"},"startTimeMillis":"1329460421765","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":5,"failures":0,"total":183,"errors":0,"passed":178},"warningCount":0,"endTimeMillis":"1329460438054","agent":"ubuntu","status":"success","completed":true,"endTime":"2012-02-17T13:33:58.000Z","startTimeMillis":"1329460400613","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_v0_6-linux-waf","startTime":"2012-02-17T13:33:58.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:33:58.000Z","warningCount":0,"endTimeMillis":"1329460440377","status":"success","completed":true,"endTime":"2012-02-17T13:34:00.000Z","properties":{},"startTimeMillis":"1329460438890","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:34:00.000Z","warningCount":0,"endTimeMillis":"1329460461115","status":"success","completed":true,"endTime":"2012-02-17T13:34:21.000Z","properties":{"exit code":"0","command line":"/home/david/.nvm/v0.6.9/bin/npm link --no-unicode","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979645/base"},"startTimeMillis":"1329460440540","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:34:21.000Z","warningCount":0,"endTimeMillis":"1329460477434","status":"success","completed":true,"endTime":"2012-02-17T13:34:37.000Z","properties":{"exit code":"0","command line":"/home/david/.pulse2-agent/data/agents/354/recipes/6979645/base/utils/testrun --verbose --ascii","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979645/base"},"startTimeMillis":"1329460461137","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":5,"failures":0,"total":183,"errors":0,"passed":178},"warningCount":0,"endTimeMillis":"1329460477458","agent":"ubuntu","status":"success","completed":true,"endTime":"2012-02-17T13:34:37.000Z","startTimeMillis":"1329460438850","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_v0_4-linux-waf","startTime":"2012-02-17T13:34:37.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:34:37.000Z","warningCount":0,"endTimeMillis":"1329460479066","status":"success","completed":true,"endTime":"2012-02-17T13:34:39.000Z","properties":{},"startTimeMillis":"1329460477797","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:34:39.000Z","warningCount":0,"endTimeMillis":"1329460508217","status":"success","completed":true,"endTime":"2012-02-17T13:35:08.000Z","properties":{"exit code":"0","command line":"/home/david/.nvm/v0.4.9/bin/npm link --no-unicode","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979646/base"},"startTimeMillis":"1329460479129","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:35:08.000Z","warningCount":0,"endTimeMillis":"1329460524421","status":"success","completed":true,"endTime":"2012-02-17T13:35:24.000Z","properties":{"exit code":"0","command line":"/home/david/.pulse2-agent/data/agents/354/recipes/6979646/base/utils/testrun --verbose --ascii","working directory":"/home/david/.pulse2-agent/data/agents/354/recipes/6979646/base"},"startTimeMillis":"1329460508228","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":5,"failures":0,"total":183,"errors":0,"passed":178},"warningCount":0,"endTimeMillis":"1329460524453","agent":"ubuntu","status":"success","completed":true,"endTime":"2012-02-17T13:35:24.000Z","startTimeMillis":"1329460477779","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_v0_6-freebsd-waf","startTime":"2012-02-17T13:32:02.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:32:02.000Z","warningCount":0,"endTimeMillis":"1329460324009","status":"success","completed":true,"endTime":"2012-02-17T13:32:04.000Z","properties":{},"startTimeMillis":"1329460322343","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:32:04.000Z","warningCount":0,"endTimeMillis":"1329460338764","status":"success","completed":true,"endTime":"2012-02-17T13:32:18.000Z","properties":{"exit code":"0","command line":"/home/david/.nvm/v0.6.10/bin/npm link --no-unicode","working directory":"/home/david/.pulse2-agent/data/agents/1342/recipes/6979647/base"},"startTimeMillis":"1329460324172","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:32:18.000Z","warningCount":0,"endTimeMillis":"1329460344660","status":"success","completed":true,"endTime":"2012-02-17T13:32:24.000Z","properties":{"exit code":"0","command line":"/home/david/.pulse2-agent/data/agents/1342/recipes/6979647/base/utils/testrun --verbose --ascii","working directory":"/home/david/.pulse2-agent/data/agents/1342/recipes/6979647/base"},"startTimeMillis":"1329460338785","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":1,"failures":0,"total":184,"errors":0,"passed":183},"warningCount":0,"endTimeMillis":"1329460344707","agent":"freebsd","status":"success","completed":true,"endTime":"2012-02-17T13:32:24.000Z","startTimeMillis":"1329460322318","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_v0_4-freebsd-waf","startTime":"2012-02-17T13:32:25.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:32:25.000Z","warningCount":0,"endTimeMillis":"1329460348170","status":"success","completed":true,"endTime":"2012-02-17T13:32:28.000Z","properties":{},"startTimeMillis":"1329460345755","errorCount":0,"succeeded":true},{"progress":-1,"name":"build","startTime":"2012-02-17T13:32:28.000Z","warningCount":0,"endTimeMillis":"1329460383025","status":"success","completed":true,"endTime":"2012-02-17T13:33:03.000Z","properties":{"exit code":"0","command line":"/home/david/.nvm/v0.4.9/bin/npm link --no-unicode","working directory":"/home/david/.pulse2-agent/data/agents/1342/recipes/6979648/base"},"startTimeMillis":"1329460348336","errorCount":0,"succeeded":true},{"progress":-1,"name":"test","startTime":"2012-02-17T13:33:03.000Z","warningCount":0,"endTimeMillis":"1329460388712","status":"success","completed":true,"endTime":"2012-02-17T13:33:08.000Z","properties":{"exit code":"0","command line":"/home/david/.pulse2-agent/data/agents/1342/recipes/6979648/base/utils/testrun --verbose --ascii","working directory":"/home/david/.pulse2-agent/data/agents/1342/recipes/6979648/base"},"startTimeMillis":"1329460383030","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":1,"failures":0,"total":184,"errors":0,"passed":183},"warningCount":0,"endTimeMillis":"1329460388748","agent":"freebsd","status":"success","completed":true,"endTime":"2012-02-17T13:33:08.000Z","startTimeMillis":"1329460345726","errorCount":0,"recipe":"waf","succeeded":true},{"progress":-1,"name":"node_head-win32-gyp","startTime":"2012-02-17T13:32:02.000Z","commands":[{"progress":-1,"name":"bootstrap","startTime":"2012-02-17T13:32:02.000Z","warningCount":0,"endTimeMillis":"1329460324787","status":"success","completed":true,"endTime":"2012-02-17T13:32:04.000Z","properties":{},"startTimeMillis":"1329460322364","errorCount":0,"succeeded":true},{"progress":-1,"name":"npm install","startTime":"2012-02-17T13:32:04.000Z","warningCount":0,"endTimeMillis":"1329460330834","status":"success","completed":true,"endTime":"2012-02-17T13:32:10.000Z","properties":{"exit code":"0","command line":"\"C:\\Program Files\\nodejs\\npm.cmd\" install --no-unicode","working directory":"C:\\Users\\david\\.pulse2-agent\\data\\agents\\1401\\recipes\\6979649\\base"},"startTimeMillis":"1329460324940","errorCount":0,"succeeded":true},{"progress":-1,"name":"gyp","startTime":"2012-02-17T13:32:10.000Z","warningCount":0,"endTimeMillis":"1329460332408","status":"success","completed":true,"endTime":"2012-02-17T13:32:12.000Z","properties":{"exit code":"0","command line":"C:\\Python27\\python.exe C:/Users/david/agnat_node/tools/gyp_addon","working directory":"C:\\Users\\david\\.pulse2-agent\\data\\agents\\1401\\recipes\\6979649\\base"},"startTimeMillis":"1329460330861","errorCount":0,"succeeded":true},{"progress":-1,"name":"msbuild","startTime":"2012-02-17T13:32:12.000Z","warningCount":0,"endTimeMillis":"1329460361932","status":"success","completed":true,"endTime":"2012-02-17T13:32:41.000Z","properties":{"build file":"dns_sd.sln","exit code":"0","command line":"C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild.exe dns_sd.sln /property:Configuration=Release /p:TargetExt=.node","working directory":"C:\\Users\\david\\.pulse2-agent\\data\\agents\\1401\\recipes\\6979649\\base","configuration":"Release"},"startTimeMillis":"1329460332427","errorCount":0,"succeeded":true}],"tests":{"expectedFailures":0,"skipped":0,"failures":0,"total":0,"errors":0,"passed":0},"warningCount":0,"endTimeMillis":"1329460362000","agent":"win7","status":"success","completed":true,"endTime":"2012-02-17T13:32:42.000Z","startTimeMillis":"1329460322342","errorCount":0,"recipe":"gyp win","succeeded":true}],"endTimeMillis":"1329460524674","project":"mdns","version":"184","endTime":"2012-02-17T13:35:24.000Z","tests":{"expectedFailures":0,"skipped":27,"failures":0,"total":1836,"errors":0,"passed":1809},"progress":-1,"revision":"d979f84891ccfe6cf101c10a37fc228efc08d045","startTime":"2012-02-17T13:32:02.000Z","maturity":"integration","id":184,"warningCount":0,"status":"success","reason":"manual trigger by admin","completed":true,"succeeded":true,"startTimeMillis":"1329460322130","errorCount":0}] -------------------------------------------------------------------------------- /test/fixtures/good_food/very_large_response.xml: -------------------------------------------------------------------------------- 1 | pinned0stagesprogress-1namenode_head-macosx-gypstartTime20120217T13:32:02Zcommandsprogress-1namebootstrapstartTime20120217T13:32:02ZwarningCount0endTimeMillis1329460332036statussuccesscompleted1endTime20120217T13:32:12ZpropertiesstartTimeMillis1329460322151errorCount0succeeded1progress-1namenpm_linkstartTime20120217T13:32:12ZwarningCount0endTimeMillis1329460347608statussuccesscompleted1endTime20120217T13:32:27Zpropertiesexit code0command line/Users/david/projects/node.js/node_install/bin/npm link --no-unicodeworking directory/Users/david/.pulse2/data/agents/33/recipes/6979639/basestartTimeMillis1329460332065errorCount0succeeded1progress-1nameremove_waf_buildstartTime20120217T13:32:27ZwarningCount0endTimeMillis1329460347699statussuccesscompleted1endTime20120217T13:32:27Zpropertiesexit code0command line/bin/rm -rf outworking directory/Users/david/.pulse2/data/agents/33/recipes/6979639/basestartTimeMillis1329460347645errorCount0succeeded1progress-1namegypstartTime20120217T13:32:27ZwarningCount0endTimeMillis1329460348019statussuccesscompleted1endTime20120217T13:32:28Zpropertiesexit code0command line/usr/bin/python /Users/david/projects/node.js/node/tools/gyp_addon --generator-output out -Goutput_dir=. -f makeworking directory/Users/david/.pulse2/data/agents/33/recipes/6979639/basestartTimeMillis1329460347765errorCount0succeeded1progress-1namemakestartTime20120217T13:32:28ZwarningCount0endTimeMillis1329460362546statussuccesscompleted1endTime20120217T13:32:42Zpropertiesexit code0command line/usr/bin/makeworking directory/Users/david/.pulse2/data/agents/33/recipes/6979639/basestartTimeMillis1329460348090errorCount0succeeded1progress-1namemake_teststartTime20120217T13:32:42ZwarningCount0endTimeMillis1329460368172statussuccesscompleted1endTime20120217T13:32:48Zpropertiestargetstestexit code0command line/usr/bin/make testworking directory/Users/david/.pulse2/data/agents/33/recipes/6979639/basestartTimeMillis1329460362621errorCount0succeeded1progress-1nametest_coveragestartTime20120217T13:32:48ZwarningCount0endTimeMillis1329460393367statussuccesscompleted1endTime20120217T13:33:13Zpropertiestargetscoverageexit code0command line/usr/bin/make coverageworking directory/Users/david/.pulse2/data/agents/33/recipes/6979639/basestartTimeMillis1329460368182errorCount0succeeded1testsexpectedFailures0skipped2failures0total184errors0passed182warningCount0endTimeMillis1329460393455agentmacosxstatussuccesscompleted1endTime20120217T13:33:13ZstartTimeMillis1329460322149errorCount0recipegypsucceeded1progress-1namenode_v0_7-macosx-wafstartTime20120217T13:33:14Zcommandsprogress-1namebootstrapstartTime20120217T13:33:14ZwarningCount0endTimeMillis1329460404598statussuccesscompleted1endTime20120217T13:33:24ZpropertiesstartTimeMillis1329460394466errorCount0succeeded1progress-1namebuildstartTime20120217T13:33:24ZwarningCount0endTimeMillis1329460421512statussuccesscompleted1endTime20120217T13:33:41Zpropertiesexit code0command line/Users/david/.nvm/v0.7.1/bin/npm link --no-unicodeworking directory/Users/david/.pulse2/data/agents/33/recipes/6979640/basestartTimeMillis1329460404615errorCount0succeeded1progress-1nameteststartTime20120217T13:33:41ZwarningCount0endTimeMillis1329460426886statussuccesscompleted1endTime20120217T13:33:46Zpropertiesexit code0command line/Users/david/.pulse2/data/agents/33/recipes/6979640/base/utils/testrun --verbose --asciiworking directory/Users/david/.pulse2/data/agents/33/recipes/6979640/basestartTimeMillis1329460421526errorCount0succeeded1testsexpectedFailures0skipped1failures0total184errors0passed183warningCount0endTimeMillis1329460426934agentmacosxstatussuccesscompleted1endTime20120217T13:33:46ZstartTimeMillis1329460394464errorCount0recipewafsucceeded1progress-1namenode_v0_6-macosx-wafstartTime20120217T13:33:47Zcommandsprogress-1namebootstrapstartTime20120217T13:33:47ZwarningCount0endTimeMillis1329460436980statussuccesscompleted1endTime20120217T13:33:56ZpropertiesstartTimeMillis1329460427602errorCount0succeeded1progress-1namebuildstartTime20120217T13:33:57ZwarningCount0endTimeMillis1329460453074statussuccesscompleted1endTime20120217T13:34:13Zpropertiesexit code0command line/Users/david/.nvm/v0.6.9/bin/npm link --no-unicodeworking directory/Users/david/.pulse2/data/agents/33/recipes/6979641/basestartTimeMillis1329460437006errorCount0succeeded1progress-1nameteststartTime20120217T13:34:13ZwarningCount0endTimeMillis1329460458650statussuccesscompleted1endTime20120217T13:34:18Zpropertiesexit code0command line/Users/david/.pulse2/data/agents/33/recipes/6979641/base/utils/testrun --verbose --asciiworking directory/Users/david/.pulse2/data/agents/33/recipes/6979641/basestartTimeMillis1329460453087errorCount0succeeded1testsexpectedFailures0skipped1failures0total184errors0passed183warningCount0endTimeMillis1329460458704agentmacosxstatussuccesscompleted1endTime20120217T13:34:18ZstartTimeMillis1329460427590errorCount0recipewafsucceeded1progress-1namenode_v0_4-macosx-wafstartTime20120217T13:34:19Zcommandsprogress-1namebootstrapstartTime20120217T13:34:19ZwarningCount0endTimeMillis1329460468260statussuccesscompleted1endTime20120217T13:34:28ZpropertiesstartTimeMillis1329460459512errorCount0succeeded1progress-1namebuildstartTime20120217T13:34:28ZwarningCount0endTimeMillis1329460499648statussuccesscompleted1endTime20120217T13:34:59Zpropertiesexit code0command line/Users/david/.nvm/v0.4.9/bin/npm link --no-unicodeworking directory/Users/david/.pulse2/data/agents/33/recipes/6979642/basestartTimeMillis1329460468279errorCount0succeeded1progress-1nameteststartTime20120217T13:34:59ZwarningCount0endTimeMillis1329460505032statussuccesscompleted1endTime20120217T13:35:05Zpropertiesexit code0command line/Users/david/.pulse2/data/agents/33/recipes/6979642/base/utils/testrun --verbose --asciiworking directory/Users/david/.pulse2/data/agents/33/recipes/6979642/basestartTimeMillis1329460499659errorCount0succeeded1testsexpectedFailures0skipped1failures0total184errors0passed183warningCount0endTimeMillis1329460505101agentmacosxstatussuccesscompleted1endTime20120217T13:35:05ZstartTimeMillis1329460459511errorCount0recipewafsucceeded1progress-1namenode_head-linux-gypstartTime20120217T13:32:02Zcommandsprogress-1namebootstrapstartTime20120217T13:32:02ZwarningCount0endTimeMillis1329460323599statussuccesscompleted1endTime20120217T13:32:03ZpropertiesstartTimeMillis1329460322339errorCount0succeeded1progress-1namenpm_linkstartTime20120217T13:32:03ZwarningCount0endTimeMillis1329460342146statussuccesscompleted1endTime20120217T13:32:22Zpropertiesexit code0command line/home/david/node_install/bin/npm link --no-unicodeworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979643/basestartTimeMillis1329460323836errorCount0succeeded1progress-1nameremove_waf_buildstartTime20120217T13:32:22ZwarningCount0endTimeMillis1329460342180statussuccesscompleted1endTime20120217T13:32:22Zpropertiesexit code0command line/bin/rm -rf outworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979643/basestartTimeMillis1329460342173errorCount0succeeded1progress-1namegypstartTime20120217T13:32:22ZwarningCount0endTimeMillis1329460342448statussuccesscompleted1endTime20120217T13:32:22Zpropertiesexit code0command line/usr/bin/python /home/david/node/tools/gyp_addon --generator-output out -Goutput_dir=. -f makeworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979643/basestartTimeMillis1329460342196errorCount0succeeded1progress-1namemakestartTime20120217T13:32:22ZwarningCount0endTimeMillis1329460353675statussuccesscompleted1endTime20120217T13:32:33Zpropertiesexit code0command line/usr/bin/makeworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979643/basestartTimeMillis1329460342467errorCount0succeeded1progress-1namemake_teststartTime20120217T13:32:33ZwarningCount0endTimeMillis1329460370055statussuccesscompleted1endTime20120217T13:32:50Zpropertiestargetstestexit code0command line/usr/bin/make testworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979643/basestartTimeMillis1329460353685errorCount0succeeded1progress-1nametest_coveragestartTime20120217T13:32:50ZwarningCount0endTimeMillis1329460398892statussuccesscompleted1endTime20120217T13:33:18Zpropertiestargetscoverageexit code0command line/usr/bin/make coverageworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979643/basestartTimeMillis1329460370064errorCount0succeeded1testsexpectedFailures0skipped5failures0total183errors0passed178warningCount0endTimeMillis1329460399063agentubuntustatussuccesscompleted1endTime20120217T13:33:19ZstartTimeMillis1329460322310errorCount0recipegypsucceeded1progress-1namenode_v0_7-linux-wafstartTime20120217T13:33:20Zcommandsprogress-1namebootstrapstartTime20120217T13:33:20ZwarningCount0endTimeMillis1329460402019statussuccesscompleted1endTime20120217T13:33:22ZpropertiesstartTimeMillis1329460400633errorCount0succeeded1progress-1namebuildstartTime20120217T13:33:22ZwarningCount0endTimeMillis1329460421750statussuccesscompleted1endTime20120217T13:33:41Zpropertiesexit code0command line/home/david/.nvm/v0.7.1/bin/npm link --no-unicodeworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979644/basestartTimeMillis1329460402101errorCount0succeeded1progress-1nameteststartTime20120217T13:33:41ZwarningCount0endTimeMillis1329460438028statussuccesscompleted1endTime20120217T13:33:58Zpropertiesexit code0command line/home/david/.pulse2-agent/data/agents/354/recipes/6979644/base/utils/testrun --verbose --asciiworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979644/basestartTimeMillis1329460421765errorCount0succeeded1testsexpectedFailures0skipped5failures0total183errors0passed178warningCount0endTimeMillis1329460438054agentubuntustatussuccesscompleted1endTime20120217T13:33:58ZstartTimeMillis1329460400613errorCount0recipewafsucceeded1progress-1namenode_v0_6-linux-wafstartTime20120217T13:33:58Zcommandsprogress-1namebootstrapstartTime20120217T13:33:58ZwarningCount0endTimeMillis1329460440377statussuccesscompleted1endTime20120217T13:34:00ZpropertiesstartTimeMillis1329460438890errorCount0succeeded1progress-1namebuildstartTime20120217T13:34:00ZwarningCount0endTimeMillis1329460461115statussuccesscompleted1endTime20120217T13:34:21Zpropertiesexit code0command line/home/david/.nvm/v0.6.9/bin/npm link --no-unicodeworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979645/basestartTimeMillis1329460440540errorCount0succeeded1progress-1nameteststartTime20120217T13:34:21ZwarningCount0endTimeMillis1329460477434statussuccesscompleted1endTime20120217T13:34:37Zpropertiesexit code0command line/home/david/.pulse2-agent/data/agents/354/recipes/6979645/base/utils/testrun --verbose --asciiworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979645/basestartTimeMillis1329460461137errorCount0succeeded1testsexpectedFailures0skipped5failures0total183errors0passed178warningCount0endTimeMillis1329460477458agentubuntustatussuccesscompleted1endTime20120217T13:34:37ZstartTimeMillis1329460438850errorCount0recipewafsucceeded1progress-1namenode_v0_4-linux-wafstartTime20120217T13:34:37Zcommandsprogress-1namebootstrapstartTime20120217T13:34:37ZwarningCount0endTimeMillis1329460479066statussuccesscompleted1endTime20120217T13:34:39ZpropertiesstartTimeMillis1329460477797errorCount0succeeded1progress-1namebuildstartTime20120217T13:34:39ZwarningCount0endTimeMillis1329460508217statussuccesscompleted1endTime20120217T13:35:08Zpropertiesexit code0command line/home/david/.nvm/v0.4.9/bin/npm link --no-unicodeworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979646/basestartTimeMillis1329460479129errorCount0succeeded1progress-1nameteststartTime20120217T13:35:08ZwarningCount0endTimeMillis1329460524421statussuccesscompleted1endTime20120217T13:35:24Zpropertiesexit code0command line/home/david/.pulse2-agent/data/agents/354/recipes/6979646/base/utils/testrun --verbose --asciiworking directory/home/david/.pulse2-agent/data/agents/354/recipes/6979646/basestartTimeMillis1329460508228errorCount0succeeded1testsexpectedFailures0skipped5failures0total183errors0passed178warningCount0endTimeMillis1329460524453agentubuntustatussuccesscompleted1endTime20120217T13:35:24ZstartTimeMillis1329460477779errorCount0recipewafsucceeded1progress-1namenode_v0_6-freebsd-wafstartTime20120217T13:32:02Zcommandsprogress-1namebootstrapstartTime20120217T13:32:02ZwarningCount0endTimeMillis1329460324009statussuccesscompleted1endTime20120217T13:32:04ZpropertiesstartTimeMillis1329460322343errorCount0succeeded1progress-1namebuildstartTime20120217T13:32:04ZwarningCount0endTimeMillis1329460338764statussuccesscompleted1endTime20120217T13:32:18Zpropertiesexit code0command line/home/david/.nvm/v0.6.10/bin/npm link --no-unicodeworking directory/home/david/.pulse2-agent/data/agents/1342/recipes/6979647/basestartTimeMillis1329460324172errorCount0succeeded1progress-1nameteststartTime20120217T13:32:18ZwarningCount0endTimeMillis1329460344660statussuccesscompleted1endTime20120217T13:32:24Zpropertiesexit code0command line/home/david/.pulse2-agent/data/agents/1342/recipes/6979647/base/utils/testrun --verbose --asciiworking directory/home/david/.pulse2-agent/data/agents/1342/recipes/6979647/basestartTimeMillis1329460338785errorCount0succeeded1testsexpectedFailures0skipped1failures0total184errors0passed183warningCount0endTimeMillis1329460344707agentfreebsdstatussuccesscompleted1endTime20120217T13:32:24ZstartTimeMillis1329460322318errorCount0recipewafsucceeded1progress-1namenode_v0_4-freebsd-wafstartTime20120217T13:32:25Zcommandsprogress-1namebootstrapstartTime20120217T13:32:25ZwarningCount0endTimeMillis1329460348170statussuccesscompleted1endTime20120217T13:32:28ZpropertiesstartTimeMillis1329460345755errorCount0succeeded1progress-1namebuildstartTime20120217T13:32:28ZwarningCount0endTimeMillis1329460383025statussuccesscompleted1endTime20120217T13:33:03Zpropertiesexit code0command line/home/david/.nvm/v0.4.9/bin/npm link --no-unicodeworking directory/home/david/.pulse2-agent/data/agents/1342/recipes/6979648/basestartTimeMillis1329460348336errorCount0succeeded1progress-1nameteststartTime20120217T13:33:03ZwarningCount0endTimeMillis1329460388712statussuccesscompleted1endTime20120217T13:33:08Zpropertiesexit code0command line/home/david/.pulse2-agent/data/agents/1342/recipes/6979648/base/utils/testrun --verbose --asciiworking directory/home/david/.pulse2-agent/data/agents/1342/recipes/6979648/basestartTimeMillis1329460383030errorCount0succeeded1testsexpectedFailures0skipped1failures0total184errors0passed183warningCount0endTimeMillis1329460388748agentfreebsdstatussuccesscompleted1endTime20120217T13:33:08ZstartTimeMillis1329460345726errorCount0recipewafsucceeded1progress-1namenode_head-win32-gypstartTime20120217T13:32:02Zcommandsprogress-1namebootstrapstartTime20120217T13:32:02ZwarningCount0endTimeMillis1329460324787statussuccesscompleted1endTime20120217T13:32:04ZpropertiesstartTimeMillis1329460322364errorCount0succeeded1progress-1namenpm installstartTime20120217T13:32:04ZwarningCount0endTimeMillis1329460330834statussuccesscompleted1endTime20120217T13:32:10Zpropertiesexit code0command line"C:\Program Files\nodejs\npm.cmd" install --no-unicodeworking directoryC:\Users\david\.pulse2-agent\data\agents\1401\recipes\6979649\basestartTimeMillis1329460324940errorCount0succeeded1progress-1namegypstartTime20120217T13:32:10ZwarningCount0endTimeMillis1329460332408statussuccesscompleted1endTime20120217T13:32:12Zpropertiesexit code0command lineC:\Python27\python.exe C:/Users/david/agnat_node/tools/gyp_addonworking directoryC:\Users\david\.pulse2-agent\data\agents\1401\recipes\6979649\basestartTimeMillis1329460330861errorCount0succeeded1progress-1namemsbuildstartTime20120217T13:32:12ZwarningCount0endTimeMillis1329460361932statussuccesscompleted1endTime20120217T13:32:41Zpropertiesbuild filedns_sd.slnexit code0command lineC:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe dns_sd.sln /property:Configuration=Release /p:TargetExt=.nodeworking directoryC:\Users\david\.pulse2-agent\data\agents\1401\recipes\6979649\baseconfigurationReleasestartTimeMillis1329460332427errorCount0succeeded1testsexpectedFailures0skipped0failures0total0errors0passed0warningCount0endTimeMillis1329460362000agentwin7statussuccesscompleted1endTime20120217T13:32:42ZstartTimeMillis1329460322342errorCount0recipegyp winsucceeded1endTimeMillis1329460524674projectmdnsversion184endTime20120217T13:35:24ZtestsexpectedFailures0skipped27failures0total1836errors0passed1809progress-1revisiond979f84891ccfe6cf101c10a37fc228efc08d045startTime20120217T13:32:02Zmaturityintegrationid184warningCount0statussuccessreasonmanual trigger by admincompleted1succeeded1startTimeMillis1329460322130errorCount0 2 | --------------------------------------------------------------------------------