| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611 |
- const { expect } = require('chai')
- const sinon = require('sinon')
- const modulePath = '../../../../app/src/Features/Subscription/RecurlyWrapper'
- const SandboxedModule = require('sandboxed-module')
- const tk = require('timekeeper')
- const Errors = require('../../../../app/src/Features/Errors/Errors')
- const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
- const fixtures = {
- 'subscriptions/44f83d7cba354d5b84812419f923ea96':
- '<?xml version="1.0" encoding="UTF-8"?>' +
- '<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">' +
- ' <account href="https://api.recurly.com/v2/accounts/104"/>' +
- ' <plan href="https://api.recurly.com/v2/plans/gold">' +
- ' <plan_code>gold</plan_code>' +
- ' <name>Gold plan</name>' +
- ' </plan>' +
- ' <uuid>44f83d7cba354d5b84812419f923ea96</uuid>' +
- ' <state>active</state>' +
- ' <unit_amount_in_cents type="integer">800</unit_amount_in_cents>' +
- ' <currency>EUR</currency>' +
- ' <quantity type="integer">1</quantity>' +
- ' <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>' +
- ' <canceled_at nil="nil"></canceled_at>' +
- ' <expires_at nil="nil"></expires_at>' +
- ' <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>' +
- ' <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>' +
- ' <trial_started_at nil="nil"></trial_started_at>' +
- ' <trial_ends_at nil="nil"></trial_ends_at>' +
- ' <subscription_add_ons type="array">' +
- ' <subscription_add_on>' +
- ' <add_on_code>ipaddresses</add_on_code>' +
- ' <quantity>10</quantity>' +
- ' <unit_amount_in_cents>150</unit_amount_in_cents>' +
- ' </subscription_add_on>' +
- ' </subscription_add_ons>' +
- ' <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>' +
- ' <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>' +
- ' <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>' +
- '</subscription>',
- 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5':
- '<?xml version="1.0" encoding="UTF-8"?>' +
- '<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">' +
- ' <account href="https://api.recurly.com/v2/accounts/104"/>' +
- ' <plan href="https://api.recurly.com/v2/plans/gold">' +
- ' <plan_code>gold</plan_code>' +
- ' <name>Gold plan</name>' +
- ' </plan>' +
- ' <uuid>44f83d7cba354d5b84812419f923ea96</uuid>' +
- ' <state>active</state>' +
- ' <unit_amount_in_cents type="integer">800</unit_amount_in_cents>' +
- ' <currency>EUR</currency>' +
- ' <quantity type="integer">1</quantity>' +
- ' <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>' +
- ' <canceled_at nil="nil"></canceled_at>' +
- ' <expires_at nil="nil"></expires_at>' +
- ' <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>' +
- ' <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>' +
- ' <trial_started_at nil="nil"></trial_started_at>' +
- ' <trial_ends_at nil="nil"></trial_ends_at>' +
- ' <subscription_add_ons type="array">' +
- ' <subscription_add_on>' +
- ' <add_on_code>ipaddresses</add_on_code>' +
- ' <quantity>10</quantity>' +
- ' <unit_amount_in_cents>150</unit_amount_in_cents>' +
- ' </subscription_add_on>' +
- ' </subscription_add_ons>' +
- ' <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>' +
- ' <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>' +
- ' <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>' +
- '</subscription>',
- 'accounts/104':
- '<?xml version="1.0" encoding="UTF-8"?>' +
- '<account href="https://api.recurly.com/v2/accounts/104">' +
- ' <adjustments href="https://api.recurly.com/v2/accounts/1/adjustments"/>' +
- ' <billing_info href="https://api.recurly.com/v2/accounts/1/billing_info"/>' +
- ' <invoices href="https://api.recurly.com/v2/accounts/1/invoices"/>' +
- ' <redemption href="https://api.recurly.com/v2/accounts/1/redemption"/>' +
- ' <subscriptions href="https://api.recurly.com/v2/accounts/1/subscriptions"/>' +
- ' <transactions href="https://api.recurly.com/v2/accounts/1/transactions"/>' +
- ' <account_code>104</account_code>' +
- ' <state>active</state>' +
- ' <username nil="nil"></username>' +
- ' <email>verena@example.com</email>' +
- ' <first_name>Verena</first_name>' +
- ' <last_name>Example</last_name>' +
- ' <accept_language nil="nil"></accept_language>' +
- ' <hosted_login_token>a92468579e9c4231a6c0031c4716c01d</hosted_login_token>' +
- ' <created_at type="datetime">2011-10-25T12:00:00</created_at>' +
- '</account>',
- }
- const mockApiRequest = function (options, callback) {
- if (fixtures[options.url]) {
- callback(null, { statusCode: 200 }, fixtures[options.url])
- } else {
- callback(new Error('Not found'), { statusCode: 404 })
- }
- }
- describe('RecurlyWrapper', function () {
- beforeEach(function () {
- this.settings = {
- plans: [
- {
- planCode: 'collaborator',
- name: 'Collaborator',
- features: {
- collaborators: -1,
- versioning: true,
- },
- },
- ],
- defaultPlanCode: {
- collaborators: 0,
- versioning: false,
- },
- apis: {
- recurly: {
- apiKey: 'nonsense',
- privateKey: 'private_nonsense',
- },
- },
- }
- tk.freeze(Date.now()) // freeze the time for these tests
- this.RecurlyWrapper = SandboxedModule.require(modulePath, {
- requires: {
- '@overleaf/settings': this.settings,
- request: sinon.stub(),
- './Errors': SubscriptionErrors,
- },
- })
- })
- afterEach(function () {
- tk.reset()
- })
- 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()
- }
- )
- })
- 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'
- )
- })
- it('should return the subscription', function () {
- this.recurlySubscription.uuid.should.equal(
- '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 look up the subscription at the RecurlyJS API end point', function () {
- this.apiRequest.args[0][0].url.should.equal(
- 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5'
- )
- })
- })
- 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 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) {
- this.recurlyAccountId = 'account-id-123'
- this.newEmail = 'example@overleaf.com'
- this.apiRequest = sinon
- .stub(this.RecurlyWrapper, 'apiRequest')
- .callsFake((options, callback) => {
- this.requestOptions = options
- callback(null, {}, fixtures['accounts/104'])
- })
- this.RecurlyWrapper.updateAccountEmailAddress(
- this.recurlyAccountId,
- this.newEmail,
- (error, recurlyAccount) => {
- if (error) return done(error)
- this.recurlyAccount = recurlyAccount
- done()
- }
- )
- })
- afterEach(function () {
- this.RecurlyWrapper.apiRequest.restore()
- })
- it('sends correct XML', function () {
- this.apiRequest.called.should.equal(true)
- const { body } = this.apiRequest.lastCall.args[0]
- expect(body).to.equal(`\
- <account>
- <email>example@overleaf.com</email>
- </account>\
- `)
- this.requestOptions.url.should.equal(`accounts/${this.recurlyAccountId}`)
- this.requestOptions.method.should.equal('PUT')
- })
- it('should return the updated account', function () {
- expect(this.recurlyAccount).to.exist
- this.recurlyAccount.account_code.should.equal('104')
- })
- })
- describe('updateAccountEmailAddress, with invalid XML', function () {
- beforeEach(function (done) {
- this.recurlyAccountId = 'account-id-123'
- this.newEmail = '\uD800@example.com'
- this.apiRequest = sinon
- .stub(this.RecurlyWrapper, 'apiRequest')
- .callsFake((options, callback) => {
- this.requestOptions = options
- callback(null, {}, fixtures['accounts/104'])
- })
- done()
- })
- afterEach(function () {
- this.RecurlyWrapper.apiRequest.restore()
- })
- it('should produce an error', function (done) {
- this.RecurlyWrapper.updateAccountEmailAddress(
- this.recurlyAccountId,
- this.newEmail,
- (error, recurlyAccount) => {
- 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) {
- this.recurlySubscriptionId = 'subscription-id-123'
- this.apiRequest = sinon
- .stub(this.RecurlyWrapper, 'apiRequest')
- .callsFake((options, callback) => {
- this.requestOptions = options
- callback(
- null,
- {},
- fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96']
- )
- })
- this.RecurlyWrapper.updateSubscription(
- this.recurlySubscriptionId,
- { plan_code: 'silver', timeframe: 'now' },
- (error, recurlySubscription) => {
- if (error) return done(error)
- this.recurlySubscription = recurlySubscription
- done()
- }
- )
- })
- afterEach(function () {
- this.RecurlyWrapper.apiRequest.restore()
- })
- it('sends correct XML', function () {
- this.apiRequest.called.should.equal(true)
- const { body } = this.apiRequest.lastCall.args[0]
- expect(body).to.equal(`\
- <subscription>
- <plan_code>silver</plan_code>
- <timeframe>now</timeframe>
- </subscription>\
- `)
- this.requestOptions.url.should.equal(
- `subscriptions/${this.recurlySubscriptionId}`
- )
- this.requestOptions.method.should.equal('put')
- })
- it('should return the updated subscription', function () {
- expect(this.recurlySubscription).to.exist
- this.recurlySubscription.plan.plan_code.should.equal('gold')
- })
- })
- describe('redeemCoupon', function () {
- beforeEach(function (done) {
- this.recurlyAccountId = 'account-id-123'
- this.coupon_code = '312321312'
- this.apiRequest = sinon
- .stub(this.RecurlyWrapper, 'apiRequest')
- .callsFake((options, callback) => {
- options.url.should.equal(`coupons/${this.coupon_code}/redeem`)
- options.method.should.equal('post')
- callback()
- })
- this.RecurlyWrapper.redeemCoupon(
- this.recurlyAccountId,
- this.coupon_code,
- done
- )
- })
- afterEach(function () {
- this.RecurlyWrapper.apiRequest.restore()
- })
- it('sends correct XML', function () {
- this.apiRequest.called.should.equal(true)
- const { body } = this.apiRequest.lastCall.args[0]
- expect(body).to.equal(`\
- <redemption>
- <account_code>account-id-123</account_code>
- <currency>USD</currency>
- </redemption>\
- `)
- })
- })
- describe('createFixedAmmountCoupon', function () {
- beforeEach(function (done) {
- 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(
- this.couponCode,
- this.couponName,
- this.currencyCode,
- this.discount,
- this.planCode,
- done
- )
- })
- afterEach(function () {
- this.RecurlyWrapper.apiRequest.restore()
- })
- it('sends correct XML', function () {
- this.apiRequest.called.should.equal(true)
- const { body } = this.apiRequest.lastCall.args[0]
- expect(body).to.equal(`\
- <coupon>
- <coupon_code>a-coupon-code</coupon_code>
- <name>a-coupon-name</name>
- <discount_type>dollars</discount_type>
- <discount_in_cents>
- <EUR>1337</EUR>
- </discount_in_cents>
- <plan_codes>
- <plan_code>a-plan-code</plan_code>
- </plan_codes>
- <applies_to_all_plans>false</applies_to_all_plans>
- </coupon>\
- `)
- })
- })
- describe('createSubscription', function () {
- beforeEach(function () {
- this.user = {
- _id: 'some_id',
- email: 'user@example.com',
- }
- this.subscriptionDetails = {
- currencyCode: 'EUR',
- plan_code: 'some_plan_code',
- coupon_code: '',
- isPaypal: true,
- address: {
- address1: 'addr_one',
- address2: 'addr_two',
- country: 'some_country',
- state: 'some_state',
- zip: 'some_zip',
- },
- }
- this.subscription = {}
- this.recurlyTokenIds = {
- billing: 'a-token-id',
- threeDSecureActionResult: 'a-3d-token-id',
- }
- this.call = callback => {
- this.RecurlyWrapper.createSubscription(
- this.user,
- this.subscriptionDetails,
- this.recurlyTokenIds,
- callback
- )
- }
- })
- describe('when paypal', function () {
- beforeEach(function () {
- this.subscriptionDetails.isPaypal = true
- this._createPaypalSubscription = sinon.stub(
- this.RecurlyWrapper,
- '_createPaypalSubscription'
- )
- this._createPaypalSubscription.callsArgWith(3, null, 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 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 call _createPaypalSubscription', function (done) {
- this.call((err, sub) => {
- if (err) return done(err)
- this._createPaypalSubscription.callCount.should.equal(1)
- done()
- })
- })
- describe('when _createPaypalSubscription produces an error', function () {
- beforeEach(function () {
- this._createPaypalSubscription.callsArgWith(3, new Error('woops'))
- })
- it('should produce an error', function (done) {
- this.call((err, sub) => {
- expect(err).to.be.instanceof(Error)
- done()
- })
- })
- })
- })
- describe('when not paypal', function () {
- beforeEach(function () {
- this.subscriptionDetails.isPaypal = false
- this._createCreditCardSubscription = sinon.stub(
- this.RecurlyWrapper,
- '_createCreditCardSubscription'
- )
- this._createCreditCardSubscription.callsArgWith(
- 3,
- null,
- 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 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 call _createCreditCardSubscription', function (done) {
- this.call((err, sub) => {
- if (err) return done(err)
- this._createCreditCardSubscription.callCount.should.equal(1)
- done()
- })
- })
- describe('when _createCreditCardSubscription produces an error', function () {
- beforeEach(function () {
- this._createCreditCardSubscription.callsArgWith(3, new Error('woops'))
- })
- it('should produce an error', function (done) {
- this.call((err, sub) => {
- expect(err).to.be.instanceof(Error)
- done()
- })
- })
- })
- })
- })
- describe('_createCreditCardSubscription', function () {
- beforeEach(function () {
- this.user = {
- _id: 'some_id',
- email: 'user@example.com',
- first_name: 'Foo',
- last_name: 'Johnson',
- }
- this.subscriptionDetails = {
- currencyCode: 'EUR',
- plan_code: 'some_plan_code',
- coupon_code: '',
- isPaypal: true,
- first_name: 'Prairie',
- address: {
- address1: 'addr_one',
- address2: 'addr_two',
- country: 'some_country',
- state: 'some_state',
- zip: 'some_zip',
- },
- ITMCampaign: 'itm-campaign-value',
- ITMContent: 'itm-content-value',
- ITMReferrer: 'itm-referrer-value',
- }
- this.subscription = {}
- this.recurlyTokenIds = {
- billing: 'a-token-id',
- threeDSecureActionResult: 'a-3d-token-id',
- }
- this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest')
- this.response = { statusCode: 200 }
- this.body = '<xml>is_bad</xml>'
- this.apiRequest.callsArgWith(1, null, this.response, this.body)
- this._parseSubscriptionXml = sinon.stub(
- this.RecurlyWrapper,
- '_parseSubscriptionXml'
- )
- this._parseSubscriptionXml.callsArgWith(1, null, this.subscription)
- this.call = callback => {
- this.RecurlyWrapper._createCreditCardSubscription(
- this.user,
- this.subscriptionDetails,
- this.recurlyTokenIds,
- callback
- )
- }
- })
- afterEach(function () {
- this.apiRequest.restore()
- 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(`\
- <subscription>
- <plan_code>some_plan_code</plan_code>
- <currency>EUR</currency>
- <coupon_code/>
- <account>
- <account_code>some_id</account_code>
- <email>user@example.com</email>
- <first_name>Prairie</first_name>
- <last_name>Johnson</last_name>
- <billing_info>
- <token_id>a-token-id</token_id>
- <three_d_secure_action_result_token_id>a-3d-token-id</three_d_secure_action_result_token_id>
- </billing_info>
- </account>
- <custom_fields>
- <custom_field>
- <name>itm_campaign</name>
- <value>itm-campaign-value</value>
- </custom_field>
- <custom_field>
- <name>itm_content</name>
- <value>itm-content-value</value>
- </custom_field>
- <custom_field>
- <name>itm_referrer</name>
- <value>itm-referrer-value</value>
- </custom_field>
- </custom_fields>
- </subscription>\
- `)
- 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 produce a subscription', function (done) {
- this.call((err, sub) => {
- if (err) return done(err)
- expect(sub).to.equal(this.subscription)
- done()
- })
- })
- 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 _parseSubscriptionXml', function (done) {
- this.call((err, sub) => {
- if (err) return done(err)
- this._parseSubscriptionXml.callCount.should.equal(1)
- done()
- })
- })
- describe('when api request returns 422', function () {
- beforeEach(function () {
- const body = `\
- <?xml version="1.0" encoding="UTF-8"?>
- <errors>
- <transaction_error>
- <error_code>three_d_secure_action_required</error_code>
- <error_category>3d_secure_action_required</error_category>
- <merchant_message>Your payment gateway is requesting that the transaction be completed with 3D Secure in accordance with PSD2.</merchant_message>
- <customer_message>Your card must be authenticated with 3D Secure before continuing.</customer_message>
- <gateway_error_code nil="nil"></gateway_error_code>
- <three_d_secure_action_token_id>mock_three_d_secure_action_token</three_d_secure_action_token_id>
- </transaction_error>
- <error field="subscription.account.base" symbol="three_d_secure_action_required">Your card must be authenticated with 3D Secure before continuing.</error>
- </errors>
- `
- this.apiRequest.yields(null, { statusCode: 422 }, body)
- })
- it('should produce an error', function (done) {
- this.call((err, sub) => {
- expect(err).to.be.instanceof(
- SubscriptionErrors.RecurlyTransactionError
- )
- expect(err.info.public.message).to.be.equal(
- 'Your card must be authenticated with 3D Secure before continuing.'
- )
- expect(err.info.public.threeDSecureActionTokenId).to.be.equal(
- 'mock_three_d_secure_action_token'
- )
- done()
- })
- })
- })
- describe('when api request produces an error', function () {
- beforeEach(function () {
- this.apiRequest.callsArgWith(1, new Error('woops'))
- })
- it('should produce an error', function (done) {
- this.call((err, sub) => {
- expect(err).to.be.instanceof(Error)
- done()
- })
- })
- it('should call apiRequest', function (done) {
- this.call(() => {
- this.apiRequest.callCount.should.equal(1)
- done()
- })
- })
- it('should not _parseSubscriptionXml', function (done) {
- this.call(() => {
- this._parseSubscriptionXml.callCount.should.equal(0)
- done()
- })
- })
- })
- describe('when parse xml produces an error', function () {
- beforeEach(function () {
- this._parseSubscriptionXml.callsArgWith(1, new Error('woops'))
- })
- it('should produce an error', function (done) {
- this.call((err, sub) => {
- expect(err).to.be.instanceof(Error)
- done()
- })
- })
- })
- })
- describe('_createPaypalSubscription', function () {
- beforeEach(function () {
- this.checkAccountExists = sinon.stub(
- this.RecurlyWrapper._paypal,
- 'checkAccountExists'
- )
- this.createAccount = sinon.stub(
- this.RecurlyWrapper._paypal,
- 'createAccount'
- )
- this.createBillingInfo = sinon.stub(
- this.RecurlyWrapper._paypal,
- 'createBillingInfo'
- )
- this.setAddressAndCompanyBillingInfo = sinon.stub(
- this.RecurlyWrapper._paypal,
- 'setAddressAndCompanyBillingInfo'
- )
- this.createSubscription = sinon.stub(
- this.RecurlyWrapper._paypal,
- 'createSubscription'
- )
- this.user = {
- _id: 'some_id',
- email: 'user@example.com',
- }
- this.subscriptionDetails = {
- currencyCode: 'EUR',
- plan_code: 'some_plan_code',
- coupon_code: '',
- isPaypal: true,
- address: {
- address1: 'addr_one',
- address2: 'addr_two',
- country: 'some_country',
- state: 'some_state',
- zip: 'some_zip',
- },
- }
- this.subscription = {}
- this.recurlyTokenIds = {
- billing: 'a-token-id',
- threeDSecureActionResult: 'a-3d-token-id',
- }
- // set up data callbacks
- const { user } = this
- const { subscriptionDetails } = this
- const { recurlyTokenIds } = this
- this.checkAccountExists.callsArgWith(1, null, {
- user,
- subscriptionDetails,
- recurlyTokenIds,
- userExists: false,
- account: { accountCode: 'xx' },
- })
- this.createAccount.callsArgWith(1, null, {
- user,
- subscriptionDetails,
- recurlyTokenIds,
- userExists: false,
- account: { accountCode: 'xx' },
- })
- this.createBillingInfo.callsArgWith(1, null, {
- user,
- subscriptionDetails,
- recurlyTokenIds,
- userExists: false,
- account: { accountCode: 'xx' },
- billingInfo: { token_id: 'abc' },
- })
- this.setAddressAndCompanyBillingInfo.callsArgWith(1, null, {
- user,
- subscriptionDetails,
- recurlyTokenIds,
- userExists: false,
- account: { accountCode: 'xx' },
- billingInfo: { token_id: 'abc' },
- })
- this.createSubscription.callsArgWith(1, null, {
- user,
- subscriptionDetails,
- recurlyTokenIds,
- userExists: false,
- account: { accountCode: 'xx' },
- billingInfo: { token_id: 'abc' },
- subscription: this.subscription,
- })
- this.call = callback => {
- this.RecurlyWrapper._createPaypalSubscription(
- this.user,
- this.subscriptionDetails,
- this.recurlyTokenIds,
- callback
- )
- }
- })
- afterEach(function () {
- this.checkAccountExists.restore()
- this.createAccount.restore()
- this.createBillingInfo.restore()
- this.setAddressAndCompanyBillingInfo.restore()
- 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 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 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()
- })
- })
- describe('when one of the paypal stages produces an error', function () {
- beforeEach(function () {
- this.createAccount.callsArgWith(1, new Error('woops'))
- })
- it('should produce an error', function (done) {
- this.call((err, sub) => {
- expect(err).to.be.instanceof(Error)
- done()
- })
- })
- 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()
- })
- })
- })
- })
- describe('paypal actions', function () {
- beforeEach(function () {
- this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest')
- this._parseAccountXml = sinon.spy(this.RecurlyWrapper, '_parseAccountXml')
- this._parseBillingInfoXml = sinon.spy(
- this.RecurlyWrapper,
- '_parseBillingInfoXml'
- )
- this._parseSubscriptionXml = sinon.spy(
- this.RecurlyWrapper,
- '_parseSubscriptionXml'
- )
- this.cache = {
- user: (this.user = {
- _id: 'some_id',
- email: 'foo@bar.com',
- first_name: 'Foo',
- last_name: 'Bar',
- }),
- recurlyTokenIds: (this.recurlyTokenIds = {
- billing: 'a-token-id',
- threeDSecureActionResult: 'a-3d-token-id',
- }),
- subscriptionDetails: (this.subscriptionDetails = {
- currencyCode: 'EUR',
- plan_code: 'some_plan_code',
- coupon_code: '',
- isPaypal: true,
- address: {
- address1: 'addr_one',
- address2: 'addr_two',
- city: 'some_city',
- country: 'some_country',
- state: 'some_state',
- zip: 'some_zip',
- },
- ITMCampaign: 'itm-campaign-value',
- ITMContent: 'itm-content-value',
- }),
- }
- })
- afterEach(function () {
- this.apiRequest.restore()
- this._parseAccountXml.restore()
- this._parseBillingInfoXml.restore()
- this._parseSubscriptionXml.restore()
- })
- describe('_paypal.checkAccountExists', function () {
- beforeEach(function () {
- this.call = callback => {
- this.RecurlyWrapper._paypal.checkAccountExists(this.cache, callback)
- }
- })
- describe('when the account exists', function () {
- beforeEach(function () {
- const resultXml =
- '<account><account_code>abc</account_code></account>'
- 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()
- })
- })
- it('should call apiRequest', function (done) {
- this.call((err, result) => {
- if (err) return done(err)
- this.apiRequest.callCount.should.equal(1)
- done()
- })
- })
- 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()
- })
- })
- })
- 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()
- })
- })
- 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 call _parseAccountXml', function (done) {
- this.call((err, result) => {
- if (err) return done(err)
- this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0)
- done()
- })
- })
- 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 set userExists to false', function (done) {
- this.call((err, result) => {
- if (err) return done(err)
- expect(result.userExists).to.equal(false)
- 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.createAccount', function () {
- beforeEach(function () {
- this.call = callback => {
- this.RecurlyWrapper._paypal.createAccount(this.cache, callback)
- }
- })
- describe('when address is missing from subscriptionDetails', function () {
- beforeEach(function () {
- this.cache.subscriptionDetails.address = null
- })
- it('should produce an error', function (done) {
- this.call((err, result) => {
- expect(err).to.be.instanceof(Error)
- done()
- })
- })
- })
- describe('when country is missing from address', function () {
- beforeEach(function () {
- this.cache.subscriptionDetails.address = {}
- })
- it('should produce an error', function (done) {
- this.call((err, result) => {
- expect(err).to.be.instanceof(Errors.InvalidError)
- done()
- })
- })
- })
- describe('when account already exists', function () {
- beforeEach(function () {
- this.cache.userExists = true
- 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 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', 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()
- })
- })
- })
- describe('when account does not exist', function () {
- beforeEach(function () {
- this.cache.userExists = false
- const resultXml =
- '<account><account_code>abc</account_code></account>'
- 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(`\
- <account>
- <account_code>some_id</account_code>
- <email>foo@bar.com</email>
- <first_name>Foo</first_name>
- <last_name>Bar</last_name>
- <address>
- <address1>addr_one</address1>
- <address2>addr_two</address2>
- <city>some_city</city>
- <state>some_state</state>
- <zip>some_zip</zip>
- <country>some_country</country>
- </address>
- </account>\
- `)
- 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 _parseAccountXml', function (done) {
- this.call((err, result) => {
- if (err) return done(err)
- this.RecurlyWrapper._parseAccountXml.callCount.should.equal(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.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 = '<billing_info><a>1</a></billing_info>'
- 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(`\
- <billing_info>
- <token_id>a-token-id</token_id>
- </billing_info>\
- `)
- 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
- )
- }
- })
- 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 country is missing', function () {
- beforeEach(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()
- })
- })
- })
- describe('when all goes well', function () {
- beforeEach(function () {
- const resultXml = '<billing_info><city>London</city></billing_info>'
- 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(`\
- <billing_info>
- <address1>addr_one</address1>
- <address2>addr_two</address2>
- <city>some_city</city>
- <state>some_state</state>
- <zip>some_zip</zip>
- <country>some_country</country>
- </billing_info>\
- `)
- 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('PUT')
- 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({
- city: 'London',
- })
- 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.createSubscription', function () {
- beforeEach(function () {
- this.cache.account = { account_code: 'abc' }
- this.cache.billingInfo = {}
- this.call = callback => {
- this.RecurlyWrapper._paypal.createSubscription(this.cache, callback)
- }
- })
- describe('when all goes well', function () {
- beforeEach(function () {
- const resultXml = '<subscription><a>1</a></subscription>'
- 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(`\
- <subscription>
- <plan_code>some_plan_code</plan_code>
- <currency>EUR</currency>
- <coupon_code/>
- <account>
- <account_code>some_id</account_code>
- </account>
- <custom_fields>
- <custom_field>
- <name>itm_campaign</name>
- <value>itm-campaign-value</value>
- </custom_field>
- <custom_field>
- <name>itm_content</name>
- <value>itm-content-value</value>
- </custom_field>
- </custom_fields>
- </subscription>\
- `)
- 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 _parseSubscriptionXml', function (done) {
- this.call((err, result) => {
- if (err) return done(err)
- this.RecurlyWrapper._parseSubscriptionXml.callCount.should.equal(1)
- done()
- })
- })
- 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()
- })
- })
- })
- 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('listAccountActiveSubscriptions', function () {
- beforeEach(function () {
- this.user_id = 'mock-user-id'
- this.callback = sinon.stub()
- this.RecurlyWrapper.apiRequest = sinon
- .stub()
- .yields(
- null,
- (this.response = { mock: 'response' }),
- (this.body = '<mock body/>')
- )
- this.RecurlyWrapper._parseSubscriptionsXml = sinon
- .stub()
- .yields(null, (this.subscriptions = ['mock', 'subscriptions']))
- })
- describe('with an account', function () {
- beforeEach(function () {
- this.RecurlyWrapper.listAccountActiveSubscriptions(
- this.user_id,
- this.callback
- )
- })
- it('should send a request to Recurly', function () {
- this.RecurlyWrapper.apiRequest
- .calledWith({
- url: `accounts/${this.user_id}/subscriptions`,
- qs: {
- state: 'active',
- },
- expect404: true,
- })
- .should.equal(true)
- })
- it('should return the subscriptions', function () {
- this.callback.calledWith(null, this.subscriptions).should.equal(true)
- })
- })
- describe('without an account', function () {
- beforeEach(function () {
- this.response.statusCode = 404
- this.RecurlyWrapper.listAccountActiveSubscriptions(
- this.user_id,
- this.callback
- )
- })
- it('should return an empty array of subscriptions', function () {
- this.callback.calledWith(null, []).should.equal(true)
- })
- })
- })
- })
|