diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b09524d --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz +*.swp + +pids +logs +results +tmp + +npm-debug.log +node_modules +.idea +*.iml +.DS_Store +Thumbs.db + +buildAssets diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..f925b42 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,8 @@ +0.0.3 May 13, 2014 + - Refactored to new Architecture + +0.0.2 May 5, 2014 + - Changes on the package.json + +0.0.1 May 5, 2014 + - Initial release diff --git a/README.md b/README.md new file mode 100644 index 0000000..63bf3bf --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Iugu para node.js [![Build Status](https://travis-ci.org/lspecian/iugu-node.png?branch=pos-refactor)](https://travis-ci.org/lspecian/iugu-node) + +## Instalação + +`npm install iugu` + +## Exemplo de Uso +```js +var iugu = require('iugu')('c73d49f9-6490-46ee-ba36-dcf69f6334fd'); // Ache sua chave API no Painel +// iugu.{ RESOURCE_NAME }.{ METHOD_NAME } +``` +Todo método aceita um callback opcional como ultimo argumento: + +```js +iugu.customer.create({ + 'email': 'email@email.com', + 'name': 'Nome do Cliente', + 'notes': 'Anotações Gerais' + }, function(err, customer) { + err; // null se não ocorreu nenhum erro + customer; // O objeto de retorno da criação + } +); +``` + +## Documentação +Acesse [iugu.com/documentacao](http://iugu.com/documentacao) para referência + +## Configuração + + * `iugu.setApiKey('c73d49f9-6490-46ee-ba36-dcf69f6334fd');` + * `iugu.setTimeout(20000); // in ms` (node's default: `120000ms`) + +## Testes +Execute : + +`npm test` + +## Autor + +Originalmente por [Luis Specian](https://github.com/lspecian) (luis@specian.com.br). diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..bcab45a --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.3 diff --git a/lib/Error.js b/lib/Error.js new file mode 100644 index 0000000..615a090 --- /dev/null +++ b/lib/Error.js @@ -0,0 +1,66 @@ +'use strict'; + +var utils = require('./utils'); + +module.exports = _Error; + +/** + * Generic Error class to wrap any errors returned by iugu-node + */ +function _Error(raw) { + this.populate.apply(this, arguments); +} + +// Extend Native Error +_Error.prototype = Object.create(Error.prototype); + +_Error.prototype.type = 'GenericError'; +_Error.prototype.populate = function(type, message) { + this.type = type; + this.message = message; +}; + +_Error.extend = utils.protoExtend; + +/** + * Create subclass of internal Error class + * (Specifically for errors returned from Iugu's REST API) + */ +var IuguError = _Error.IuguError = _Error.extend({ + type: 'IuguError', + populate: function(raw) { + + // Move from prototype def (so it appears in stringified obj) + this.type = this.type; + + this.rawType = raw.type; + this.code = raw.code; + this.param = raw.param; + this.message = raw.message; + this.detail = raw.detail; + this.raw = raw; + + } +}); + +/** + * Helper factory which takes raw iugu errors and outputs wrapping instances + */ +IuguError.generate = function(rawIuguError) { + switch (rawIuguError.type) { + case 'card_error': + return new _Error.IuguCardError(rawIuguError); + case 'invalid_request_error': + return new _Error.IuguInvalidRequestError(rawIuguError); + case 'api_error': + return new _Error.IuguAPIError(rawIuguError); + } + return new _Error('Generic', 'Unknown Error'); +}; + +// Specific Stripe Error types: +_Error.IuguCardError = IuguError.extend({ type: 'IuguCardError' }); +_Error.IuguInvalidRequestError = IuguError.extend({ type: 'IuguInvalidRequest' }); +_Error.IuguAPIError = IuguError.extend({ type: 'IuguAPIError' }); +_Error.IuguAuthenticationError = IuguError.extend({ type: 'IuguAuthenticationError' }); +_Error.IuguConnectionError = IuguError.extend({ type: 'IuguConnectionError' }); diff --git a/lib/IuguMethod.basic.js b/lib/IuguMethod.basic.js new file mode 100644 index 0000000..465cd89 --- /dev/null +++ b/lib/IuguMethod.basic.js @@ -0,0 +1,35 @@ +'use strict'; + +var iuguMethod = require('./IuguMethod'); +var utils = require('./utils'); + +module.exports = { + + create: iuguMethod({ + method: 'POST' + }), + + list: iuguMethod({ + method: 'GET' + }), + + retrieve: iuguMethod({ + method: 'GET', + path: '/{id}', + urlParams: ['id'] + }), + + update: iuguMethod({ + method: 'POST', + path: '{id}', + urlParams: ['id'] + }), + + // Avoid 'delete' keyword in JS + del: iuguMethod({ + method: 'DELETE', + path: '{id}', + urlParams: ['id'] + }) + +}; \ No newline at end of file diff --git a/lib/IuguMethod.js b/lib/IuguMethod.js new file mode 100644 index 0000000..fec0d08 --- /dev/null +++ b/lib/IuguMethod.js @@ -0,0 +1,66 @@ +'use strict'; + +var path = require('path'); +var utils = require('./utils'); + +/** + * Create an API method from the declared spec. + * + * @param [spec.method='GET'] Request Method (POST, GET, DELETE, PUT) + * @param [spec.path=''] Path to be appended to the API BASE_PATH, joined with + * the instance's path (e.g. "charges" or "customers") + * @param [spec.required=[]] Array of required arguments in the order that they + * must be passed by the consumer of the API. Subsequent optional arguments are + * optionally passed through a hash (Object) as the penultimate argument + * (preceeding the also-optional callback argument + */ +module.exports = function iuguMethod(spec) { + + var commandPath = utils.makeURLInterpolator( spec.path || '' ); + var requestMethod = (spec.method || 'GET').toUpperCase(); + var urlParams = spec.urlParams || []; + + return function() { + + var self = this; + var args = [].slice.call(arguments); + + var callback = typeof args[args.length - 1] == 'function' && args.pop(); + var auth = args.length > urlParams.length && utils.isAuthKey(args[args.length - 1]) ? args.pop() : null; + var data = utils.isObject(args[args.length - 1]) ? args.pop() : {}; + var urlData = this.createUrlData(); + + var deferred = this.createDeferred(callback); + + for (var i = 0, l = urlParams.length; i < l; ++i) { + var arg = args[0]; + if (urlParams[i] && !arg) { + throw new Error('Iugu: I require argument "' + urlParams[i] + '", but I got: ' + arg); + } + urlData[urlParams[i]] = args.shift(); + } + + if (args.length) { + throw new Error( + 'Iugu: Unknown arguments (' + args + '). Did you mean to pass an options object? ' + ); + } + + var requestPath = this.createFullPath(commandPath, urlData); + + self._request(requestMethod, requestPath, data, auth, function(err, response) { + if (err) { + deferred.reject(err); + } else { + deferred.resolve( + spec.transformResponseData ? + spec.transformResponseData(response) : + response + ); + } + }); + + return deferred.promise; + + }; +}; \ No newline at end of file diff --git a/lib/IuguResource.js b/lib/IuguResource.js new file mode 100644 index 0000000..c4c1a12 --- /dev/null +++ b/lib/IuguResource.js @@ -0,0 +1,213 @@ +'use strict'; + +var http = require('http'); +var https = require('https'); +var path = require('path'); +var when = require('when'); + +var utils = require('./utils'); +var Error = require('./Error'); + +var hasOwn = {}.hasOwnProperty; + +// Provide extension mechanism for Iugu Resource Sub-Classes +IuguResource.extend = utils.protoExtend; + +// Expose method-creator & prepared (basic) methods +IuguResource.method = require('./IuguMethod'); +IuguResource.BASIC_METHODS = require('./IuguMethod.basic'); + +/** + * Encapsulates request logic for a Iugu Resource + */ +function IuguResource(iugu, urlData) { + + this._iugu = iugu; + this._urlData = urlData || {}; + + this.basePath = utils.makeURLInterpolator(iugu.getApiField('basePath')); + this.path = utils.makeURLInterpolator(this.path); + + if (this.includeBasic) { + this.includeBasic.forEach(function(methodName) { + this[methodName] = IuguResource.BASIC_METHODS[methodName]; + }, this); + } + + this.initialize.apply(this, arguments); + +} + +IuguResource.prototype = { + + path: '', + + initialize: function() {}, + + createFullPath: function(commandPath, urlData) { + return path.join( + this.basePath(urlData), + this.path(urlData), + typeof commandPath == 'function' ? + commandPath(urlData) : commandPath + ).replace(/\\/g, '/'); // ugly workaround for Windows + }, + + createUrlData: function() { + var urlData = {}; + // Merge in baseData + for (var i in this._urlData) { + if (hasOwn.call(this._urlData, i)) { + urlData[i] = this._urlData[i]; + } + } + return urlData; + }, + + createDeferred: function(callback) { + var deferred = when.defer(); + + if (callback) { + // Callback, if provided, is a simply translated to Promise'esque: + // (Ensure callback is called outside of promise stack) + deferred.promise.then(function(res) { + setTimeout(function(){ callback(null, res) }, 0); + }, function(err) { + setTimeout(function(){ callback(err, null); }, 0); + }); + } + + return deferred; + }, + + _timeoutHandler: function(timeout, req, callback) { + var self = this; + return function() { + var timeoutErr = new Error('ETIMEDOUT'); + timeoutErr.code = 'ETIMEDOUT'; + + req._isAborted = true; + req.abort(); + + callback.call( + self, + new Error.IuguConnectionError({ + message: 'Request aborted due to timeout being reached (' + timeout + 'ms)', + detail: timeoutErr + }), + null + ); + } + }, + + _responseHandler: function(req, callback) { + var self = this; + return function(res) { + var response = ''; + + res.setEncoding('utf8'); + res.on('data', function(chunk) { + response += chunk; + }); + res.on('end', function() { + try { + response = JSON.parse(response); + if (response.error) { + var err; + if (res.statusCode === 401) { + err = new Error.IuguAuthenticationError(response.error); + } else { + err = Error.IuguError.generate(response.error); + } + return callback.call(self, err, null); + } + } catch (e) { + return callback.call( + self, + new Error.IuguAPIError({ + message: 'Invalid JSON received from the Iugu API', + response: response, + exception: e + }), + null + ); + } + callback.call(self, null, response); + }); + }; + }, + + _errorHandler: function(req, callback) { + var self = this; + return function(error) { + if (req._isAborted) return; // already handled + callback.call( + self, + new Error.StripeConnectionError({ + message: 'An error occurred with our connection to Iugu', + detail: error + }), + null + ); + } + }, + + _request: function(method, path, data, auth, callback) { + + var requestData = utils.stringifyRequestData(data || {}); + var self = this; + + var apiVersion = this._iugu.getApiField('version'); + var headers = { + // Use specified auth token or use default from this stripe instance: + 'Authorization': auth ? + 'Basic ' + new Buffer(auth + ':').toString('base64') : + this._iugu.getApiField('auth'), + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': requestData.length, + 'User-Agent': 'Iugu/v1 NodeBindings/' + this._iugu.getConstant('PACKAGE_VERSION') + }; + + if (apiVersion) { + headers['Iugu-Version'] = apiVersion; + } + + // Grab client-user-agent before making the request: + this._iugu.getClientUserAgent(function(cua) { + headers['X-Iugu-Client-User-Agent'] = cua; + makeRequest(); + }); + + function makeRequest() { + + var timeout = self._iugu.getApiField('timeout'); + + var req = ( + self._iugu.getApiField('protocol') == 'http' ? http : https + ).request({ + host: self._iugu.getApiField('host'), + port: self._iugu.getApiField('port'), + path: path, + method: method, + headers: headers + }); + + req.setTimeout(timeout, self._timeoutHandler(timeout, req, callback)); + req.on('response', self._responseHandler(req, callback)); + req.on('error', self._errorHandler(req, callback)); + + req.on('socket', function(socket) { + socket.on('secureConnect', function() { + req.write(requestData); + req.end(); + }); + }); + + } + + } + +}; + +module.exports = IuguResource; diff --git a/lib/iugu.js b/lib/iugu.js new file mode 100644 index 0000000..92710b1 --- /dev/null +++ b/lib/iugu.js @@ -0,0 +1,139 @@ +'use strict'; + +Iugu.DEFAULT_HOST = 'api.iugu.com'; +Iugu.DEFAULT_PORT = '443'; +Iugu.DEFAULT_BASE_PATH = '/v1/'; +Iugu.DEFAULT_API_VERSION = null; + +// Use node's default timeout: +Iugu.DEFAULT_TIMEOUT = require('http').createServer().timeout; + +Iugu.PACKAGE_VERSION = require('../package.json').version; + +Iugu.USER_AGENT = { + bindings_version: Iugu.PACKAGE_VERSION, + lang: 'node', + lang_version: process.version, + platform: process.platform, + publisher: 'iugu', + uname: null +}; + +Iugu.USER_AGENT_SERIALIZED = null; + +var exec = require('child_process').exec; + +var resources = { + Accounts: require('./resources/Accounts'), + Charge: require('./resources/Charge'), + Customers: require('./resources/Customers'), + Invoices: require('./resources/Invoices'), + MarketPlace: require('./resources/MarketPlace'), + Plans: require('./resources/Plans'), + Subscriptions: require('./resources/Subscriptions'), + PaymentToken: require('./resources/PaymentToken'), + Transfers: require('./resources/Transfers'), + + // The following rely on pre-filled customer IDs: + CustomerPaymentMethods: require('./resources/CustomerPaymentMethods'), + +}; + +Iugu.IuguResource = require('./IuguResource'); +Iugu.resources = resources; + +function Iugu(key, version) { + + if (!(this instanceof Iugu)) { + return new Iugu(key, version); + } + + this._api = { + auth: null, + host: Iugu.DEFAULT_HOST, + port: Iugu.DEFAULT_PORT, + basePath: Iugu.DEFAULT_BASE_PATH, + version: Iugu.DEFAULT_API_VERSION, + timeout: Iugu.DEFAULT_TIMEOUT, + dev: false + }; + + this._prepResources(); + this.setApiKey(key); + this.setApiVersion(version); +} + +Iugu.prototype = { + + setHost: function(host, port, protocol) { + this._setApiField('host', host); + if (port) this.setPort(port); + if (protocol) this.setProtocol(protocol); + }, + + setProtocol: function(protocol) { + this._setApiField('protocol', protocol.toLowerCase()); + }, + + setPort: function(port) { + this._setApiField('port', port); + }, + + setApiVersion: function(version) { + if (version) { + this._setApiField('version', version); + } + }, + + setApiKey: function(key) { + if (key) { + this._setApiField( + 'auth', + 'Basic ' + new Buffer(key + ':').toString('base64') + ); + } + }, + + setTimeout: function(timeout) { + this._setApiField( + 'timeout', + timeout == null ? Iugu.DEFAULT_TIMEOUT : timeout + ); + }, + + _setApiField: function(key, value) { + this._api[key] = value; + }, + + getApiField: function(key) { + return this._api[key]; + }, + + getConstant: function(c) { + return Iugu[c]; + }, + + getClientUserAgent: function(cb) { + if (Iugu.USER_AGENT_SERIALIZED) { + return cb(Iugu.USER_AGENT_SERIALIZED); + } + exec('uname -a', function(err, uname) { + Iugu.USER_AGENT.uname = uname || 'UNKNOWN'; + Iugu.USER_AGENT_SERIALIZED = JSON.stringify(Iugu.USER_AGENT); + cb(Iugu.USER_AGENT_SERIALIZED); + }); + }, + + _prepResources: function() { + + for (var name in resources) { + this[ + name[0].toLowerCase() + name.substring(1) + ] = new resources[name](this); + } + + } + +}; + +module.exports = Iugu; diff --git a/lib/resources/Accounts.js b/lib/resources/Accounts.js new file mode 100644 index 0000000..a100533 --- /dev/null +++ b/lib/resources/Accounts.js @@ -0,0 +1,17 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + + path: 'accounts', + includeBasic: ['retrieve'], + + request_verification: iuguMethod({ + method: 'POST', + path: '{accountId}/request_verification', + urlParams: ['accountId'] + }) + +}); diff --git a/lib/resources/Charge.js b/lib/resources/Charge.js new file mode 100644 index 0000000..df90aaf --- /dev/null +++ b/lib/resources/Charge.js @@ -0,0 +1,9 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + path: 'charge', + includeBasic: ['create'] +}); diff --git a/lib/resources/CustomerPaymentMethods.js b/lib/resources/CustomerPaymentMethods.js new file mode 100644 index 0000000..e2c491e --- /dev/null +++ b/lib/resources/CustomerPaymentMethods.js @@ -0,0 +1,19 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); + +/** + * CustomerPaymentMethods is a unique resource in that, upon instantiation, + * requires a customerId, and therefore each of its methods only + * require the paymentMethodId argument. + * + * This streamlines the API specifically for the case of accessing Payment Methods + * on a returned customer object. + * + * E.g. customerObject.paymentMethods.retrieve(paymentMethodId) + * (As opposed to the also-supported iugu.Customer.retrieveCard(custId, paymentMethodId)) + */ +module.exports = IuguResource.extend({ + path: 'customers/{customerId}/payment_methods', + includeBasic: ['create', 'list', 'retrieve', 'update', 'del'] +}); diff --git a/lib/resources/Customers.js b/lib/resources/Customers.js new file mode 100644 index 0000000..fbfbaca --- /dev/null +++ b/lib/resources/Customers.js @@ -0,0 +1,46 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + path: 'customers', + includeBasic: [ + 'create', 'list', 'retrieve', 'update', 'del' + ], + + /** + * Customer: PaymentMethods methods + */ + + createPaymentMethod: iuguMethod({ + method: 'POST', + path: '/{customerId}/payment_methods', + urlParams: ['customerId'] + }), + + listPaymentMethod: iuguMethod({ + method: 'GET', + path: '/{customerId}/payment_methods', + urlParams: ['customerId'] + }), + + retrievePaymentMethod: iuguMethod({ + method: 'GET', + path: '/{customerId}/payment_methods/{paymentMethodId}', + urlParams: ['customerId', 'paymentMethodId'] + }), + + updatePaymentMethod: iuguMethod({ + method: 'POST', + path: '/{customerId}/payment_methods/{paymentMethodId}', + urlParams: ['customerId', 'paymentMethodId'] + }), + + deletePaymentMethod: iuguMethod({ + method: 'DELETE', + path: '/{customerId}/payment_methods/{paymentMethodId}', + urlParams: ['customerId', 'paymentMethodId'] + }), + +}); diff --git a/lib/resources/Invoices.js b/lib/resources/Invoices.js new file mode 100644 index 0000000..c55b09d --- /dev/null +++ b/lib/resources/Invoices.js @@ -0,0 +1,23 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + + path: 'invoices', + includeBasic: ['create', 'list', 'retrieve', 'update', 'del'], + + cancel: iuguMethod({ + method: 'PUT', + path: '{invoiceId}/cancel', + urlParams: ['invoiceId'] + }), + + refund : iuguMethod({ + method: 'POST', + path: '{invoiceId}/refund', + urlParams: ['invoiceId'] + }), + +}); diff --git a/lib/resources/MarketPlace.js b/lib/resources/MarketPlace.js new file mode 100644 index 0000000..8a623ed --- /dev/null +++ b/lib/resources/MarketPlace.js @@ -0,0 +1,13 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + path: 'marketplace', + + create_account: iuguMethod({ + method: 'POST', + path: 'create_account' + }) +}); diff --git a/lib/resources/PaymentToken.js b/lib/resources/PaymentToken.js new file mode 100644 index 0000000..97eab05 --- /dev/null +++ b/lib/resources/PaymentToken.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = require('../IuguResource').extend({ + path: 'payment_token', + includeBasic: ['create'] +}); diff --git a/lib/resources/Plans.js b/lib/resources/Plans.js new file mode 100644 index 0000000..1032108 --- /dev/null +++ b/lib/resources/Plans.js @@ -0,0 +1,15 @@ +'use strict'; +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + path: 'plans', + includeBasic: ['create', 'list', 'retrieve', 'update', 'del'], + + retrieveByIdentifier: iuguMethod({ + method: 'GET', + path: 'identifier/{identifier}/', + urlParams: ['identifier'] + }) +}); + diff --git a/lib/resources/Subscriptions.js b/lib/resources/Subscriptions.js new file mode 100644 index 0000000..67dd5f5 --- /dev/null +++ b/lib/resources/Subscriptions.js @@ -0,0 +1,35 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + path: 'subscriptions', + includeBasic: ['create', 'list', 'retrieve', 'update', 'del'], + + suspend: iuguMethod({ + method: 'POST', + path: '{subscriptionId}/suspend', + urlParams: ['subscriptionId'] + }), + activate: iuguMethod({ + method: 'POST', + path: '{subscriptionId}/activate', + urlParams: ['subscriptionId'] + }), + change_plan: iuguMethod({ + method: 'POST', + path: '{subscriptionId}/change_plan/{plan_identifier}', + urlParams: ['subscriptionId', 'plan_identifier'] + }), + add_credits: iuguMethod({ + method: 'PUT', + path: '{subscriptionId}/add_credits', + urlParams: ['subscriptionId'] + }), + remove_credits: iuguMethod({ + method: 'PUT', + path: '{subscriptionId}/remove_credits', + urlParams: ['subscriptionId'] + }) +}); diff --git a/lib/resources/Transfers.js b/lib/resources/Transfers.js new file mode 100644 index 0000000..5b74ca2 --- /dev/null +++ b/lib/resources/Transfers.js @@ -0,0 +1,13 @@ +'use strict'; + +var IuguResource = require('../IuguResource'); +var iuguMethod = IuguResource.method; + +module.exports = IuguResource.extend({ + + path: 'transfers', + includeBasic: [ + 'create', 'list' + ], +}); + diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..c2f05c7 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,106 @@ +'use strict'; + +var querystring = require('querystring'); + +var hasOwn = {}.hasOwnProperty; +var toString = {}.toString; + +var ARRAY_ + +var utils = module.exports = { + + isAuthKey: function (key) { + return typeof key == 'string' && /^(?:[a-z]{2}_)?[A-z0-9]{32}$/.test(key); + }, + + isObject: function(o) { + return toString.call(o) === '[object Object]'; + }, + + /** + * Stringifies an Object, accommodating a single-level of nested objects + * (forming the conventional key "parent[child]=value") + */ + stringifyRequestData: function(data) { + + var output = []; + + for (var i in data) { + if (hasOwn.call(data, i)) { + if (utils.isObject(data[i])) { + var hasProps = false; + for (var ii in data[i]) { + if (hasOwn.call(data[i], ii)) { + hasProps = true; + output.push(encode(i + '[' + ii + ']') + '=' + encode(data[i][ii])); + } + } + if (!hasProps) { + output.push(encode(i) + '=' + encode('')); + } + } else if (Array.isArray(data[i])) { + for (var a = 0, l = data[i].length; a < l; ++a) { + output.push(encode(i + '[]') + '=' + encode(data[i][a])); + } + } else { + output.push(encode(i) + '=' + encode(data[i])); + } + } + } + + return output.join('&'); + + function encode(v) { + return v == null ? '' : encodeURIComponent(v); + } + }, + + /** + * https://gist.github.com/padolsey/6008842 + * Outputs a new function with interpolated object property values. + * Use like so: + * var fn = makeURLInterpolator('some/url/{param1}/{param2}'); + * fn({ param1: 123, param2: 456 }); // => 'some/url/123/456' + */ + makeURLInterpolator: (function() { + var rc = { + '\n': '\\n', '\"': '\\\"', + '\u2028': '\\u2028', '\u2029': '\\u2029' + }; + return function makeURLInterpolator(str) { + return new Function( + 'o', + 'return "' + ( + str + .replace(/["\n\r\u2028\u2029]/g, function($0) { + return rc[$0]; + }) + .replace(/\{([\s\S]+?)\}/g, '" + encodeURIComponent(o["$1"]) + "') + ) + '";' + ); + }; + }()), + + /** + * Provide simple "Class" extension mechanism + */ + protoExtend: function(sub) { + var Super = this; + var Constructor = hasOwn.call(sub, 'constructor') ? sub.constructor : function() { + Super.apply(this, arguments); + }; + Constructor.prototype = Object.create(Super.prototype); + for (var i in sub) { + if (hasOwn.call(sub, i)) { + Constructor.prototype[i] = sub[i]; + } + } + for (i in Super) { + if (hasOwn.call(Super, i)) { + Constructor[i] = Super[i]; + } + } + return Constructor; + } + +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0163a17 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "iugu", + "version": "0.0.3", + "description": "Iugu API wrapper", + "homepage": "https://github.com/iugu/iugu-node", + "author": { + "name": "Luis Specian", + "email": "lspecian@gmail.com", + "url": "http://specian.com.br/" + }, + "repository": { + "type": "git", + "url": "https://github.com/iugu/iugu-node.git" + }, + "bugs:": "https://github.com/iugu/iugu-node/issues", + "engines": { + "node": ">= v0.8.0" + }, + "main": "lib/iugu.js", + "devDependencies": { + "mocha": "~1.13.0", + "chai": "~1.8.0", + "chai-as-promised": "~4.0.0", + "mocha-as-promised": "~1.4.0" + }, + "dependencies": { + "when": "~3.1.0" + }, + "scripts": { + "test": "mocha" + } +} diff --git a/test/Error.spec.js b/test/Error.spec.js new file mode 100644 index 0000000..c9d627b --- /dev/null +++ b/test/Error.spec.js @@ -0,0 +1,24 @@ +'use strict'; + +require('./testUtils'); + +var Error = require('../lib/Error'); +var expect = require('chai').expect; + +describe('Error', function() { + + it('Populates with type and message params', function() { + var e = new Error('FooError', 'Foo happened'); + expect(e).to.have.property('type', 'FooError'); + expect(e).to.have.property('message', 'Foo happened'); + }); + + describe('IuguError', function() { + it('Generates specific instance depending on error-type', function() { + expect(Error.IuguError.generate({ type: 'card_error' })).to.be.instanceOf(Error.IuguCardError); + expect(Error.IuguError.generate({ type: 'invalid_request_error' })).to.be.instanceOf(Error.IuguInvalidRequestError); + expect(Error.IuguError.generate({ type: 'api_error' })).to.be.instanceOf(Error.IuguAPIError); + }); + }); + +}); \ No newline at end of file diff --git a/test/flows.spec.js b/test/flows.spec.js new file mode 100644 index 0000000..2cbcbc6 --- /dev/null +++ b/test/flows.spec.js @@ -0,0 +1,211 @@ +'use strict'; + +var testUtils = require('./testUtils'); +var chai = require('chai'); +var when = require('when'); +var iugu = require('../lib/iugu')( + testUtils.getUserIuguKey(), + 'latest' +); + +var expect = chai.expect; + +var CUSTOMER_DETAILS = { + description: 'Some customer', + card: { + number: '4242424242424242', + exp_month: 12, + exp_year: 2015 + } +}; + +describe('Flows', function() { + + // Note: These tests must be run as one so we can retrieve the + // default_currency (required in subsequent tests); + + var cleanup = new testUtils.CleanupUtility(); + this.timeout(30000); + + describe('create', function() { + + it('Sends the correct request', function() { + + + }); + + }); + + +/* + describe('Plan+Subscription flow', function() { + + it('Allows me to: Create a plan and subscribe a customer to it', function() { + + return expect( + when.join( + iugu.plans.create({ + id: 'plan' + +new Date, + amount: 1700, + currency: CURRENCY, + interval: 'month', + name: 'Gold Super Amazing Tier' + }), + iugu.customers.create(CUSTOMER_DETAILS) + ).then(function(j) { + + var plan = j[0]; + var customer = j[1]; + + cleanup.deleteCustomer(customer.id); + cleanup.deletePlan(plan.id); + + return iugu.customers.updateSubscription(customer.id, { + plan: plan.id + }); + + }) + ).to.eventually.have.property('status', 'active'); + + }); + + it('Allows me to: Create a plan and subscribe a customer to it, and update subscription (multi-subs API)', function() { + var plan; + return expect( + when.join( + iugu.plans.create({ + id: 'plan' + +new Date, + amount: 1700, + currency: CURRENCY, + interval: 'month', + name: 'Gold Super Amazing Tier' + }), + iugu.customers.create(CUSTOMER_DETAILS) + ).then(function(j) { + + plan = j[0]; + var customer = j[1]; + + cleanup.deleteCustomer(customer.id); + cleanup.deletePlan(plan.id); + + return iugu.customers.createSubscription(customer.id, { + plan: plan.id + }); + + }).then(function(subscription) { + return iugu.customers.updateSubscription(subscription.customer, subscription.id, { + plan: plan.id, quantity: '3' + }); + }).then(function(subscription) { + return [subscription.status, subscription.quantity]; + }) + ).to.eventually.deep.equal(['active', 3]); + + }); + + it('Errors when I attempt to subscribe a customer to a non-existent plan', function() { + + return expect( + iugu.customers.create(CUSTOMER_DETAILS) + .then(function(customer) { + + cleanup.deleteCustomer(customer.id); + + return iugu.customers.updateSubscription(customer.id, { + plan: 'someNonExistentPlan' + +new Date + }).then(null, function(err) { + // Resolve with the error so we can inspect it below + return err; + }); + + }) + ).to.eventually.satisfy(function(err) { + return err.type === 'StripeInvalidRequest' && + err.rawType === 'invalid_request_error'; + }); + + }); + + it('Allows me to: subscribe then cancel with `at_period_end` defined', function() { + + return expect( + when.join( + iugu.plans.create({ + id: 'plan' + +new Date, + amount: 1700, + currency: CURRENCY, + interval: 'month', + name: 'Silver Super Amazing Tier' + }), + iugu.customers.create(CUSTOMER_DETAILS) + ).then(function(j) { + + var plan = j[0]; + var customer = j[1]; + + cleanup.deleteCustomer(customer.id); + cleanup.deletePlan(plan.id); + + return iugu.customers.updateSubscription(customer.id, { + plan: plan.id + }); + + }).then(function(subscription) { + return iugu.customers.cancelSubscription(subscription.customer, { + at_period_end: true + }); + }) + ).to.eventually.have.property('cancel_at_period_end', true); + + }); + + describe('Plan name variations', function() { + [ + '34535 355453' + +new Date, + 'TEST 239291' + +new Date, + 'TEST_a-i' + +new Date, + 'foobarbazteston###etwothree' + +new Date + ].forEach(function(planID) { + it('Allows me to create and retrieve plan with ID: ' + planID, function() { + var plan; + return expect( + iugu.plans.create({ + id: planID, + amount: 1700, + currency: CURRENCY, + interval: 'month', + name: 'generic' + }).then(function() { + cleanup.deletePlan(planID); + return iugu.plans.retrieve(planID); + }) + ).to.eventually.have.property('id', planID); + }); + }); + }); + + }); + + describe('Charge', function() { + it('Allows you to create a charge', function() { + return expect( + iugu.charges.create({ + amount: 1234, + currency: CURRENCY, + card: { + number: '4000000000000002', + exp_month: 12, + exp_year: 2020, + cvc: 123 + } + }).then(null, function(error) { + return error; + }) + ).to.eventually.have.deep.property('raw.charge'); + }); + }); + + +*/ +}); diff --git a/test/iugu.spec.js b/test/iugu.spec.js new file mode 100644 index 0000000..d156f22 --- /dev/null +++ b/test/iugu.spec.js @@ -0,0 +1,92 @@ +'use strict'; + +var testUtils = require('./testUtils'); +var chai = require('chai'); +var when = require('when'); +var iugu = require('../lib/iugu')( + testUtils.getUserIuguKey(), + 'latest' +); + +var expect = chai.expect; + +var CUSTOMER_DETAILS = { + description: 'Some customer', + card: { + number: '4242424242424242', + exp_month: 12, + exp_year: 2015 + } +}; +/* +describe('Iugu Module', function() { + + var cleanup = new testUtils.CleanupUtility(); + this.timeout(20000); + + describe('ClientUserAgent', function() { + it('Should return a user-agent serialized JSON object', function() { + var d = when.defer(); + iugu.getClientUserAgent(function(c) { + d.resolve(JSON.parse(c)); + }); + return expect(d.promise).to.eventually.have.property('lang', 'node'); + }); + }); + + describe('setTimeout', function() { + it('Should define a default equal to the node default', function() { + expect(iugu.getApiField('timeout')).to.equal(require('http').createServer().timeout); + }); + it('Should allow me to set a custom timeout', function() { + iugu.setTimeout(900); + expect(iugu.getApiField('timeout')).to.equal(900); + }); + it('Should allow me to set null, to reset to the default', function() { + iugu.setTimeout(null); + expect(iugu.getApiField('timeout')).to.equal(require('http').createServer().timeout); + }); + }); + + describe('Callback support', function() { + + describe('Any given endpoint', function() { + + it('Will call a callback if successful', function(done) { + + var defer = when.defer(); + + iugu.customers.create({ + description: 'Some customer', + card: { + number: '4242424242424242', + exp_month: 12, + exp_year: 2015 + } + }, function(err, customer) { + cleanup.deleteCustomer(customer.id); + defer.resolve('Called!'); + }); + + return expect(defer.promise).to.eventually.become('Called!'); + }); + + it('Given an error the callback will receive it', function() { + + var defer = when.defer(); + + iugu.customers.createCard('nonExistentCustId', { card: {} }, function(err, customer) { + if (err) { + defer.resolve('ErrorWasPassed'); + } else { + defer.reject('NoErrorPassed'); + } + }); + + return expect(defer.promise).to.eventually.become('ErrorWasPassed') + }); + + }); + }); +}); +*/ diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..fdf51df --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,4 @@ +--bail +--recursive +--debug +--reporter spec \ No newline at end of file diff --git a/test/resources/Accounts.spec.js b/test/resources/Accounts.spec.js new file mode 100644 index 0000000..a0cbad3 --- /dev/null +++ b/test/resources/Accounts.spec.js @@ -0,0 +1,59 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('Account Resource', function() { + + describe('retrieve', function() { + + it('Sends the correct request', function() { + /* + iugu.accounts.retrieve('4bef97b6b36bc0b2c569470b6de9256e'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/accounts', + data: {} + }); + */ + }); + + }); + + describe('request verification', function() { + + it('Sends the correct request', function() { + var data_reqverification = { + 'data[price_range]': 'Subconta', + 'data[physical_products]': 'false', + 'data[business_type]': 'Serviços de Limpeza', + 'data[person_type]': 'Pessoa Física', + 'data[automatic_transfer]': 'true', + 'data[cpf]': '123.123.123-12', + 'data[name]': 'Nome da Pessoa', + 'data[address]': 'Av. Paulista 320 cj 10', + 'data[cep]': '01419-000', + 'data[city]': 'São Paulo', + 'data[state]': 'São Paulo', + 'data[telephone]': '11-91231-1234', + 'data[bank]': 'Itaú', + 'data[bank_ag]': '1234', + 'data[account_type]': 'Corrente', + 'data[bank_cc]': '11231-2', + 'files[id]': '@/home/user1/Desktop/rg.png', + 'files[cpf]': '@/home/user1/Desktop/cpf.png', + 'files[activity]': '@/home/user1/Desktop/contrato.png' + } + /* + iugu.accounts.request_verification(data_reqverification); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/account', + data: {} + }); + */ + }); + + }); + +}); \ No newline at end of file diff --git a/test/resources/Charge.spec.js b/test/resources/Charge.spec.js new file mode 100644 index 0000000..a2d008d --- /dev/null +++ b/test/resources/Charge.spec.js @@ -0,0 +1,41 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('Charge Resource', function() { + + describe('create', function() { + + it('Sends the correct request', function() { + var charge_data = { + 'email': 'test@test.com', + 'items[][description]': 'Item Um', + 'items[][quantity]': '1', + 'items[][price_cents]': '1099', + 'payer[cpf_cnpj]': '12312312312', + 'payer[name]': 'Nome do Cliente', + 'payer[phone_prefix]': '11', + 'payer[phone]': '12121212', + 'payer[email]': 'test@test.com', + 'payer[address][street]': 'Rua Tal', + 'payer[address][number]': '700', + 'payer[address][city]': 'São Paulo', + 'payer[address][state]': 'SP', + 'payer[address][country]': 'Brasil', + 'payer[address][zip_code]': '12122-000' + }; + //charge_data.token = token.id; + + iugu.charge.create(charge_data); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/charge', + data: charge_data + }); + + }); + + }); + +}); diff --git a/test/resources/CustomerPaymentMethods.spec.js b/test/resources/CustomerPaymentMethods.spec.js new file mode 100644 index 0000000..977350d --- /dev/null +++ b/test/resources/CustomerPaymentMethods.spec.js @@ -0,0 +1,99 @@ +'use strict'; + +var resources = require('../../lib/iugu').resources; +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +var CUSTOMER_TEST_ID = 'customerIdTest999'; + +// Create new CustomerCard instance with pre-filled customerId: +var customerPaymentMethod = new resources.CustomerPaymentMethods( + iugu, + { customerId: CUSTOMER_TEST_ID } +); + +// Use spy from existing resource: +customerPaymentMethod._request = iugu.customers._request; + +describe('CustomerPaymentMethod Resource', function() { +/* + describe('retrieve', function() { + + it('Sends the correct request', function() { + + customerPaymentMethod.retrieve('cardIdFoo456'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/' + CUSTOMER_TEST_ID + '/cards/cardIdFoo456', + data: {} + }); + + }); + + }); + + describe('create', function() { + + it('Sends the correct request', function() { + + customerPaymentMethod.create({ + number: '123456', exp_month: '12' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers/' + CUSTOMER_TEST_ID + '/cards', + data: { number: '123456', exp_month: '12' } + }); + + }); + + }); + + describe('update', function() { + + it('Sends the correct request', function() { + + customerPaymentMethod.update('cardIdFoo456', { + name: 'Bob M. Baz' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers/' + CUSTOMER_TEST_ID + '/cards/cardIdFoo456', + data: { name: 'Bob M. Baz' } + }); + + }); + + }); + + describe('del', function() { + + it('Sends the correct request', function() { + + customerPaymentMethod.del('cardIdFoo456'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'DELETE', + url: '/v1/customers/' + CUSTOMER_TEST_ID + '/cards/cardIdFoo456', + data: {} + }); + + }); + + }); + + describe('list', function() { + + it('Sends the correct request', function() { + + customerPaymentMethod.list(); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/' + CUSTOMER_TEST_ID + '/cards', + data: {} + }); + + }); + + }); +*/ +}); diff --git a/test/resources/Customers.spec.js b/test/resources/Customers.spec.js new file mode 100644 index 0000000..48d82a2 --- /dev/null +++ b/test/resources/Customers.spec.js @@ -0,0 +1,257 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; +var when = require('when'); + +var TEST_AUTH_KEY = 'aGN0bIwXnHdw5645VABjPdSn8nWY7G11'; + +describe('Customers Resource', function() { + + describe('retrieve', function() { + + it('Sends the correct request', function() { + + iugu.customers.retrieve('cus_2dkAb792h1mfa4'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/cus_2dkAb792h1mfa4', + data: {} + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.retrieve('cus_2dkAb792h1mfa4', TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/cus_2dkAb792h1mfa4', + data: {}, + auth: TEST_AUTH_KEY + }); + + }); + + }); + + describe('create', function() { + + it('Sends the correct request', function() { + + iugu.customers.create({ description: 'Some customer' }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers', + data: { description: 'Some customer' } + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.create({ description: 'Some customer' }, TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers', + data: { description: 'Some customer' }, + auth: TEST_AUTH_KEY + }); + + }); + + }); + + describe('update', function() { + + it('Sends the correct request', function() { + + iugu.customers.update('cus_2dkAb792h1mfa4', { + description: 'Foo "baz"' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers/cus_2dkAb792h1mfa4', + data: { description: 'Foo "baz"' } + }); + + }); + + }); + + describe('del', function() { + + it('Sends the correct request', function() { + + iugu.customers.del('cus_2dkAb792h1mfa4'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'DELETE', + url: '/v1/customers/cus_2dkAb792h1mfa4', + data: {} + }); + + }); + + }); + + describe('list', function() { + + it('Sends the correct request', function() { + + iugu.customers.list(); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers', + data: {} + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.list(TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers', + data: {}, + auth: TEST_AUTH_KEY + }); + + }); + + }); +/* + describe('Payment methods', function() { + + describe('retrieveCard', function() { + + it('Sends the correct request', function() { + + iugu.customers.retrieveCard('customerIdFoo321', 'cardIdFoo456'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/customerIdFoo321/cards/cardIdFoo456', + data: {} + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.retrieveCard('customerIdFoo321', 'cardIdFoo456', TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/customerIdFoo321/cards/cardIdFoo456', + data: {}, + auth: TEST_AUTH_KEY + }); + + }); + + }); + + describe('createCard', function() { + + it('Sends the correct request', function() { + + iugu.customers.createCard('customerIdFoo321', { + number: '123456', exp_month: '12' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers/customerIdFoo321/cards', + data: { number: '123456', exp_month: '12' } + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.createCard('customerIdFoo321', { + number: '123456', exp_month: '12' + }, TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers/customerIdFoo321/cards', + data: { number: '123456', exp_month: '12' }, + auth: TEST_AUTH_KEY + }); + + }); + + }); + + describe('updateCard', function() { + + it('Sends the correct request', function() { + + iugu.customers.updateCard('customerIdFoo321', 'cardIdFoo456', { + name: 'Bob M. Baz' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/customers/customerIdFoo321/cards/cardIdFoo456', + data: { name: 'Bob M. Baz' } + }); + + }); + + }); + + describe('deleteCard', function() { + + it('Sends the correct request', function() { + + iugu.customers.deleteCard('customerIdFoo321', 'cardIdFoo456'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'DELETE', + url: '/v1/customers/customerIdFoo321/cards/cardIdFoo456', + data: {} + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.deleteCard('customerIdFoo321', 'cardIdFoo456', TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'DELETE', + url: '/v1/customers/customerIdFoo321/cards/cardIdFoo456', + data: {}, + auth: TEST_AUTH_KEY + }); + + }); + + }); + + describe('listCards', function() { + + it('Sends the correct request', function() { + + iugu.customers.listCards('customerIdFoo321'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/customerIdFoo321/cards', + data: {} + }); + + }); + + it('Sends the correct request [with specified auth]', function() { + + iugu.customers.listCards('customerIdFoo321', TEST_AUTH_KEY); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/customers/customerIdFoo321/cards', + data: {}, + auth: TEST_AUTH_KEY + }); + + }); + + }); + + }); +*/ +}); \ No newline at end of file diff --git a/test/resources/Invoices.spec.js b/test/resources/Invoices.spec.js new file mode 100644 index 0000000..ae50f2a --- /dev/null +++ b/test/resources/Invoices.spec.js @@ -0,0 +1,68 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('Invoices Resource', function() { + + describe('retrieve', function() { + + it('Sends the correct request', function() { + + iugu.invoices.retrieve('invoiceId1'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/invoices/invoiceId1', + data: {} + }); + + }); + + }); + + describe('create', function() { + + it('Sends the correct request', function() { + + iugu.invoices.create({ application_fee: 111 }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/invoices', + data: { application_fee: 111 } + }); + + }); + + }); + + describe('update', function() { + + it('Sends the correct request', function() { + + iugu.invoices.update('invoiceId1', { application_fee: 200 }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/invoices/invoiceId1', + data: { application_fee: 200 } + }); + + }); + + }); + + describe('list', function() { + + it('Sends the correct request', function() { + + iugu.invoices.list({ count: 25 }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/invoices', + data: { count: 25 } + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/test/resources/MarketPlace.spec.js b/test/resources/MarketPlace.spec.js new file mode 100644 index 0000000..dc987cd --- /dev/null +++ b/test/resources/MarketPlace.spec.js @@ -0,0 +1,27 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('MarketPlace Resource', function() { + + describe('create', function() { + + it('Sends the correct request', function() { + var subconta_data = { + 'name': 'Subconta', + 'commission_percent': '10' + } + + iugu.marketPlace.create_account(subconta_data); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/marketplace/create_account', + data: subconta_data + }); + + }); + + }); + +}); diff --git a/test/resources/PaymentToken.spec.js b/test/resources/PaymentToken.spec.js new file mode 100644 index 0000000..f143bfb --- /dev/null +++ b/test/resources/PaymentToken.spec.js @@ -0,0 +1,40 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('PaymentToken Resource', function() { + + describe('create', function() { + + it('Sends the correct request', function() { + + iugu.paymentToken.create({ + card: { number: 123 } + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/payment_token', + data: { card: { number: 123 } } + }); + + }); + + }); + + describe('retrieve', function() { + + it('Sends the correct request', function() { + /* + iugu.token.retrieve('tokenId1'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/tokens/tokenId1', + data: {} + }); + */ + }); + + }); + +}); diff --git a/test/resources/Plans.spec.js b/test/resources/Plans.spec.js new file mode 100644 index 0000000..f1e5299 --- /dev/null +++ b/test/resources/Plans.spec.js @@ -0,0 +1,87 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('Plans Resource', function() { + + describe('retrieve', function() { + + it('Sends the correct request', function() { + + iugu.plans.retrieve('planId1'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/plans/planId1', + data: {} + }); + + }); + + }); + + describe('create', function() { + + it('Sends the correct request', function() { + + iugu.plans.create({ + amount: 200, currency: 'usd' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/plans', + data: { amount: 200, currency: 'usd' } + }); + + }); + + }); + + describe('update', function() { + + it('Sends the correct request', function() { + + iugu.plans.update('planId3', { + amount: 1900, currency: 'usd' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/plans/planId3', + data: { amount: 1900, currency: 'usd' } + }); + + }); + + }); + + describe('del', function() { + + it('Sends the correct request', function() { + + iugu.plans.del('planId4'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'DELETE', + url: '/v1/plans/planId4', + data: {} + }); + + }); + + }); + + describe('list', function() { + + it('Sends the correct request', function() { + + iugu.plans.list(); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/plans', + data: {} + }); + + }); + + }); + +}); diff --git a/test/resources/Subscriptions.spec.js b/test/resources/Subscriptions.spec.js new file mode 100644 index 0000000..bee6452 --- /dev/null +++ b/test/resources/Subscriptions.spec.js @@ -0,0 +1,87 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('Subscriptions Resource', function() { + + describe('retrieve', function() { + + it('Sends the correct request', function() { + + iugu.subscriptions.retrieve('planId1'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/subscriptions/planId1', + data: {} + }); + + }); + + }); + + describe('create', function() { + + it('Sends the correct request', function() { + + iugu.subscriptions.create({ + amount: 200, currency: 'usd' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/subscriptions', + data: { amount: 200, currency: 'usd' } + }); + + }); + + }); + + describe('update', function() { + + it('Sends the correct request', function() { + + iugu.subscriptions.update('planId3', { + amount: 1900, currency: 'usd' + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/subscriptions/planId3', + data: { amount: 1900, currency: 'usd' } + }); + + }); + + }); + + describe('del', function() { + + it('Sends the correct request', function() { + + iugu.subscriptions.del('planId4'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'DELETE', + url: '/v1/subscriptions/planId4', + data: {} + }); + + }); + + }); + + describe('list', function() { + + it('Sends the correct request', function() { + + iugu.subscriptions.list(); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/subscriptions', + data: {} + }); + + }); + + }); + +}); diff --git a/test/resources/Transfers.spec.js b/test/resources/Transfers.spec.js new file mode 100644 index 0000000..05fe347 --- /dev/null +++ b/test/resources/Transfers.spec.js @@ -0,0 +1,55 @@ +'use strict'; + +var iugu = require('../testUtils').getSpyableIugu(); +var expect = require('chai').expect; + +describe('Transfers Resource', function() { + + describe('retrieve', function() { + + it('Sends the correct request', function() { + /* + iugu.transfers.retrieve('transferId1'); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/transfers/transferId1', + data: {} + }); + */ + }); + + }); + + describe('create', function() { + + it('Sends the correct request', function() { + + iugu.transfers.create({ + amount: 200, currency: 'usd', recipient: {} + }); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'POST', + url: '/v1/transfers', + data: { amount: 200, currency: 'usd', recipient: {} } + }); + + }); + + }); + + describe('list', function() { + + it('Sends the correct request', function() { + + iugu.transfers.list(); + expect(iugu.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v1/transfers', + data: {} + }); + + }); + + }); + +}); diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..181b350 --- /dev/null +++ b/test/test.js @@ -0,0 +1,47 @@ +'use strict'; + +var iugu = require('../lib/iugu')( + '4bef97b6b36bc0b2c569470b6de9256e', + 'latest' +); +/* +console.log('test.js'); +iugu.token.create( + { + 'account_id': '0738d949-27cf-493b-b079-abf045d13e12', + 'test': true, + 'method': 'credit_card', + 'data[number]': '4111111111111111', + 'data[verification_value]':'123', + 'data[first_name]': 'Joao', + 'data[last_name]': 'Silva', + 'data[month]': '12', + 'data[year]': '2014' + }, function(err, token) { + console.log(err); + console.log(token); + var data = { + 'email': 'test@test.com', + 'items[][description]': 'Item Um', + 'items[][quantity]': '1', + 'items[][price_cents]': '1099', + 'payer[cpf_cnpj]': '12312312312', + 'payer[name]': 'Nome do Cliente', + 'payer[phone_prefix]': '11', + 'payer[phone]': '12121212', + 'payer[email]': 'test@test.com', + 'payer[address][street]': 'Rua Tal', + 'payer[address][number]': '700', + 'payer[address][city]': 'São Paulo', + 'payer[address][state]': 'SP', + 'payer[address][country]': 'Brasil', + 'payer[address][zip_code]': '12122-000' + }; + data.token = token.id; + iugu.charge.create(data, function(err, res) { + console.log(err); + console.log(res); + }); +}); + +*/ \ No newline at end of file diff --git a/test/testUtils.js b/test/testUtils.js new file mode 100644 index 0000000..d53e27a --- /dev/null +++ b/test/testUtils.js @@ -0,0 +1,130 @@ +'use strict'; + +// NOTE: testUtils should be require'd before anything else in each spec file! + +// Ensure we are using the 'as promised' libs before any tests are run: +require('mocha-as-promised')(); +require('chai').use(require('chai-as-promised')); + +var when = require('when'); + +var utils = module.exports = { + + getUserIuguKey: function() { + var key = process.env.IUGU_TEST_API_KEY || '4bef97b6b36bc0b2c569470b6de9256e'; + + return key; + }, + + getSpyableIugu: function() { + // Provide a testable stripe instance + // That is, with mock-requests built in and hookable + + var Iugu = require('../lib/iugu'); + var iuguInstance = Iugu('fakeAuthToken'); + + iuguInstance.REQUESTS = []; + + for (var i in iuguInstance) { + if (iuguInstance[i] instanceof Iugu.IuguResource) { + + // Override each _request method so we can make the params + // avaialable to consuming tests (revealing requests made on + // REQUESTS and LAST_REQUEST): + iuguInstance[i]._request = function(method, url, data, auth, cb) { + var req = iuguInstance.LAST_REQUEST = { + method: method, + url: url, + data: data + }; + if (auth) req.auth = auth; + iuguInstance.REQUESTS.push(req); + cb.call(this, null, {}); + }; + + } + } + + return iuguInstance; + + }, + + /** + * A utility where cleanup functions can be registered to be called post-spec. + * CleanupUtility will automatically register on the mocha afterEach hook, + * ensuring its called after each descendent-describe block. + */ + CleanupUtility: (function() { + + CleanupUtility.DEFAULT_TIMEOUT = 20000; + + function CleanupUtility(timeout) { + var self = this; + this._cleanupFns = []; + this._iugu = require('../lib/iugu')( + utils.getUserIuguKey(), + 'latest' + ); + afterEach(function(done) { + this.timeout(timeout || CleanupUtility.DEFAULT_TIMEOUT); + return self.doCleanup(done); + }); + } + + CleanupUtility.prototype = { + + doCleanup: function(done) { + var cleanups = this._cleanupFns; + var total = cleanups.length; + var completed = 0; + for (var fn; fn = cleanups.shift();) { + var promise = fn.call(this); + if (!promise || !promise.then) { + throw new Error('CleanupUtility expects cleanup functions to return promises!'); + } + promise.then(function() { + // cleanup successful + ++completed; + if (completed === total) { + done(); + } + }, function(err) { + // not successful + throw err; + }); + } + if (total === 0) done(); + }, + add: function(fn) { + this._cleanupFns.push(fn); + }, + deleteCustomer: function(custId) { + this.add(function() { + return this._iugu.customers.del(custId); + }); + }, + deletePlan: function(pId) { + this.add(function() { + return this._iugu.plans.del(pId); + }); + }, + deleteCoupon: function(cId) { + this.add(function() { + return this._iugu.coupons.del(cId); + }); + }, + deleteInvoiceItem: function(iiId) { + this.add(function() { + return this._iugu.invoiceItems.del(iiId); + }); + } + }; + + return CleanupUtility; + + }()) + +}; + + + diff --git a/test/utils.spec.js b/test/utils.spec.js new file mode 100644 index 0000000..da56510 --- /dev/null +++ b/test/utils.spec.js @@ -0,0 +1,75 @@ +'use strict'; + +require('./testUtils'); + +var utils = require('../lib/utils'); +var expect = require('chai').expect; + +describe('utils', function() { + + describe('makeURLInterpolator', function() { + it('Interpolates values into a prepared template', function() { + var template = utils.makeURLInterpolator('/some/url/{foo}/{baz}?ok=1'); + + expect( + template({foo: 1, baz: 2}) + ).to.equal('/some/url/1/2?ok=1'); + + expect( + template({foo: '', baz: ''}) + ).to.equal('/some/url//?ok=1'); + + expect( + // Test encoding: + template({foo: 'FOO', baz: '__::baz::__'}) + ).to.equal('/some/url/FOO/__%3A%3Abaz%3A%3A__?ok=1'); + + }); + }); + + describe('stringifyRequestData', function() { + it('Creates a string from an object, handling shallow nested objects', function() { + expect(utils.stringifyRequestData({ + test: 1, + foo: 'baz', + somethingElse: '::""%&', + nested: { + 1: 2, + 'a n o t h e r': null + }, + arr: [1, 2, 3] + })).to.equal([ + 'test=1', + 'foo=baz', + 'somethingElse=%3A%3A%22%22%25%26', + 'nested%5B1%5D=2', // Unencoded: nested[1]=2 + 'nested%5Ba%20n%20o%20t%20h%20e%20r%5D=', + 'arr%5B%5D=1', + 'arr%5B%5D=2', + 'arr%5B%5D=3' + ].join('&')); + }); + it('Ensures empty objects are represented', function() { + expect(utils.stringifyRequestData({ + test: {} + })).to.equal('test='); + }); + }); + + describe('protoExtend', function() { + it('Provides an extension mechanism', function() { + function A() {} + A.extend = utils.protoExtend; + var B = A.extend({ + constructor: function() { + this.called = true; + } + }); + expect(new B()).to.be.an.instanceof(A); + expect(new B()).to.be.an.instanceof(B); + expect(new B().called).to.equal(true); + expect(B.extend === utils.protoExtend).to.equal(true); + }); + }); + +});