diff --git a/services/web/app/src/Features/Subscription/RecurlyWrapper.js b/services/web/app/src/Features/Subscription/RecurlyWrapper.js index 830b3a047e..ebd8e64d43 100644 --- a/services/web/app/src/Features/Subscription/RecurlyWrapper.js +++ b/services/web/app/src/Features/Subscription/RecurlyWrapper.js @@ -3,12 +3,11 @@ const request = require('request') const Settings = require('@overleaf/settings') const xml2js = require('xml2js') const logger = require('@overleaf/logger') -const Async = require('async') const Errors = require('../Errors/Errors') const SubscriptionErrors = require('./Errors') -const { promisify } = require('util') +const { callbackify } = require('@overleaf/promise-utils') -function updateAccountEmailAddress(accountId, newEmail, callback) { +async function updateAccountEmailAddress(accountId, newEmail) { const data = { email: newEmail, } @@ -16,93 +15,76 @@ function updateAccountEmailAddress(accountId, newEmail, callback) { try { requestBody = RecurlyWrapper._buildXml('account', data) } catch (error) { - return callback( - OError.tag(error, 'error building xml', { accountId, newEmail }) - ) + throw OError.tag(error, 'error building xml', { accountId, newEmail }) } - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}`, - method: 'PUT', - body: requestBody, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseAccountXml(body, callback) - } - ) + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}`, + method: 'PUT', + body: requestBody, + }) + return await RecurlyWrapper.promises._parseAccountXml(body) } -const RecurlyWrapper = { - apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2', - +const promises = { _paypal: { - checkAccountExists(cache, next) { + async checkAccountExists(cache) { const { user } = cache logger.debug( { userId: user._id }, 'checking if recurly account exists for user' ) - RecurlyWrapper.apiRequest( - { + let response, body + try { + ;({ response, body } = await RecurlyWrapper.promises.apiRequest({ url: `accounts/${user._id}`, method: 'GET', expect404: true, - }, - function (error, response, responseBody) { - if (error) { - OError.tag( - error, - 'error response from recurly while checking account', - { - user_id: user._id, - } - ) - return next(error) + })) + } catch (error) { + OError.tag( + error, + 'error response from recurly while checking account', + { + user_id: user._id, } - if (response.statusCode === 404) { - // actually not an error in this case, just no existing account - logger.debug( - { userId: user._id }, - 'user does not currently exist in recurly, proceed' - ) - cache.userExists = false - return next(null, cache) - } - logger.debug({ userId: user._id }, 'user appears to exist in recurly') - RecurlyWrapper._parseAccountXml( - responseBody, - function (err, account) { - if (err) { - OError.tag(err, 'error parsing account', { - user_id: user._id, - }) - return next(err) - } - cache.userExists = true - cache.account = account - next(null, cache) - } - ) - } - ) + ) + throw error + } + if (response.statusCode === 404) { + // actually not an error in this case, just no existing account + logger.debug( + { userId: user._id }, + 'user does not currently exist in recurly, proceed' + ) + cache.userExists = false + return cache + } + logger.debug({ userId: user._id }, 'user appears to exist in recurly') + try { + const account = await RecurlyWrapper.promises._parseAccountXml(body) + cache.userExists = true + cache.account = account + return cache + } catch (err) { + OError.tag(err, 'error parsing account', { + user_id: user._id, + }) + throw err + } }, - createAccount(cache, next) { + async createAccount(cache) { const { user } = cache const { subscriptionDetails } = cache if (cache.userExists) { - return next(null, cache) + return cache } - let address - try { - address = getAddressFromSubscriptionDetails(subscriptionDetails, false) - } catch (error) { - return next(error) - } + const address = getAddressFromSubscriptionDetails( + subscriptionDetails, + false + ) + const data = { account_code: user._id, email: user.email, @@ -114,97 +96,78 @@ const RecurlyWrapper = { try { requestBody = RecurlyWrapper._buildXml('account', data) } catch (error) { - return next( - OError.tag(error, 'error building xml', { user_id: user._id }) - ) + throw OError.tag(error, 'error building xml', { user_id: user._id }) } - RecurlyWrapper.apiRequest( - { + let body + try { + ;({ body } = await RecurlyWrapper.promises.apiRequest({ url: 'accounts', method: 'POST', body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - OError.tag( - error, - 'error response from recurly while creating account', - { - user_id: user._id, - } - ) - return next(error) - } - RecurlyWrapper._parseAccountXml( - responseBody, - function (err, account) { - if (err) { - OError.tag(err, 'error creating account', { - user_id: user._id, - }) - return next(err) - } - cache.account = account - next(null, cache) - } - ) - } - ) + })) + } catch (error) { + OError.tag( + error, + 'error response from recurly while creating account', + { user_id: user._id } + ) + throw error + } + try { + cache.account = await RecurlyWrapper.promises._parseAccountXml(body) + return cache + } catch (err) { + OError.tag(err, 'error creating account', { + user_id: user._id, + }) + throw err + } }, - createBillingInfo(cache, next) { + async createBillingInfo(cache) { const { user } = cache const { recurlyTokenIds } = cache logger.debug({ userId: user._id }, 'creating billing info in recurly') const accountCode = cache?.account?.account_code if (!accountCode) { - return next(new Error('no account code at createBillingInfo stage')) + throw new Error('no account code at createBillingInfo stage') } const data = { token_id: recurlyTokenIds.billing } let requestBody try { requestBody = RecurlyWrapper._buildXml('billing_info', data) } catch (error) { - return next( - OError.tag(error, 'error building xml', { user_id: user._id }) - ) + throw OError.tag(error, 'error building xml', { user_id: user._id }) } - RecurlyWrapper.apiRequest( - { + let body + try { + ;({ body } = await RecurlyWrapper.promises.apiRequest({ url: `accounts/${accountCode}/billing_info`, method: 'POST', body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - OError.tag( - error, - 'error response from recurly while creating billing info', - { - user_id: user._id, - } - ) - return next(error) - } - RecurlyWrapper._parseBillingInfoXml( - responseBody, - function (err, billingInfo) { - if (err) { - OError.tag(err, 'error creating billing info', { - user_id: user._id, - accountCode, - }) - return next(err) - } - cache.billingInfo = billingInfo - next(null, cache) - } - ) - } - ) + })) + } catch (error) { + OError.tag( + error, + 'error response from recurly while creating billing info', + { user_id: user._id } + ) + throw error + } + try { + cache.billingInfo = + await RecurlyWrapper.promises._parseBillingInfoXml(body) + return cache + } catch (err) { + OError.tag(err, 'error creating billing info', { + user_id: user._id, + accountCode, + }) + throw err + } }, - setAddressAndCompanyBillingInfo(cache, next) { + async setAddressAndCompanyBillingInfo(cache) { const { user } = cache const { subscriptionDetails } = cache logger.debug( @@ -213,20 +176,15 @@ const RecurlyWrapper = { ) const accountCode = cache?.account?.account_code if (!accountCode) { - return next( - new Error('no account code at setAddressAndCompanyBillingInfo stage') + throw new Error( + 'no account code at setAddressAndCompanyBillingInfo stage' ) } - let addressAndCompanyBillingInfo - try { - addressAndCompanyBillingInfo = getAddressFromSubscriptionDetails( - subscriptionDetails, - true - ) - } catch (error) { - return next(error) - } + const addressAndCompanyBillingInfo = getAddressFromSubscriptionDetails( + subscriptionDetails, + true + ) let requestBody try { @@ -235,45 +193,36 @@ const RecurlyWrapper = { addressAndCompanyBillingInfo ) } catch (error) { - return next( - OError.tag(error, 'error building xml', { user_id: user._id }) - ) + throw OError.tag(error, 'error building xml', { user_id: user._id }) } - RecurlyWrapper.apiRequest( - { + let body + try { + ;({ body } = await RecurlyWrapper.promises.apiRequest({ url: `accounts/${accountCode}/billing_info`, method: 'PUT', body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - OError.tag( - error, - 'error response from recurly while setting address', - { - user_id: user._id, - } - ) - return next(error) - } - RecurlyWrapper._parseBillingInfoXml( - responseBody, - function (err, billingInfo) { - if (err) { - OError.tag(err, 'error updating billing info', { - user_id: user._id, - }) - return next(err) - } - cache.billingInfo = billingInfo - next(null, cache) - } - ) + })) + } catch (error) { + OError.tag(error, 'error response from recurly while setting address', { + user_id: user._id, + }) + throw error + } + try { + cache.billingInfo = + await RecurlyWrapper.promises._parseBillingInfoXml(body) + return cache + } catch (err) { + if (err) { + OError.tag(err, 'error updating billing info', { + user_id: user._id, + }) + throw err } - ) + } }, - createSubscription(cache, next) { + async createSubscription(cache) { const { user } = cache const { subscriptionDetails } = cache logger.debug({ userId: user._id }, 'creating subscription in recurly') @@ -294,96 +243,80 @@ const RecurlyWrapper = { try { requestBody = RecurlyWrapper._buildXml('subscription', data) } catch (error) { - return next( - OError.tag(error, 'error building xml', { user_id: user._id }) - ) + throw OError.tag(error, 'error building xml', { user_id: user._id }) } - RecurlyWrapper.apiRequest( - { + let body + try { + ;({ body } = await RecurlyWrapper.promises.apiRequest({ url: 'subscriptions', method: 'POST', body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - OError.tag( - error, - 'error response from recurly while creating subscription', - { - user_id: user._id, - } - ) - return next(error) - } - RecurlyWrapper._parseSubscriptionXml( - responseBody, - function (err, subscription) { - if (err) { - OError.tag(err, 'error creating subscription', { - user_id: user._id, - }) - return next(err) - } - cache.subscription = subscription - next(null, cache) - } - ) - } - ) + })) + } catch (error) { + OError.tag( + error, + 'error response from recurly while creating subscription', + { user_id: user._id } + ) + throw error + } + try { + cache.subscription = + await RecurlyWrapper.promises._parseSubscriptionXml(body) + return cache + } catch (err) { + OError.tag(err, 'error creating subscription', { + user_id: user._id, + }) + throw err + } }, }, - _createPaypalSubscription( - user, - subscriptionDetails, - recurlyTokenIds, - callback - ) { + async _createPaypalSubscription(user, subscriptionDetails, recurlyTokenIds) { logger.debug( { userId: user._id }, 'starting process of creating paypal subscription' ) - // We use `async.waterfall` to run each of these actions in sequence + // We use waterfall through each of these actions in sequence // passing a `cache` object along the way. The cache is initialized // with required data, and `async.apply` to pass the cache to the first function const cache = { user, recurlyTokenIds, subscriptionDetails } - Async.waterfall( - [ - Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache), - RecurlyWrapper._paypal.createAccount, - RecurlyWrapper._paypal.createBillingInfo, - RecurlyWrapper._paypal.setAddressAndCompanyBillingInfo, - RecurlyWrapper._paypal.createSubscription, - ], - function (err, result) { - if (err) { - OError.tag(err, 'error in paypal subscription creation process', { - user_id: user._id, - }) - return callback(err) - } - if (!result.subscription) { - err = new Error('no subscription object in result') - OError.tag(err, 'error in paypal subscription creation process', { - user_id: user._id, - }) - return callback(err) - } - logger.debug( - { userId: user._id }, - 'done creating paypal subscription for user' + let result + try { + result = await RecurlyWrapper.promises._paypal.checkAccountExists(cache) + result = await RecurlyWrapper.promises._paypal.createAccount(result) + result = await RecurlyWrapper.promises._paypal.createBillingInfo(result) + result = + await RecurlyWrapper.promises._paypal.setAddressAndCompanyBillingInfo( + result ) - callback(null, result.subscription) - } + result = await RecurlyWrapper.promises._paypal.createSubscription(result) + } catch (err) { + OError.tag(err, 'error in paypal subscription creation process', { + user_id: user._id, + }) + throw err + } + if (!result.subscription) { + const err = new Error('no subscription object in result') + OError.tag(err, 'error in paypal subscription creation process', { + user_id: user._id, + }) + throw err + } + logger.debug( + { userId: user._id }, + 'done creating paypal subscription for user' ) + return result.subscription }, - _createCreditCardSubscription( + async _createCreditCardSubscription( user, subscriptionDetails, - recurlyTokenIds, - callback + recurlyTokenIds ) { const data = { plan_code: subscriptionDetails.plan_code, @@ -412,45 +345,40 @@ const RecurlyWrapper = { try { requestBody = RecurlyWrapper._buildXml('subscription', data) } catch (error) { - return callback( - OError.tag(error, 'error building xml', { user_id: user._id }) - ) + throw OError.tag(error, 'error building xml', { user_id: user._id }) } - RecurlyWrapper.apiRequest( - { - url: 'subscriptions', - method: 'POST', - body: requestBody, - expect422: true, - }, - (error, response, responseBody) => { - if (error) { - return callback(error) - } + const { response, body } = await RecurlyWrapper.promises.apiRequest({ + url: 'subscriptions', + method: 'POST', + body: requestBody, + expect422: true, + }) - if (response.statusCode === 422) { - RecurlyWrapper._handle422Response(responseBody, callback) - } else { - RecurlyWrapper._parseSubscriptionXml(responseBody, callback) - } - } - ) + if (response.statusCode === 422) { + return await RecurlyWrapper.promises._handle422Response(body) + } else { + return await RecurlyWrapper.promises._parseSubscriptionXml(body) + } }, - createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) { + async createSubscription(user, subscriptionDetails, recurlyTokenIds) { const { isPaypal } = subscriptionDetails logger.debug( { userId: user._id, isPaypal }, 'setting up subscription in recurly' ) const fn = isPaypal - ? RecurlyWrapper._createPaypalSubscription - : RecurlyWrapper._createCreditCardSubscription - return fn(user, subscriptionDetails, recurlyTokenIds, callback) + ? RecurlyWrapper.promises._createPaypalSubscription + : RecurlyWrapper.promises._createCreditCardSubscription + return fn(user, subscriptionDetails, recurlyTokenIds) }, - apiRequest(options, callback) { + /** + * @param options - the options to pass to the request library + * @returns {Promise<{ response: unknown, body: string}>} + */ + apiRequest(options) { options.url = RecurlyWrapper.apiUrl + '/' + options.url options.headers = { Authorization: `Basic ${Buffer.from( @@ -463,55 +391,48 @@ const RecurlyWrapper = { const { expect404, expect422 } = options delete options.expect404 delete options.expect422 - request(options, function (error, response, body) { - if ( - !error && - response.statusCode !== 200 && - response.statusCode !== 201 && - response.statusCode !== 204 && - (response.statusCode !== 404 || !expect404) && - (response.statusCode !== 422 || !expect422) - ) { - if (options.headers.Authorization) { - options.headers.Authorization = 'REDACTED' + return new Promise((resolve, reject) => { + request(options, function (error, response, body) { + if ( + !error && + response.statusCode !== 200 && + response.statusCode !== 201 && + response.statusCode !== 204 && + (response.statusCode !== 404 || !expect404) && + (response.statusCode !== 422 || !expect422) + ) { + if (options.headers.Authorization) { + options.headers.Authorization = 'REDACTED' + } + logger.warn( + { + err: error, + body, + options, + statusCode: response ? response.statusCode : undefined, + }, + 'error returned from recurly' + ) + error = new OError( + `Recurly API returned with status code: ${response.statusCode}`, + { statusCode: response.statusCode } + ) + reject(error) } - logger.warn( - { - err: error, - body, - options, - statusCode: response ? response.statusCode : undefined, - }, - 'error returned from recurly' - ) - error = new OError( - `Recurly API returned with status code: ${response.statusCode}`, - { statusCode: response.statusCode } - ) - } - callback(error, response, body) + resolve({ response, body }) + }) }) }, - getSubscriptions(accountId, callback) { - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}/subscriptions`, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseXml(body, callback) - } - ) + async getSubscriptions(accountId) { + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}/subscriptions`, + }) + return await RecurlyWrapper.promises._parseXml(body) }, - getSubscription(subscriptionId, options, callback) { + async getSubscription(subscriptionId, options) { let url - if (!callback) { - callback = options - } if (!options) { options = {} } @@ -522,54 +443,34 @@ const RecurlyWrapper = { url = `subscriptions/${subscriptionId}` } - RecurlyWrapper.apiRequest( - { - url, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseSubscriptionXml( - body, - (error, recurlySubscription) => { - if (error) { - return callback(error) - } - if (options.includeAccount) { - let accountId - if ( - recurlySubscription.account && - recurlySubscription.account.url - ) { - accountId = - recurlySubscription.account.url.match(/accounts\/(.*)/)[1] - } else { - return callback( - new Error("I don't understand the response from Recurly") - ) - } + const { body } = await RecurlyWrapper.promises.apiRequest({ + url, + }) - RecurlyWrapper.getAccount(accountId, function (error, account) { - if (error) { - return callback(error) - } - recurlySubscription.account = account - callback(null, recurlySubscription) - }) - } else { - callback(null, recurlySubscription) - } - } - ) + const recurlySubscription = + await RecurlyWrapper.promises._parseSubscriptionXml(body) + + if (options.includeAccount) { + let accountId + if (recurlySubscription.account && recurlySubscription.account.url) { + accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1] + } else { + throw new Error("I don't understand the response from Recurly") } - ) + + recurlySubscription.account = + await RecurlyWrapper.promises.getAccount(accountId) + + return recurlySubscription + } else { + return recurlySubscription + } }, - getPaginatedEndpoint(resource, queryParams, callback) { + async getPaginatedEndpoint(resource, queryParams) { queryParams.per_page = queryParams.per_page || 200 let allItems = [] - const getPage = (cursor = null) => { + const getPage = async (cursor = null) => { const opts = { url: resource, qs: queryParams, @@ -577,139 +478,86 @@ const RecurlyWrapper = { if (cursor) { opts.qs.cursor = cursor } - return RecurlyWrapper.apiRequest(opts, (error, response, body) => { - if (error) { - return callback(error) - } - return RecurlyWrapper._parseXml(body, function (err, data) { - if (err) { - logger.warn({ err }, 'could not get accounts') - return callback(err) - } - const items = data[resource] - allItems = allItems.concat(items) - logger.debug( - `got another ${items.length}, total now ${allItems.length}` - ) - const match = response.headers.link?.match( - /cursor=([0-9.]+%3A[0-9.]+)&/ - ) - cursor = match && match[1] - if (cursor) { - cursor = decodeURIComponent(cursor) - return getPage(cursor) - } else { - callback(err, allItems) - } - }) - }) + const { response, body } = await RecurlyWrapper.promises.apiRequest(opts) + + const data = await RecurlyWrapper.promises._parseXml(body) + + const items = data[resource] + allItems = allItems.concat(items) + logger.debug(`got another ${items.length}, total now ${allItems.length}`) + const match = response.headers.link?.match(/cursor=([0-9.]+%3A[0-9.]+)&/) + cursor = match && match[1] + if (cursor) { + cursor = decodeURIComponent(cursor) + return getPage(cursor) + } else { + return allItems + } } - getPage() + await getPage() + + return allItems }, - getAccount(accountId, callback) { - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}`, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseAccountXml(body, callback) - } - ) + async getAccount(accountId) { + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}`, + }) + return await RecurlyWrapper.promises._parseAccountXml(body) }, updateAccountEmailAddress, - getAccountActiveCoupons(accountId, callback) { - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}/redemptions`, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseRedemptionsXml( - body, - function (error, redemptions) { - if (error) { - return callback(error) - } - const activeRedemptions = redemptions.filter( - redemption => redemption.state === 'active' - ) - const couponCodes = activeRedemptions.map( - redemption => redemption.coupon_code - ) - Async.map( - couponCodes, - RecurlyWrapper.getCoupon, - function (error, coupons) { - if (error) { - return callback(error) - } - return callback(null, coupons) - } - ) - } - ) - } + async getAccountActiveCoupons(accountId) { + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}/redemptions`, + }) + + const redemptions = await RecurlyWrapper.promises._parseRedemptionsXml(body) + + const activeRedemptions = redemptions.filter( + redemption => redemption.state === 'active' + ) + const couponCodes = activeRedemptions.map( + redemption => redemption.coupon_code + ) + + return await Promise.all( + couponCodes.map(couponCode => + RecurlyWrapper.promises.getCoupon(couponCode) + ) ) }, - getCoupon(couponCode, callback) { + async getCoupon(couponCode) { const opts = { url: `coupons/${couponCode}` } - RecurlyWrapper.apiRequest(opts, (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseCouponXml(body, callback) + const { body } = await RecurlyWrapper.promises.apiRequest(opts) + return await RecurlyWrapper.promises._parseCouponXml(body) + }, + + async getBillingInfo(accountId) { + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}/billing_info`, + }) + return await RecurlyWrapper.promises._parseXml(body) + }, + + async getAccountPastDueInvoices(accountId) { + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}/invoices?state=past_due`, + }) + return await RecurlyWrapper.promises._parseInvoicesXml(body) + }, + + async attemptInvoiceCollection(invoiceId) { + return await RecurlyWrapper.promises.apiRequest({ + url: `invoices/${invoiceId}/collect`, + method: 'put', }) }, - getBillingInfo(accountId, callback) { - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}/billing_info`, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseXml(body, callback) - } - ) - }, - - getAccountPastDueInvoices(accountId, callback) { - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}/invoices?state=past_due`, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseInvoicesXml(body, callback) - } - ) - }, - - attemptInvoiceCollection(invoiceId, callback) { - RecurlyWrapper.apiRequest( - { - url: `invoices/${invoiceId}/collect`, - method: 'put', - }, - callback - ) - }, - - updateSubscription(subscriptionId, options, callback) { + async updateSubscription(subscriptionId, options) { logger.debug( { subscriptionId, options }, 'telling recurly to update subscription' @@ -722,33 +570,23 @@ const RecurlyWrapper = { try { requestBody = RecurlyWrapper._buildXml('subscription', data) } catch (error) { - return callback( - OError.tag(error, 'error building xml', { subscriptionId }) - ) + throw OError.tag(error, 'error building xml', { subscriptionId }) } - RecurlyWrapper.apiRequest( - { - url: `subscriptions/${subscriptionId}`, - method: 'put', - body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseSubscriptionXml(responseBody, callback) - } - ) + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `subscriptions/${subscriptionId}`, + method: 'put', + body: requestBody, + }) + return await RecurlyWrapper.promises._parseSubscriptionXml(body) }, - createFixedAmmountCoupon( + async createFixedAmmountCoupon( couponCode, name, currencyCode, discountInCents, - planCode, - callback + planCode ) { const data = { coupon_code: couponCode, @@ -765,45 +603,33 @@ const RecurlyWrapper = { try { requestBody = RecurlyWrapper._buildXml('coupon', data) } catch (error) { - return callback( - OError.tag(error, 'error building xml', { - couponCode, - name, - }) - ) + throw OError.tag(error, 'error building xml', { + couponCode, + name, + }) } logger.debug({ couponCode, requestBody }, 'creating coupon') - RecurlyWrapper.apiRequest( - { + try { + await RecurlyWrapper.promises.apiRequest({ url: 'coupons', method: 'post', body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - logger.warn({ err: error, couponCode }, 'error creating coupon') - } - callback(error) - } - ) + }) + } catch (error) { + logger.warn({ err: error, couponCode }, 'error creating coupon') + throw error + } }, - lookupCoupon(couponCode, callback) { - RecurlyWrapper.apiRequest( - { - url: `coupons/${couponCode}`, - }, - (error, response, body) => { - if (error) { - return callback(error) - } - RecurlyWrapper._parseXml(body, callback) - } - ) + async lookupCoupon(couponCode) { + const { body } = await RecurlyWrapper.promises.apiRequest({ + url: `coupons/${couponCode}`, + }) + return await RecurlyWrapper.promises._parseCouponXml(body) }, - redeemCoupon(accountCode, couponCode, callback) { + async redeemCoupon(accountCode, couponCode) { const data = { account_code: accountCode, currency: 'USD', @@ -812,37 +638,32 @@ const RecurlyWrapper = { try { requestBody = RecurlyWrapper._buildXml('redemption', data) } catch (error) { - return callback( - OError.tag(error, 'error building xml', { - accountCode, - couponCode, - }) - ) + throw OError.tag(error, 'error building xml', { + accountCode, + couponCode, + }) } logger.debug( { accountCode, couponCode, requestBody }, 'redeeming coupon for user' ) - RecurlyWrapper.apiRequest( - { + try { + await RecurlyWrapper.promises.apiRequest({ url: `coupons/${couponCode}/redeem`, method: 'post', body: requestBody, - }, - (error, response, responseBody) => { - if (error) { - logger.warn( - { err: error, accountCode, couponCode }, - 'error redeeming coupon' - ) - } - callback(error) - } - ) + }) + } catch (error) { + logger.warn( + { err: error, accountCode, couponCode }, + 'error redeeming coupon' + ) + throw error + } }, - extendTrial(subscriptionId, daysUntilExpire, callback) { + async extendTrial(subscriptionId, daysUntilExpire) { if (daysUntilExpire == null) { daysUntilExpire = 7 } @@ -852,128 +673,121 @@ const RecurlyWrapper = { { subscriptionId, daysUntilExpire }, 'Exending Free trial for user' ) - RecurlyWrapper.apiRequest( - { + try { + await RecurlyWrapper.promises.apiRequest({ url: `/subscriptions/${subscriptionId}/postpone?next_bill_date=${nextRenewalDate}&bulk=false`, method: 'put', - }, - (error, response, responseBody) => { - if (error) { - logger.warn( - { err: error, subscriptionId, daysUntilExpire }, - 'error exending trial' - ) - } - callback(error) - } - ) + }) + } catch (error) { + logger.warn( + { err: error, subscriptionId, daysUntilExpire }, + 'error exending trial' + ) + throw error + } }, - listAccountActiveSubscriptions(accountId, callback) { - RecurlyWrapper.apiRequest( - { - url: `accounts/${accountId}/subscriptions`, - qs: { - state: 'active', + async listAccountActiveSubscriptions(accountId) { + const { response, body } = await RecurlyWrapper.promises.apiRequest({ + url: `accounts/${accountId}/subscriptions`, + qs: { + state: 'active', + }, + expect404: true, + }) + if (response.statusCode === 404) { + return [] + } else { + return await RecurlyWrapper.promises._parseSubscriptionsXml(body) + } + }, + + async _handle422Response(body) { + const data = await RecurlyWrapper.promises._parseErrorsXml(body) + let errorData = {} + if (data.transaction_error) { + errorData = { + message: data.transaction_error.merchant_message, + info: { + category: data.transaction_error.error_category, + gatewayCode: data.transaction_error.gateway_error_code, + public: { + code: data.transaction_error.error_code, + message: data.transaction_error.customer_message, + }, }, - expect404: true, - }, - function (error, response, body) { - if (error) { - return callback(error) - } - if (response.statusCode === 404) { - callback(null, []) - } else { - RecurlyWrapper._parseSubscriptionsXml(body, callback) - } } + if (data.transaction_error.three_d_secure_action_token_id) { + errorData.info.public.threeDSecureActionTokenId = + data.transaction_error.three_d_secure_action_token_id + } + } else if (data.error && data.error._) { + // fallback for errors that don't have a `transaction_error` field, but + // instead a `error` field with a message (e.g. VATMOSS errors) + errorData = { + info: { + public: { + message: data.error._, + }, + }, + } + } + throw new SubscriptionErrors.RecurlyTransactionError(errorData) + }, + + async _parseSubscriptionsXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute( + xml, + 'subscriptions' + ) + }, + async _parseSubscriptionXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute( + xml, + 'subscription' + ) + }, + async _parseAccountXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute( + xml, + 'account' + ) + }, + async _parseBillingInfoXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute( + xml, + 'billing_info' + ) + }, + async _parseRedemptionsXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute( + xml, + 'redemptions' + ) + }, + async _parseCouponXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute(xml, 'coupon') + }, + async _parseErrorsXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute(xml, 'errors') + }, + async _parseInvoicesXml(xml) { + return await RecurlyWrapper.promises._parseXmlAndGetAttribute( + xml, + 'invoices' ) }, - _handle422Response(body, callback) { - RecurlyWrapper._parseErrorsXml(body, (error, data) => { - if (error) { - return callback(error) - } - - let errorData = {} - if (data.transaction_error) { - errorData = { - message: data.transaction_error.merchant_message, - info: { - category: data.transaction_error.error_category, - gatewayCode: data.transaction_error.gateway_error_code, - public: { - code: data.transaction_error.error_code, - message: data.transaction_error.customer_message, - }, - }, - } - if (data.transaction_error.three_d_secure_action_token_id) { - errorData.info.public.threeDSecureActionTokenId = - data.transaction_error.three_d_secure_action_token_id - } - } else if (data.error && data.error._) { - // fallback for errors that don't have a `transaction_error` field, but - // instead a `error` field with a message (e.g. VATMOSS errors) - errorData = { - info: { - public: { - message: data.error._, - }, - }, - } - } - callback(new SubscriptionErrors.RecurlyTransactionError(errorData)) - }) - }, - _parseSubscriptionsXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'subscriptions', callback) + async _parseXmlAndGetAttribute(xml, attribute) { + const data = await RecurlyWrapper.promises._parseXml(xml) + if (data && data[attribute] != null) { + return data[attribute] + } else { + throw new Error("I don't understand the response from Recurly") + } }, - _parseSubscriptionXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'subscription', callback) - }, - - _parseAccountXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'account', callback) - }, - - _parseBillingInfoXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'billing_info', callback) - }, - - _parseRedemptionsXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'redemptions', callback) - }, - - _parseCouponXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback) - }, - - _parseErrorsXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'errors', callback) - }, - - _parseInvoicesXml(xml, callback) { - RecurlyWrapper._parseXmlAndGetAttribute(xml, 'invoices', callback) - }, - - _parseXmlAndGetAttribute(xml, attribute, callback) { - RecurlyWrapper._parseXml(xml, function (error, data) { - if (error) { - return callback(error) - } - if (data && data[attribute] != null) { - callback(null, data[attribute]) - } else { - callback(new Error("I don't understand the response from Recurly")) - } - }) - }, - - _parseXml(xml, callback) { + _parseXml(xml) { function convertDataTypes(data) { let key, value if (data && data.$) { @@ -1017,43 +831,54 @@ const RecurlyWrapper = { explicitArray: false, emptyTag: '', }) - parser.parseString(xml, function (error, data) { - if (error) { - return callback(error) - } - const result = convertDataTypes(data) - callback(null, result) - }) - }, - - _buildXml(rootName, data) { - const options = { - headless: true, - renderOpts: { - pretty: true, - indent: '\t', - }, - rootName, - } - const builder = new xml2js.Builder(options) - return builder.buildObject(data) + return new Promise((resolve, reject) => + parser.parseString(xml, function (error, data) { + if (error) { + return reject(error) + } + const result = convertDataTypes(data) + resolve(result) + }) + ) }, } +function _buildXml(rootName, data) { + const options = { + headless: true, + renderOpts: { + pretty: true, + indent: '\t', + }, + rootName, + } + const builder = new xml2js.Builder(options) + return builder.buildObject(data) +} + +const RecurlyWrapper = { + apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2', + _buildXml, + _parseXml: callbackify(promises._parseXml), + // This one needs to be callbackified manually because we need to transform {response, body} to (err, response, body) + attemptInvoiceCollection: (invoiceId, callback) => { + promises + .attemptInvoiceCollection(invoiceId) + .then(({ response, body }) => callback(null, response, body)) + .catch(callback) + }, + createFixedAmmountCoupon: callbackify(promises.createFixedAmmountCoupon), + getAccountActiveCoupons: callbackify(promises.getAccountActiveCoupons), + getBillingInfo: callbackify(promises.getBillingInfo), + getPaginatedEndpoint: callbackify(promises.getPaginatedEndpoint), + getSubscription: callbackify(promises.getSubscription), + getSubscriptions: callbackify(promises.getSubscriptions), + updateAccountEmailAddress: callbackify(promises.updateAccountEmailAddress), +} + RecurlyWrapper.promises = { - attemptInvoiceCollection: promisify(RecurlyWrapper.attemptInvoiceCollection), - createSubscription: promisify(RecurlyWrapper.createSubscription), - extendTrial: promisify(RecurlyWrapper.extendTrial), - getBillingInfo: promisify(RecurlyWrapper.getBillingInfo), - getAccountPastDueInvoices: promisify( - RecurlyWrapper.getAccountPastDueInvoices - ), - getSubscription: promisify(RecurlyWrapper.getSubscription), - listAccountActiveSubscriptions: promisify( - RecurlyWrapper.listAccountActiveSubscriptions - ), - redeemCoupon: promisify(RecurlyWrapper.redeemCoupon), - updateAccountEmailAddress: promisify(updateAccountEmailAddress), + ...promises, + updateAccountEmailAddress, } module.exports = RecurlyWrapper diff --git a/services/web/scripts/recurly/collect_paypal_past_due_invoice.js b/services/web/scripts/recurly/collect_paypal_past_due_invoice.js index 89a5702bb4..9dbe32744b 100644 --- a/services/web/scripts/recurly/collect_paypal_past_due_invoice.js +++ b/services/web/scripts/recurly/collect_paypal_past_due_invoice.js @@ -76,7 +76,7 @@ const main = async () => { 'invoices', { state: 'past_due' }, (error, invoices) => { - logger.info('invoices', invoices.length) + logger.info('invoices', invoices?.length) if (error) { return callback(error) } @@ -90,7 +90,7 @@ const main = async () => { const INVOICES_COLLECTED_SUCCESS = [] const USERS_COLLECTED = [] - return new Promise(resolve => { + return new Promise((resolve, reject) => { attemptInvoicesCollection(error => { logger.info( `DONE (DRY_RUN=${DRY_RUN}). ${INVOICES_COLLECTED.length} invoices collection attempts for ${USERS_COLLECTED.length} users. ${INVOICES_COLLECTED_SUCCESS.length} successful collections` @@ -105,11 +105,11 @@ const main = async () => { ) if (error) { - throw error + reject(error) } if (INVOICES_COLLECTED_SUCCESS.length === 0) { - throw new Error('No invoices collected') + reject(new Error('No invoices collected')) } resolve({ diff --git a/services/web/test/acceptance/src/CollectPayPalPastDueInvoiceTest.js b/services/web/test/acceptance/src/CollectPayPalPastDueInvoiceTest.js index 57ba6b85ef..cfb200d0cf 100644 --- a/services/web/test/acceptance/src/CollectPayPalPastDueInvoiceTest.js +++ b/services/web/test/acceptance/src/CollectPayPalPastDueInvoiceTest.js @@ -155,44 +155,32 @@ const invoiceCollectXml = ` ` -// from our logs -const invoiceCollectErrXml2 = ` - - - not_found - Couldn't find BillingInfo with account_code = abcdef87654321 - -` - describe('CollectPayPalPastDueInvoice', function () { let apiRequestStub const fakeApiRequests = invoiceIdsAndReturnCode => { - apiRequestStub = sinon.stub(RecurlyWrapper, 'apiRequest') - apiRequestStub.callsFake((options, callback) => { + apiRequestStub = sinon.stub(RecurlyWrapper.promises, 'apiRequest') + apiRequestStub.callsFake(options => { switch (options.url) { case 'invoices': - callback( - null, - { statusCode: 200, headers: {} }, - invoicesXml(invoiceIdsAndReturnCode) - ) - return + return { + response: { statusCode: 200, headers: {} }, + body: invoicesXml(invoiceIdsAndReturnCode), + } case 'accounts/200/billing_info': case 'accounts/404/billing_info': - callback(null, { statusCode: 200, headers: {} }, billingInfoXml) - return + return { + response: { statusCode: 200, headers: {} }, + body: billingInfoXml, + } case 'invoices/200/collect': - callback(null, { statusCode: 200, headers: {} }, invoiceCollectXml) - return + return { + response: { statusCode: 200, headers: {} }, + body: invoiceCollectXml, + } case 'invoices/404/collect': - callback( - new OError(`Recurly API returned with status code: 404`, { - statusCode: 404, - }), - { statusCode: 404, headers: {} }, - invoiceCollectErrXml2 - ) - return + throw new OError(`Recurly API returned with status code: 404`, { + statusCode: 404, + }) default: throw new Error(`Unexpected URL: ${options.url}`) } diff --git a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js index 3eb1008178..225f78c2fc 100644 --- a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js +++ b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js @@ -90,11 +90,18 @@ const fixtures = { '', } -const mockApiRequest = function (options, callback) { +const mockApiRequest = function (options) { if (fixtures[options.url]) { - callback(null, { statusCode: 200 }, fixtures[options.url]) + return { + err: null, + response: { statusCode: 200 }, + body: fixtures[options.url], + } } else { - callback(new Error('Not found'), { statusCode: 404 }) + return { + err: new Error('Not found'), + response: { statusCode: 404 }, + } } } @@ -138,122 +145,130 @@ describe('RecurlyWrapper', function () { }) describe('getSubscription', function () { - describe('with proper subscription id', function () { - beforeEach(function (done) { - this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake(mockApiRequest) - this.RecurlyWrapper.getSubscription( - '44f83d7cba354d5b84812419f923ea96', - (error, recurlySubscription) => { - if (error) return done(error) - this.recurlySubscription = recurlySubscription - done() + for (const functionType of ['promise', 'callback']) { + describe(`as ${functionType}`, function () { + beforeEach(function () { + this.recurlySubscription = 'RESET' + this.getSubscription = (...params) => { + if (functionType === 'promise') { + return this.RecurlyWrapper.promises.getSubscription(...params) + } + if (functionType === 'callback') { + return new Promise((resolve, reject) => + this.RecurlyWrapper.getSubscription( + ...params, + (err, subscription) => + err ? reject(err) : resolve(subscription) + ) + ) + } + throw Error('Invalid function type') } - ) - }) - afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() - }) + }) - it('should look up the subscription at the normal API end point', function () { - this.apiRequest.args[0][0].url.should.equal( - 'subscriptions/44f83d7cba354d5b84812419f923ea96' - ) - }) + describe('with proper subscription id', function () { + beforeEach(async function () { + this.apiRequest = sinon + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(mockApiRequest) + this.recurlySubscription = await this.getSubscription( + '44f83d7cba354d5b84812419f923ea96' + ) + }) + afterEach(function () { + this.RecurlyWrapper.promises.apiRequest.restore() + }) - it('should return the subscription', function () { - this.recurlySubscription.uuid.should.equal( - '44f83d7cba354d5b84812419f923ea96' - ) - }) - }) + it('should look up the subscription at the normal API end point', function () { + this.apiRequest.args[0][0].url.should.equal( + 'subscriptions/44f83d7cba354d5b84812419f923ea96' + ) + }) - describe('with ReculyJS token', function () { - beforeEach(function (done) { - this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake(mockApiRequest) - this.RecurlyWrapper.getSubscription( - '70db44b10f5f4b238669480c9903f6f5', - { recurlyJsResult: true }, - (error, recurlySubscription) => { - if (error) return done(error) - this.recurlySubscription = recurlySubscription - done() - } - ) - }) - afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() - }) + it('should return the subscription', function () { + this.recurlySubscription.uuid.should.equal( + '44f83d7cba354d5b84812419f923ea96' + ) + }) + }) - it('should return the subscription', function () { - this.recurlySubscription.uuid.should.equal( - '44f83d7cba354d5b84812419f923ea96' - ) - }) + describe('with ReculyJS token', function () { + beforeEach(async function () { + this.apiRequest = sinon + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(mockApiRequest) + this.recurlySubscription = await this.getSubscription( + '70db44b10f5f4b238669480c9903f6f5', + { recurlyJsResult: true } + ) + }) + afterEach(function () { + this.RecurlyWrapper.promises.apiRequest.restore() + }) - it('should look up the subscription at the RecurlyJS API end point', function () { - this.apiRequest.args[0][0].url.should.equal( - 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5' - ) - }) - }) + it('should return the subscription', function () { + this.recurlySubscription.uuid.should.equal( + '44f83d7cba354d5b84812419f923ea96' + ) + }) - describe('with includeAccount', function () { - beforeEach(function (done) { - this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake(mockApiRequest) - this.RecurlyWrapper.getSubscription( - '44f83d7cba354d5b84812419f923ea96', - { includeAccount: true }, - (error, recurlySubscription) => { - if (error) return done(error) - this.recurlySubscription = recurlySubscription - done() - } - ) - }) - afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() - }) + it('should look up the subscription at the RecurlyJS API end point', function () { + this.apiRequest.args[0][0].url.should.equal( + 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5' + ) + }) + }) - it('should request the account from the API', function () { - this.apiRequest.args[1][0].url.should.equal('accounts/104') - }) + describe('with includeAccount', function () { + beforeEach(async function () { + this.apiRequest = sinon + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(mockApiRequest) + this.recurlySubscription = await this.getSubscription( + '44f83d7cba354d5b84812419f923ea96', + { includeAccount: true } + ) + }) + afterEach(function () { + this.RecurlyWrapper.promises.apiRequest.restore() + }) - it('should populate the account attribute', function () { - this.recurlySubscription.account.account_code.should.equal('104') + it('should request the account from the API', function () { + this.apiRequest.args[1][0].url.should.equal('accounts/104') + }) + + it('should populate the account attribute', function () { + this.recurlySubscription.account.account_code.should.equal('104') + }) + }) }) - }) + } }) describe('updateAccountEmailAddress', function () { - beforeEach(function (done) { + beforeEach(async function () { this.recurlyAccountId = 'account-id-123' this.newEmail = 'example@overleaf.com' this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake((options, callback) => { + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(options => { this.requestOptions = options - callback(null, {}, fixtures['accounts/104']) + return { + err: null, + response: {}, + body: fixtures['accounts/104'], + } }) - this.RecurlyWrapper.updateAccountEmailAddress( - this.recurlyAccountId, - this.newEmail, - (error, recurlyAccount) => { - if (error) return done(error) - this.recurlyAccount = recurlyAccount - done() - } - ) + this.recurlyAccount = + await this.RecurlyWrapper.promises.updateAccountEmailAddress( + this.recurlyAccountId, + this.newEmail + ) }) afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() + this.RecurlyWrapper.promises.apiRequest.restore() }) it('sends correct XML', function () { @@ -275,61 +290,59 @@ describe('RecurlyWrapper', function () { }) describe('updateAccountEmailAddress, with invalid XML', function () { - beforeEach(function (done) { + beforeEach(async function (done) { this.recurlyAccountId = 'account-id-123' this.newEmail = '\uD800@example.com' this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake((options, callback) => { + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(options => { this.requestOptions = options - callback(null, {}, fixtures['accounts/104']) + return { + err: null, + response: {}, + body: fixtures['accounts/104'], + } }) done() }) afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() + this.RecurlyWrapper.promises.apiRequest.restore() }) it('should produce an error', function (done) { - this.RecurlyWrapper.updateAccountEmailAddress( - this.recurlyAccountId, - this.newEmail, - (error, recurlyAccount) => { + this.RecurlyWrapper.promises + .updateAccountEmailAddress(this.recurlyAccountId, this.newEmail) + .catch(error => { expect(error).to.exist expect(error.message.startsWith('Invalid character')).to.equal(true) expect(this.apiRequest.called).to.equal(false) done() - } - ) + }) }) }) describe('updateSubscription', function () { - beforeEach(function (done) { + beforeEach(async function () { this.recurlySubscriptionId = 'subscription-id-123' this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake((options, callback) => { + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(options => { this.requestOptions = options - callback( - null, - {}, - fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96'] - ) + return { + error: null, + response: {}, + body: fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96'], + } }) - this.RecurlyWrapper.updateSubscription( - this.recurlySubscriptionId, - { plan_code: 'silver', timeframe: 'now' }, - (error, recurlySubscription) => { - if (error) return done(error) - this.recurlySubscription = recurlySubscription - done() - } - ) + this.recurlySubscription = + await this.RecurlyWrapper.promises.updateSubscription( + this.recurlySubscriptionId, + { plan_code: 'silver', timeframe: 'now' } + ) }) afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() + this.RecurlyWrapper.promises.apiRequest.restore() }) it('sends correct XML', function () { @@ -354,25 +367,24 @@ describe('RecurlyWrapper', function () { }) describe('redeemCoupon', function () { - beforeEach(function (done) { + beforeEach(async function () { this.recurlyAccountId = 'account-id-123' this.coupon_code = '312321312' this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake((options, callback) => { + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .callsFake(options => { options.url.should.equal(`coupons/${this.coupon_code}/redeem`) options.method.should.equal('post') - callback() + return {} }) - this.RecurlyWrapper.redeemCoupon( + await this.RecurlyWrapper.promises.redeemCoupon( this.recurlyAccountId, - this.coupon_code, - done + this.coupon_code ) }) afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() + this.RecurlyWrapper.promises.apiRequest.restore() }) it('sends correct XML', function () { @@ -388,29 +400,26 @@ describe('RecurlyWrapper', function () { }) describe('createFixedAmmountCoupon', function () { - beforeEach(function (done) { + beforeEach(async function () { this.couponCode = 'a-coupon-code' this.couponName = 'a-coupon-name' this.currencyCode = 'EUR' this.discount = 1337 this.planCode = 'a-plan-code' this.apiRequest = sinon - .stub(this.RecurlyWrapper, 'apiRequest') - .callsFake((options, callback) => { - callback() - }) - this.RecurlyWrapper.createFixedAmmountCoupon( + .stub(this.RecurlyWrapper.promises, 'apiRequest') + .resolves() + await this.RecurlyWrapper.promises.createFixedAmmountCoupon( this.couponCode, this.couponName, this.currencyCode, this.discount, - this.planCode, - done + this.planCode ) }) afterEach(function () { - this.RecurlyWrapper.apiRequest.restore() + this.RecurlyWrapper.promises.apiRequest.restore() }) it('sends correct XML', function () { @@ -457,12 +466,11 @@ describe('RecurlyWrapper', function () { billing: 'a-token-id', threeDSecureActionResult: 'a-3d-token-id', } - this.call = callback => { - this.RecurlyWrapper.createSubscription( + this.call = () => { + return this.RecurlyWrapper.promises.createSubscription( this.user, this.subscriptionDetails, - this.recurlyTokenIds, - callback + this.recurlyTokenIds ) } }) @@ -471,50 +479,37 @@ describe('RecurlyWrapper', function () { beforeEach(function () { this.subscriptionDetails.isPaypal = true this._createPaypalSubscription = sinon.stub( - this.RecurlyWrapper, + this.RecurlyWrapper.promises, '_createPaypalSubscription' ) - this._createPaypalSubscription.callsArgWith(3, null, this.subscription) + this._createPaypalSubscription.resolves(this.subscription) }) afterEach(function () { this._createPaypalSubscription.restore() }) - it('should not produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.equal(null) - expect(err).to.not.be.instanceof(Error) - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should produce a subscription object', function (done) { - this.call((err, sub) => { - if (err) return done(err) - expect(sub).to.deep.equal(this.subscription) - done() - }) + it('should produce a subscription object', async function () { + const sub = await this.call() + expect(sub).to.deep.equal(this.subscription) }) - it('should call _createPaypalSubscription', function (done) { - this.call((err, sub) => { - if (err) return done(err) - this._createPaypalSubscription.callCount.should.equal(1) - done() - }) + it('should call _createPaypalSubscription', async function () { + await this.call() + this._createPaypalSubscription.callCount.should.equal(1) }) describe('when _createPaypalSubscription produces an error', function () { beforeEach(function () { - this._createPaypalSubscription.callsArgWith(3, new Error('woops')) + this._createPaypalSubscription.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith('woops') }) }) }) @@ -523,54 +518,37 @@ describe('RecurlyWrapper', function () { beforeEach(function () { this.subscriptionDetails.isPaypal = false this._createCreditCardSubscription = sinon.stub( - this.RecurlyWrapper, + this.RecurlyWrapper.promises, '_createCreditCardSubscription' ) - this._createCreditCardSubscription.callsArgWith( - 3, - null, - this.subscription - ) + this._createCreditCardSubscription.resolves(this.subscription) }) afterEach(function () { this._createCreditCardSubscription.restore() }) - it('should not produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.equal(null) - expect(err).to.not.be.instanceof(Error) - done() - }) + it('should not produce an error', async function () { + await this.call() }) - it('should produce a subscription object', function (done) { - this.call((err, sub) => { - if (err) return done(err) - expect(sub).to.deep.equal(this.subscription) - done() - }) + it('should produce a subscription object', async function () { + const sub = await this.call() + expect(sub).to.deep.equal(this.subscription) }) - it('should call _createCreditCardSubscription', function (done) { - this.call((err, sub) => { - if (err) return done(err) - this._createCreditCardSubscription.callCount.should.equal(1) - done() - }) + it('should call _createCreditCardSubscription', async function () { + await this.call() + this._createCreditCardSubscription.callCount.should.equal(1) }) describe('when _createCreditCardSubscription produces an error', function () { beforeEach(function () { - this._createCreditCardSubscription.callsArgWith(3, new Error('woops')) + this._createCreditCardSubscription.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith('woops') }) }) }) @@ -606,21 +584,23 @@ describe('RecurlyWrapper', function () { billing: 'a-token-id', threeDSecureActionResult: 'a-3d-token-id', } - this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest') + this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest') this.response = { statusCode: 200 } this.body = 'is_bad' - this.apiRequest.callsArgWith(1, null, this.response, this.body) + this.apiRequest.resolves({ + response: this.response, + body: this.body, + }) this._parseSubscriptionXml = sinon.stub( - this.RecurlyWrapper, + this.RecurlyWrapper.promises, '_parseSubscriptionXml' ) - this._parseSubscriptionXml.callsArgWith(1, null, this.subscription) - this.call = callback => { - this.RecurlyWrapper._createCreditCardSubscription( + this._parseSubscriptionXml.resolves(this.subscription) + this.call = () => { + return this.RecurlyWrapper.promises._createCreditCardSubscription( this.user, this.subscriptionDetails, - this.recurlyTokenIds, - callback + this.recurlyTokenIds ) } }) @@ -630,11 +610,11 @@ describe('RecurlyWrapper', function () { this._parseSubscriptionXml.restore() }) - it('sends correct XML', function (done) { - this.call((err, result) => { - if (err) return done(err) - const { body } = this.apiRequest.lastCall.args[0] - expect(body).to.equal(`\ + it('sends correct XML', async function () { + await this.call() + + const { body } = this.apiRequest.lastCall.args[0] + expect(body).to.equal(`\ some_plan_code EUR @@ -665,40 +645,25 @@ describe('RecurlyWrapper', function () { \ `) - done() - }) }) - it('should not produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.not.be.instanceof(Error) - expect(err).to.equal(null) - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should produce a subscription', function (done) { - this.call((err, sub) => { - if (err) return done(err) - expect(sub).to.equal(this.subscription) - done() - }) + it('should produce a subscription', async function () { + const sub = await this.call() + expect(sub).to.equal(this.subscription) }) - it('should call apiRequest', function (done) { - this.call((err, sub) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - done() - }) + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) }) - it('should call _parseSubscriptionXml', function (done) { - this.call((err, sub) => { - if (err) return done(err) - this._parseSubscriptionXml.callCount.should.equal(1) - done() - }) + it('should call _parseSubscriptionXml', async function () { + await this.call() + this._parseSubscriptionXml.callCount.should.equal(1) }) describe('when api request returns 422', function () { @@ -717,11 +682,15 @@ describe('RecurlyWrapper', function () { Your card must be authenticated with 3D Secure before continuing. ` - this.apiRequest.yields(null, { statusCode: 422 }, body) + // this.apiRequest.yields(null, { statusCode: 422 }, body) + this.apiRequest.resolves({ + response: { statusCode: 422 }, + body, + }) }) it('should produce an error', function (done) { - this.call((err, sub) => { + this.call().catch(err => { expect(err).to.be.instanceof( SubscriptionErrors.RecurlyTransactionError ) @@ -738,41 +707,31 @@ describe('RecurlyWrapper', function () { describe('when api request produces an error', function () { beforeEach(function () { - this.apiRequest.callsArgWith(1, new Error('woops')) + this.apiRequest.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith('woops') }) - it('should call apiRequest', function (done) { - this.call(() => { - this.apiRequest.callCount.should.equal(1) - done() - }) + it('should call apiRequest', async function () { + await expect(this.call()).to.be.rejected + this.apiRequest.callCount.should.equal(1) }) - it('should not _parseSubscriptionXml', function (done) { - this.call(() => { - this._parseSubscriptionXml.callCount.should.equal(0) - done() - }) + it('should not _parseSubscriptionXml', async function () { + await expect(this.call()).to.be.rejected + this._parseSubscriptionXml.callCount.should.equal(0) }) }) describe('when parse xml produces an error', function () { beforeEach(function () { - this._parseSubscriptionXml.callsArgWith(1, new Error('woops')) + this._parseSubscriptionXml.rejects(new Error('woops xml')) }) - it('should produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith('woops xml') }) }) }) @@ -780,23 +739,23 @@ describe('RecurlyWrapper', function () { describe('_createPaypalSubscription', function () { beforeEach(function () { this.checkAccountExists = sinon.stub( - this.RecurlyWrapper._paypal, + this.RecurlyWrapper.promises._paypal, 'checkAccountExists' ) this.createAccount = sinon.stub( - this.RecurlyWrapper._paypal, + this.RecurlyWrapper.promises._paypal, 'createAccount' ) this.createBillingInfo = sinon.stub( - this.RecurlyWrapper._paypal, + this.RecurlyWrapper.promises._paypal, 'createBillingInfo' ) this.setAddressAndCompanyBillingInfo = sinon.stub( - this.RecurlyWrapper._paypal, + this.RecurlyWrapper.promises._paypal, 'setAddressAndCompanyBillingInfo' ) this.createSubscription = sinon.stub( - this.RecurlyWrapper._paypal, + this.RecurlyWrapper.promises._paypal, 'createSubscription' ) this.user = { @@ -827,21 +786,21 @@ describe('RecurlyWrapper', function () { const { subscriptionDetails } = this const { recurlyTokenIds } = this - this.checkAccountExists.callsArgWith(1, null, { + this.checkAccountExists.resolves({ user, subscriptionDetails, recurlyTokenIds, userExists: false, account: { accountCode: 'xx' }, }) - this.createAccount.callsArgWith(1, null, { + this.createAccount.resolves({ user, subscriptionDetails, recurlyTokenIds, userExists: false, account: { accountCode: 'xx' }, }) - this.createBillingInfo.callsArgWith(1, null, { + this.createBillingInfo.resolves({ user, subscriptionDetails, recurlyTokenIds, @@ -849,7 +808,7 @@ describe('RecurlyWrapper', function () { account: { accountCode: 'xx' }, billingInfo: { token_id: 'abc' }, }) - this.setAddressAndCompanyBillingInfo.callsArgWith(1, null, { + this.setAddressAndCompanyBillingInfo.resolves({ user, subscriptionDetails, recurlyTokenIds, @@ -857,7 +816,7 @@ describe('RecurlyWrapper', function () { account: { accountCode: 'xx' }, billingInfo: { token_id: 'abc' }, }) - this.createSubscription.callsArgWith(1, null, { + this.createSubscription.resolves({ user, subscriptionDetails, recurlyTokenIds, @@ -867,12 +826,11 @@ describe('RecurlyWrapper', function () { subscription: this.subscription, }) - this.call = callback => { - this.RecurlyWrapper._createPaypalSubscription( + this.call = () => { + return this.RecurlyWrapper.promises._createPaypalSubscription( this.user, this.subscriptionDetails, - this.recurlyTokenIds, - callback + this.recurlyTokenIds ) } }) @@ -885,69 +843,58 @@ describe('RecurlyWrapper', function () { this.createSubscription.restore() }) - it('should not produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.not.be.instanceof(Error) - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should produce a subscription object', function (done) { - this.call((err, sub) => { - if (err) return done(err) - expect(sub).to.not.equal(null) - expect(sub).to.equal(this.subscription) - done() - }) + it('should produce a subscription object', async function () { + const sub = await this.call() + expect(sub).to.not.equal(null) + expect(sub).to.equal(this.subscription) }) - it('should call each of the paypal stages', function (done) { - this.call((err, sub) => { - if (err) return done(err) - this.checkAccountExists.callCount.should.equal(1) - this.createAccount.callCount.should.equal(1) - this.createBillingInfo.callCount.should.equal(1) - this.setAddressAndCompanyBillingInfo.callCount.should.equal(1) - this.createSubscription.callCount.should.equal(1) - done() - }) + it('should call each of the paypal stages', async function () { + await this.call() + this.checkAccountExists.callCount.should.equal(1) + this.createAccount.callCount.should.equal(1) + this.createBillingInfo.callCount.should.equal(1) + this.setAddressAndCompanyBillingInfo.callCount.should.equal(1) + this.createSubscription.callCount.should.equal(1) }) describe('when one of the paypal stages produces an error', function () { beforeEach(function () { - this.createAccount.callsArgWith(1, new Error('woops')) + this.createAccount.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, sub) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith('woops') }) - it('should stop calling the paypal stages after the error', function (done) { - this.call(() => { - this.checkAccountExists.callCount.should.equal(1) - this.createAccount.callCount.should.equal(1) - this.createBillingInfo.callCount.should.equal(0) - this.setAddressAndCompanyBillingInfo.callCount.should.equal(0) - this.createSubscription.callCount.should.equal(0) - done() - }) + it('should stop calling the paypal stages after the error', async function () { + await expect(this.call()).to.be.rejected + this.checkAccountExists.callCount.should.equal(1) + this.createAccount.callCount.should.equal(1) + this.createBillingInfo.callCount.should.equal(0) + this.setAddressAndCompanyBillingInfo.callCount.should.equal(0) + this.createSubscription.callCount.should.equal(0) }) }) }) describe('paypal actions', function () { beforeEach(function () { - this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest') - this._parseAccountXml = sinon.spy(this.RecurlyWrapper, '_parseAccountXml') + this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest') + this._parseAccountXml = sinon.spy( + this.RecurlyWrapper.promises, + '_parseAccountXml' + ) this._parseBillingInfoXml = sinon.spy( - this.RecurlyWrapper, + this.RecurlyWrapper.promises, '_parseBillingInfoXml' ) this._parseSubscriptionXml = sinon.spy( - this.RecurlyWrapper, + this.RecurlyWrapper.promises, '_parseSubscriptionXml' ) this.cache = { @@ -989,8 +936,10 @@ describe('RecurlyWrapper', function () { describe('_paypal.checkAccountExists', function () { beforeEach(function () { - this.call = callback => { - this.RecurlyWrapper._paypal.checkAccountExists(this.cache, callback) + this.call = () => { + return this.RecurlyWrapper.promises._paypal.checkAccountExists( + this.cache + ) } }) @@ -998,119 +947,94 @@ describe('RecurlyWrapper', function () { beforeEach(function () { const resultXml = 'abc' - this.apiRequest.callsArgWith(1, null, { statusCode: 200 }, resultXml) - }) - - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() + this.apiRequest.resolves({ + response: { statusCode: 200 }, + body: resultXml, }) }) - it('should call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - done() + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled + }) + + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) + }) + + it('should call _parseAccountXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal( + 1 + ) + }) + + it('should add the account to the cumulative result', async function () { + const result = await this.call() + expect(result.account).to.not.equal(null) + expect(result.account).to.not.equal(undefined) + expect(result.account).to.deep.equal({ + account_code: 'abc', }) }) - it('should call _parseAccountXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseAccountXml.callCount.should.equal(1) - done() - }) - }) - - it('should add the account to the cumulative result', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.account).to.not.equal(null) - expect(result.account).to.not.equal(undefined) - expect(result.account).to.deep.equal({ - account_code: 'abc', - }) - done() - }) - }) - - it('should set userExists to true', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.userExists).to.equal(true) - done() - }) + it('should set userExists to true', async function () { + const result = await this.call() + expect(result.userExists).to.equal(true) }) }) describe('when the account does not exist', function () { beforeEach(function () { - this.apiRequest.callsArgWith(1, null, { statusCode: 404 }, '') - }) - - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() + this.apiRequest.resolves({ + response: { statusCode: 404 }, + body: '', }) }) - it('should call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - this.apiRequest.firstCall.args[0].method.should.equal('GET') - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should not call _parseAccountXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0) - done() - }) + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('GET') }) - it('should not add the account to result', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.account).to.equal(undefined) - done() - }) + it('should not call _parseAccountXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal( + 0 + ) }) - it('should set userExists to false', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.userExists).to.equal(false) - done() - }) + it('should not add the account to result', async function () { + const result = await this.call() + expect(result.account).to.equal(undefined) + }) + + it('should set userExists to false', async function () { + const result = await this.call() + expect(result.userExists).to.equal(false) }) }) describe('when apiRequest produces an error', function () { beforeEach(function () { - this.apiRequest.callsArgWith(1, new Error('woops'), { - statusCode: 500, - }) + this.apiRequest.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected }) }) }) describe('_paypal.createAccount', function () { beforeEach(function () { - this.call = callback => { - this.RecurlyWrapper._paypal.createAccount(this.cache, callback) + this.call = () => { + return this.RecurlyWrapper.promises._paypal.createAccount(this.cache) } }) @@ -1119,11 +1043,8 @@ describe('RecurlyWrapper', function () { this.cache.subscriptionDetails.address = null }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected }) }) @@ -1132,11 +1053,8 @@ describe('RecurlyWrapper', function () { this.cache.subscriptionDetails.address = {} }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Errors.InvalidError) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith(Errors.InvalidError) }) }) @@ -1146,38 +1064,28 @@ describe('RecurlyWrapper', function () { this.cache.account = { account_code: 'abc' } }) - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled + }) + + it('should produce cache object', async function () { + const result = await this.call() + expect(result).to.deep.equal(this.cache) + expect(result.account).to.deep.equal({ + account_code: 'abc', }) }) - it('should produce cache object', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result).to.deep.equal(this.cache) - expect(result.account).to.deep.equal({ - account_code: 'abc', - }) - done() - }) + it('should not call apiRequest', async function () { + await expect(this.call()).to.be.fulfilled + this.apiRequest.callCount.should.equal(0) }) - it('should not call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(0) - done() - }) - }) - - it('should not call _parseAccountXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0) - done() - }) + it('should not call _parseAccountXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal( + 0 + ) }) }) @@ -1186,14 +1094,16 @@ describe('RecurlyWrapper', function () { this.cache.userExists = false const resultXml = 'abc' - this.apiRequest.callsArgWith(1, null, { statusCode: 200 }, resultXml) + this.apiRequest.resolves({ + response: { statusCode: 200 }, + body: resultXml, + }) }) - it('sends correct XML', function (done) { - this.call((err, result) => { - if (err) return done(err) - const { body } = this.apiRequest.lastCall.args[0] - expect(body).to.equal(`\ + it('sends correct XML', async function () { + await this.call() + const { body } = this.apiRequest.lastCall.args[0] + expect(body).to.equal(`\ some_id foo@bar.com @@ -1209,46 +1119,32 @@ describe('RecurlyWrapper', function () { \ `) - done() - }) }) - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - this.apiRequest.firstCall.args[0].method.should.equal('POST') - done() - }) + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('POST') }) - it('should call _parseAccountXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseAccountXml.callCount.should.equal(1) - done() - }) + it('should call _parseAccountXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal( + 1 + ) }) describe('when apiRequest produces an error', function () { beforeEach(function () { - this.apiRequest.callsArgWith(1, new Error('woops'), { - statusCode: 500, - }) + this.apiRequest.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith('woops') }) }) }) @@ -1257,102 +1153,9 @@ describe('RecurlyWrapper', function () { describe('_paypal.createBillingInfo', function () { beforeEach(function () { this.cache.account = { account_code: 'abc' } - this.call = callback => { - this.RecurlyWrapper._paypal.createBillingInfo(this.cache, callback) - } - }) - - describe('when account_code is missing from cache', function () { - beforeEach(function () { - this.cache.account.account_code = null - }) - - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) - }) - }) - - describe('when all goes well', function () { - beforeEach(function () { - const resultXml = '1' - this.apiRequest.callsArgWith(1, null, { statusCode: 200 }, resultXml) - }) - - it('sends correct XML', function (done) { - this.call((err, result) => { - if (err) return done(err) - const { body } = this.apiRequest.lastCall.args[0] - expect(body).to.equal(`\ - - a-token-id -\ -`) - done() - }) - }) - - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() - }) - }) - - it('should call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - this.apiRequest.firstCall.args[0].method.should.equal('POST') - done() - }) - }) - - it('should call _parseBillingInfoXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseBillingInfoXml.callCount.should.equal(1) - done() - }) - }) - - it('should set billingInfo on cache', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.billingInfo).to.deep.equal({ - a: '1', - }) - done() - }) - }) - }) - - describe('when apiRequest produces an error', function () { - beforeEach(function () { - this.apiRequest.callsArgWith(1, new Error('woops'), { - statusCode: 500, - }) - }) - - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) - }) - }) - }) - - describe('_paypal.setAddressAndCompanyBillingInfo', function () { - beforeEach(function () { - this.cache.account = { account_code: 'abc' } - this.cache.billingInfo = {} - this.call = callback => { - this.RecurlyWrapper._paypal.setAddressAndCompanyBillingInfo( - this.cache, - callback + this.call = () => { + return this.RecurlyWrapper.promises._paypal.createBillingInfo( + this.cache ) } }) @@ -1362,12 +1165,85 @@ describe('RecurlyWrapper', function () { this.cache.account.account_code = null }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected + }) + }) + + describe('when all goes well', function () { + beforeEach(function () { + const resultXml = '1' + this.apiRequest.resolves({ + response: { statusCode: 200 }, + body: resultXml, }) }) + + it('sends correct XML', async function () { + await this.call() + const { body } = this.apiRequest.lastCall.args[0] + expect(body).to.equal(`\ + + a-token-id +\ +`) + }) + + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled + }) + + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('POST') + }) + + it('should call _parseBillingInfoXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal( + 1 + ) + }) + + it('should set billingInfo on cache', async function () { + const result = await this.call() + expect(result.billingInfo).to.deep.equal({ + a: '1', + }) + }) + }) + + describe('when apiRequest produces an error', function () { + beforeEach(function () { + this.apiRequest.resolves(new Error('woops')) + }) + + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected + }) + }) + }) + + describe('_paypal.setAddressAndCompanyBillingInfo', function () { + beforeEach(function () { + this.cache.account = { account_code: 'abc' } + this.cache.billingInfo = {} + this.call = () => { + return this.RecurlyWrapper.promises._paypal.setAddressAndCompanyBillingInfo( + this.cache + ) + } + }) + + describe('when account_code is missing from cache', function () { + beforeEach(function () { + this.cache.account.account_code = null + }) + + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected + }) }) describe('when country is missing', function () { @@ -1375,25 +1251,24 @@ describe('RecurlyWrapper', function () { this.cache.subscriptionDetails.address = { country: '' } }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Errors.InvalidError) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejectedWith(Errors.InvalidError) }) }) describe('when all goes well', function () { beforeEach(function () { const resultXml = 'London' - this.apiRequest.callsArgWith(1, null, { statusCode: 200 }, resultXml) + this.apiRequest.resolves({ + response: { statusCode: 200 }, + body: resultXml, + }) }) - it('sends correct XML', function (done) { - this.call((err, result) => { - if (err) return done(err) - const { body } = this.apiRequest.lastCall.args[0] - expect(body).to.equal(`\ + it('sends correct XML', async function () { + await this.call() + const { body } = this.apiRequest.lastCall.args[0] + expect(body).to.equal(`\ addr_one addr_two @@ -1403,57 +1278,40 @@ describe('RecurlyWrapper', function () { some_country \ `) - done() - }) }) - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - this.apiRequest.firstCall.args[0].method.should.equal('PUT') - done() - }) + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('PUT') }) - it('should call _parseBillingInfoXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseBillingInfoXml.callCount.should.equal(1) - done() - }) + it('should call _parseBillingInfoXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal( + 1 + ) }) - it('should set billingInfo on cache', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.billingInfo).to.deep.equal({ - city: 'London', - }) - done() + it('should set billingInfo on cache', async function () { + const result = await this.call() + expect(result.billingInfo).to.deep.equal({ + city: 'London', }) }) }) describe('when apiRequest produces an error', function () { beforeEach(function () { - this.apiRequest.callsArgWith(1, new Error('woops'), { - statusCode: 500, - }) + this.apiRequest.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected }) }) }) @@ -1462,22 +1320,23 @@ describe('RecurlyWrapper', function () { beforeEach(function () { this.cache.account = { account_code: 'abc' } this.cache.billingInfo = {} - this.call = callback => { - this.RecurlyWrapper._paypal.createSubscription(this.cache, callback) - } + this.call = () => + this.RecurlyWrapper.promises._paypal.createSubscription(this.cache) }) describe('when all goes well', function () { beforeEach(function () { const resultXml = '1' - this.apiRequest.callsArgWith(1, null, { statusCode: 200 }, resultXml) + this.apiRequest.resolves({ + response: { statusCode: 200 }, + body: resultXml, + }) }) - it('sends correct XML', function (done) { - this.call((err, result) => { - if (err) return done(err) - const { body } = this.apiRequest.lastCall.args[0] - expect(body).to.equal(`\ + it('sends correct XML', async function () { + await this.call() + const { body } = this.apiRequest.lastCall.args[0] + expect(body).to.equal(`\ some_plan_code EUR @@ -1497,57 +1356,40 @@ describe('RecurlyWrapper', function () { \ `) - done() - }) }) - it('should not produce an error', function (done) { - this.call((err, result) => { - expect(err).to.not.be.instanceof(Error) - done() - }) + it('should not produce an error', async function () { + await expect(this.call()).to.be.fulfilled }) - it('should call apiRequest', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.apiRequest.callCount.should.equal(1) - this.apiRequest.firstCall.args[0].method.should.equal('POST') - done() - }) + it('should call apiRequest', async function () { + await this.call() + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('POST') }) - it('should call _parseSubscriptionXml', function (done) { - this.call((err, result) => { - if (err) return done(err) - this.RecurlyWrapper._parseSubscriptionXml.callCount.should.equal(1) - done() - }) + it('should call _parseSubscriptionXml', async function () { + await this.call() + this.RecurlyWrapper.promises._parseSubscriptionXml.callCount.should.equal( + 1 + ) }) - it('should set subscription on cache', function (done) { - this.call((err, result) => { - if (err) return done(err) - expect(result.subscription).to.deep.equal({ - a: '1', - }) - done() + it('should set subscription on cache', async function () { + const result = await this.call() + expect(result.subscription).to.deep.equal({ + a: '1', }) }) }) describe('when apiRequest produces an error', function () { beforeEach(function () { - this.apiRequest.callsArgWith(1, new Error('woops'), { - statusCode: 500, - }) + this.apiRequest.rejects(new Error('woops')) }) - it('should produce an error', function (done) { - this.call((err, result) => { - expect(err).to.be.instanceof(Error) - done() - }) + it('should produce an error', async function () { + await expect(this.call()).to.be.rejected }) }) }) @@ -1556,29 +1398,28 @@ describe('RecurlyWrapper', function () { describe('listAccountActiveSubscriptions', function () { beforeEach(function () { this.user_id = 'mock-user-id' - this.callback = sinon.stub() - this.RecurlyWrapper.apiRequest = sinon + this.response = { mock: 'response' } + this.body = '' + this.RecurlyWrapper.promises.apiRequest = sinon.stub().resolves({ + response: this.response, + body: this.body, + }) + this.subscriptions = ['mock', 'subscriptions'] + this.RecurlyWrapper.promises._parseSubscriptionsXml = sinon .stub() - .yields( - null, - (this.response = { mock: 'response' }), - (this.body = '') - ) - this.RecurlyWrapper._parseSubscriptionsXml = sinon - .stub() - .yields(null, (this.subscriptions = ['mock', 'subscriptions'])) + .resolves(this.subscriptions) }) describe('with an account', function () { - beforeEach(function () { - this.RecurlyWrapper.listAccountActiveSubscriptions( - this.user_id, - this.callback - ) + beforeEach(async function () { + this.result = + await this.RecurlyWrapper.promises.listAccountActiveSubscriptions( + this.user_id + ) }) - it('should send a request to Recurly', function () { - this.RecurlyWrapper.apiRequest + it('should send a request to Recurly', async function () { + this.RecurlyWrapper.promises.apiRequest .calledWith({ url: `accounts/${this.user_id}/subscriptions`, qs: { @@ -1589,22 +1430,22 @@ describe('RecurlyWrapper', function () { .should.equal(true) }) - it('should return the subscriptions', function () { - this.callback.calledWith(null, this.subscriptions).should.equal(true) + it('should return the subscriptions', async function () { + expect(this.result).to.deep.equal(this.subscriptions) }) }) describe('without an account', function () { - beforeEach(function () { + beforeEach(async function () { this.response.statusCode = 404 - this.RecurlyWrapper.listAccountActiveSubscriptions( - this.user_id, - this.callback - ) + this.accountActiveSubscriptions = + await this.RecurlyWrapper.promises.listAccountActiveSubscriptions( + this.user_id + ) }) it('should return an empty array of subscriptions', function () { - this.callback.calledWith(null, []).should.equal(true) + expect(this.accountActiveSubscriptions).to.deep.equal([]) }) }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js index bc84d2815f..11878a60b6 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js @@ -582,7 +582,7 @@ describe('SubscriptionHandler', function () { ) }) - it('should call RecurlyWrapper.listAccountActiveSubscriptions with the user id', function () { + it('should call RecurlyWrapper.promises.listAccountActiveSubscriptions with the user id', function () { this.RecurlyWrapper.promises.listAccountActiveSubscriptions .calledWith(this.user_id) .should.equal(true)