RecurlyClientTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const recurly = require('recurly')
  4. const SandboxedModule = require('sandboxed-module')
  5. const {
  6. PaymentProviderSubscription,
  7. PaymentProviderSubscriptionChangeRequest,
  8. PaymentProviderSubscriptionUpdateRequest,
  9. PaymentProviderSubscriptionAddOnUpdate,
  10. PaymentProviderAccount,
  11. PaymentProviderCoupon,
  12. } = require('../../../../app/src/Features/Subscription/PaymentProviderEntities')
  13. const MODULE_PATH = '../../../../app/src/Features/Subscription/RecurlyClient'
  14. describe('RecurlyClient', function () {
  15. beforeEach(function () {
  16. this.settings = {
  17. apis: {
  18. recurly: {
  19. apiKey: 'nonsense',
  20. privateKey: 'private_nonsense',
  21. subdomain: 'test',
  22. },
  23. },
  24. plans: [],
  25. features: [],
  26. }
  27. this.user = { _id: '123456', email: 'joe@example.com', first_name: 'Joe' }
  28. this.subscriptionChange = { id: 'subscription-change-123' }
  29. this.recurlyAccount = new recurly.Account()
  30. Object.assign(this.recurlyAccount, {
  31. code: this.user._id,
  32. email: this.user.email,
  33. })
  34. this.subscriptionAddOn = {
  35. code: 'addon-code',
  36. name: 'My Add-On',
  37. quantity: 1,
  38. unitPrice: 2,
  39. preTaxTotal: 2,
  40. }
  41. this.subscription = new PaymentProviderSubscription({
  42. id: 'subscription-id',
  43. userId: 'user-id',
  44. currency: 'EUR',
  45. planCode: 'plan-code',
  46. planName: 'plan-name',
  47. planPrice: 13,
  48. addOns: [this.subscriptionAddOn],
  49. subtotal: 15,
  50. taxRate: 0.1,
  51. taxAmount: 1.5,
  52. total: 16.5,
  53. periodStart: new Date(),
  54. periodEnd: new Date(),
  55. collectionMethod: 'automatic',
  56. netTerms: 0,
  57. poNumber: '',
  58. termsAndConditions: '',
  59. })
  60. this.recurlySubscription = {
  61. uuid: this.subscription.id,
  62. account: {
  63. code: this.subscription.userId,
  64. },
  65. plan: {
  66. code: this.subscription.planCode,
  67. name: this.subscription.planName,
  68. },
  69. addOns: [
  70. {
  71. addOn: {
  72. code: this.subscriptionAddOn.code,
  73. name: this.subscriptionAddOn.name,
  74. },
  75. quantity: this.subscriptionAddOn.quantity,
  76. unitAmount: this.subscriptionAddOn.unitPrice,
  77. },
  78. ],
  79. unitAmount: this.subscription.planPrice,
  80. subtotal: this.subscription.subtotal,
  81. taxInfo: { rate: this.subscription.taxRate },
  82. tax: this.subscription.taxAmount,
  83. total: this.subscription.total,
  84. currency: this.subscription.currency,
  85. currentPeriodStartedAt: this.subscription.periodStart,
  86. currentPeriodEndsAt: this.subscription.periodEnd,
  87. collectionMethod: this.subscription.collectionMethod,
  88. netTerms: this.subscription.netTerms,
  89. poNumber: this.subscription.poNumber,
  90. termsAndConditions: this.subscription.termsAndConditions,
  91. }
  92. this.recurlySubscriptionChange = new recurly.SubscriptionChange()
  93. Object.assign(this.recurlySubscriptionChange, this.subscriptionChange)
  94. this.UserGetter = {
  95. promises: {
  96. getUser: sinon.stub().callsFake(userId => {
  97. if (userId === this.user._id) {
  98. return this.user
  99. }
  100. }),
  101. },
  102. }
  103. let client
  104. this.client = client = {
  105. getAccount: sinon.stub(),
  106. getBillingInfo: sinon.stub(),
  107. listAccountSubscriptions: sinon.stub(),
  108. listActiveCouponRedemptions: sinon.stub(),
  109. previewSubscriptionChange: sinon.stub(),
  110. }
  111. this.recurly = {
  112. errors: recurly.errors,
  113. Client: function () {
  114. return client
  115. },
  116. }
  117. this.Errors = {
  118. MissingBillingInfoError: class extends Error {},
  119. SubtotalLimitExceededError: class extends Error {},
  120. }
  121. return (this.RecurlyClient = SandboxedModule.require(MODULE_PATH, {
  122. globals: {
  123. console,
  124. },
  125. requires: {
  126. '@overleaf/settings': this.settings,
  127. recurly: this.recurly,
  128. '@overleaf/logger': {
  129. err: sinon.stub(),
  130. error: sinon.stub(),
  131. warn: sinon.stub(),
  132. log: sinon.stub(),
  133. debug: sinon.stub(),
  134. },
  135. '../User/UserGetter': this.UserGetter,
  136. './Errors': this.Errors,
  137. '../../models/Subscription': {},
  138. },
  139. }))
  140. })
  141. describe('initalizing recurly client with undefined API key parameter', function () {
  142. it('should create a client without error', function () {
  143. let testClient
  144. expect(() => {
  145. testClient = new recurly.Client(undefined)
  146. }).to.not.throw()
  147. expect(testClient).to.be.instanceOf(recurly.Client)
  148. })
  149. })
  150. describe('getAccountForUserId', function () {
  151. it('should return an Account if one exists', async function () {
  152. this.client.getAccount = sinon.stub().resolves(this.recurlyAccount)
  153. const account = await this.RecurlyClient.promises.getAccountForUserId(
  154. this.user._id
  155. )
  156. const expectedAccount = new PaymentProviderAccount({
  157. code: this.user._id,
  158. email: this.user.email,
  159. hasPastDueInvoice: false,
  160. })
  161. expect(account).to.deep.equal(expectedAccount)
  162. })
  163. it('should return null if no account found', async function () {
  164. this.client.getAccount = sinon
  165. .stub()
  166. .throws(new recurly.errors.NotFoundError())
  167. const account =
  168. await this.RecurlyClient.promises.getAccountForUserId('nonsense')
  169. expect(account).to.equal(null)
  170. })
  171. it('should re-throw caught errors', async function () {
  172. this.client.getAccount = sinon.stub().throws()
  173. await expect(
  174. this.RecurlyClient.promises.getAccountForUserId(this.user._id)
  175. ).to.eventually.be.rejectedWith(Error)
  176. })
  177. })
  178. describe('createAccountForUserId', function () {
  179. it('should return the Account as created by recurly', async function () {
  180. this.client.createAccount = sinon.stub().resolves(this.recurlyAccount)
  181. await expect(
  182. this.RecurlyClient.promises.createAccountForUserId(this.user._id)
  183. )
  184. .to.eventually.be.an.instanceOf(recurly.Account)
  185. .that.has.property('code', this.user._id)
  186. })
  187. it('should throw any API errors', async function () {
  188. this.client.createAccount = sinon.stub().throws()
  189. await expect(
  190. this.RecurlyClient.promises.createAccountForUserId(this.user._id)
  191. ).to.eventually.be.rejectedWith(Error)
  192. })
  193. })
  194. describe('getActiveCouponsForUserId', function () {
  195. it('should return an empty array if no coupons returned', async function () {
  196. this.client.listActiveCouponRedemptions.returns({
  197. each: async function* () {},
  198. })
  199. const coupons =
  200. await this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  201. expect(coupons).to.deep.equal([])
  202. })
  203. it('should return a coupons returned by recurly', async function () {
  204. const recurlyCoupon = {
  205. coupon: {
  206. code: 'coupon-code',
  207. name: 'Coupon Name',
  208. hostedPageDescription: 'hosted page description',
  209. invoiceDescription: 'invoice description',
  210. },
  211. }
  212. this.client.listActiveCouponRedemptions.returns({
  213. each: async function* () {
  214. yield recurlyCoupon
  215. },
  216. })
  217. const coupons =
  218. await this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  219. const expectedCoupons = [
  220. new PaymentProviderCoupon({
  221. code: 'coupon-code',
  222. name: 'Coupon Name',
  223. description: 'hosted page description',
  224. }),
  225. ]
  226. expect(coupons).to.deep.equal(expectedCoupons)
  227. })
  228. it('should not throw for Recurly not found error', async function () {
  229. this.client.listActiveCouponRedemptions = sinon
  230. .stub()
  231. .throws(new recurly.errors.NotFoundError())
  232. const coupons =
  233. await this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  234. expect(coupons).to.deep.equal([])
  235. })
  236. it('should throw any other API errors', async function () {
  237. this.client.listActiveCouponRedemptions = sinon.stub().throws()
  238. await expect(
  239. this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  240. ).to.eventually.be.rejectedWith(Error)
  241. })
  242. })
  243. describe('getCustomerManagementLink', function () {
  244. it('should throw if recurly token is not returned', async function () {
  245. this.client.getAccount.resolves({})
  246. await expect(
  247. this.RecurlyClient.promises.getCustomerManagementLink(
  248. '12345',
  249. 'account-management',
  250. 'en-US'
  251. )
  252. ).to.be.rejectedWith('recurly account does not have hosted login token')
  253. })
  254. it('should generate the correct account management url', async function () {
  255. this.client.getAccount.resolves({
  256. hostedLoginToken: '987654321',
  257. })
  258. const result =
  259. await this.RecurlyClient.promises.getCustomerManagementLink(
  260. '12345',
  261. 'account-management',
  262. 'en-US'
  263. )
  264. expect(result).to.equal('https://test.recurly.com/account/987654321')
  265. })
  266. it('should generate the correct billing details url', async function () {
  267. this.client.getAccount.resolves({
  268. hostedLoginToken: '987654321',
  269. })
  270. const result =
  271. await this.RecurlyClient.promises.getCustomerManagementLink(
  272. '12345',
  273. 'billing-details',
  274. 'en-US'
  275. )
  276. expect(result).to.equal(
  277. 'https://test.recurly.com/account/billing_info/edit?ht=987654321'
  278. )
  279. })
  280. })
  281. describe('getSubscription', function () {
  282. it('should return the subscription found by recurly', async function () {
  283. this.client.getSubscription = sinon
  284. .stub()
  285. .withArgs('uuid-subscription-id')
  286. .resolves(this.recurlySubscription)
  287. const subscription = await this.RecurlyClient.promises.getSubscription(
  288. this.subscription.id
  289. )
  290. expect(subscription).to.deep.equal(this.subscription)
  291. })
  292. it('should throw any API errors', async function () {
  293. this.client.getSubscription = sinon.stub().throws()
  294. await expect(
  295. this.RecurlyClient.promises.getSubscription(this.user._id)
  296. ).to.eventually.be.rejectedWith(Error)
  297. })
  298. })
  299. describe('getSubscriptionForUser', function () {
  300. it("should return null if the account doesn't exist", async function () {
  301. this.client.listAccountSubscriptions.returns({
  302. // eslint-disable-next-line require-yield
  303. each: async function* () {
  304. throw new recurly.errors.NotFoundError('account not found')
  305. },
  306. })
  307. const subscription =
  308. await this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  309. expect(subscription).to.be.null
  310. })
  311. it("should return null if the account doesn't have subscriptions", async function () {
  312. this.client.listAccountSubscriptions.returns({
  313. each: async function* () {},
  314. })
  315. const subscription =
  316. await this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  317. expect(subscription).to.be.null
  318. })
  319. it('should return the subscription if the account has one subscription', async function () {
  320. const recurlySubscription = this.recurlySubscription
  321. this.client.listAccountSubscriptions.returns({
  322. each: async function* () {
  323. yield recurlySubscription
  324. },
  325. })
  326. const subscription =
  327. await this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  328. expect(subscription).to.deep.equal(this.subscription)
  329. })
  330. it('should throw an error if the account has more than one subscription', async function () {
  331. const recurlySubscription = this.recurlySubscription
  332. this.client.listAccountSubscriptions.returns({
  333. each: async function* () {
  334. yield recurlySubscription
  335. yield { another: 'subscription' }
  336. },
  337. })
  338. await expect(
  339. this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  340. ).to.be.rejected
  341. })
  342. })
  343. describe('applySubscriptionChangeRequest', function () {
  344. beforeEach(function () {
  345. this.client.createSubscriptionChange = sinon
  346. .stub()
  347. .resolves(this.recurlySubscriptionChange)
  348. })
  349. it('handles plan changes', async function () {
  350. await this.RecurlyClient.promises.applySubscriptionChangeRequest(
  351. new PaymentProviderSubscriptionChangeRequest({
  352. subscription: this.subscription,
  353. timeframe: 'now',
  354. planCode: 'new-plan',
  355. })
  356. )
  357. expect(this.client.createSubscriptionChange).to.be.calledWith(
  358. 'uuid-subscription-id',
  359. { timeframe: 'now', planCode: 'new-plan' }
  360. )
  361. })
  362. it('handles add-on changes', async function () {
  363. await this.RecurlyClient.promises.applySubscriptionChangeRequest(
  364. new PaymentProviderSubscriptionChangeRequest({
  365. subscription: this.subscription,
  366. timeframe: 'now',
  367. addOnUpdates: [
  368. new PaymentProviderSubscriptionAddOnUpdate({
  369. code: 'new-add-on',
  370. quantity: 2,
  371. unitPrice: 8.99,
  372. }),
  373. ],
  374. })
  375. )
  376. expect(this.client.createSubscriptionChange).to.be.calledWith(
  377. 'uuid-subscription-id',
  378. {
  379. timeframe: 'now',
  380. addOns: [{ code: 'new-add-on', quantity: 2, unitAmount: 8.99 }],
  381. }
  382. )
  383. })
  384. it('should throw any API errors', async function () {
  385. this.client.createSubscriptionChange = sinon.stub().throws()
  386. await expect(
  387. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  388. subscription: this.subscription,
  389. })
  390. ).to.eventually.be.rejectedWith(Error)
  391. })
  392. it('should throw SubtotalLimitExceededError', async function () {
  393. class ValidationError extends recurly.errors.ValidationError {
  394. constructor() {
  395. super()
  396. this.params = [{ param: 'subtotal_amount_in_cents' }]
  397. }
  398. }
  399. this.client.createSubscriptionChange = sinon
  400. .stub()
  401. .throws(new ValidationError())
  402. await expect(
  403. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  404. subscription: this.subscription,
  405. })
  406. ).to.be.rejectedWith(this.Errors.SubtotalLimitExceededError)
  407. })
  408. it('should rethrow errors different than SubtotalLimitExceededError', async function () {
  409. this.client.createSubscriptionChange = sinon.stub().throws(new Error())
  410. await expect(
  411. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  412. subscription: this.subscription,
  413. })
  414. ).to.be.rejectedWith(Error)
  415. })
  416. })
  417. describe('updateSubscriptionDetails', function () {
  418. beforeEach(function () {
  419. this.client.updateSubscription = sinon
  420. .stub()
  421. .resolves({ id: this.subscription.id })
  422. })
  423. it('handles subscription update', async function () {
  424. await this.RecurlyClient.promises.updateSubscriptionDetails(
  425. new PaymentProviderSubscriptionUpdateRequest({
  426. subscription: this.subscription,
  427. poNumber: '012345',
  428. termsAndConditions: 'T&C',
  429. })
  430. )
  431. expect(this.client.updateSubscription).to.be.calledWith(
  432. 'uuid-subscription-id',
  433. { poNumber: '012345', termsAndConditions: 'T&C' }
  434. )
  435. })
  436. it('should throw any API errors', async function () {
  437. this.client.updateSubscription = sinon.stub().throws()
  438. await expect(
  439. this.RecurlyClient.promises.updateSubscriptionDetails({
  440. subscription: this.subscription,
  441. })
  442. ).to.eventually.be.rejectedWith(Error)
  443. })
  444. })
  445. describe('removeSubscriptionChange', function () {
  446. beforeEach(function () {
  447. this.client.removeSubscriptionChange = sinon.stub().resolves()
  448. })
  449. it('should attempt to remove a pending subscription change', async function () {
  450. this.RecurlyClient.promises.removeSubscriptionChange(
  451. this.subscription.id,
  452. {}
  453. )
  454. expect(this.client.removeSubscriptionChange).to.be.calledWith(
  455. this.subscription.id
  456. )
  457. })
  458. it('should throw any API errors', async function () {
  459. this.client.removeSubscriptionChange = sinon.stub().throws()
  460. await expect(
  461. this.RecurlyClient.promises.removeSubscriptionChange(
  462. this.subscription.id,
  463. {}
  464. )
  465. ).to.eventually.be.rejectedWith(Error)
  466. })
  467. describe('removeSubscriptionChangeByUuid', function () {
  468. it('should attempt to remove a pending subscription change', async function () {
  469. this.RecurlyClient.promises.removeSubscriptionChangeByUuid(
  470. this.subscription.uuid,
  471. {}
  472. )
  473. expect(this.client.removeSubscriptionChange).to.be.calledWith(
  474. 'uuid-' + this.subscription.uuid
  475. )
  476. })
  477. it('should throw any API errors', async function () {
  478. this.client.removeSubscriptionChange = sinon.stub().throws()
  479. await expect(
  480. this.RecurlyClient.promises.removeSubscriptionChangeByUuid(
  481. this.subscription.id,
  482. {}
  483. )
  484. ).to.eventually.be.rejectedWith(Error)
  485. })
  486. })
  487. })
  488. describe('reactivateSubscriptionByUuid', function () {
  489. it('should attempt to reactivate the subscription', async function () {
  490. this.client.reactivateSubscription = sinon
  491. .stub()
  492. .resolves(this.recurlySubscription)
  493. const subscription =
  494. await this.RecurlyClient.promises.reactivateSubscriptionByUuid(
  495. this.subscription.uuid
  496. )
  497. expect(subscription).to.deep.equal(this.recurlySubscription)
  498. expect(this.client.reactivateSubscription).to.be.calledWith(
  499. 'uuid-' + this.subscription.uuid
  500. )
  501. })
  502. })
  503. describe('cancelSubscriptionByUuid', function () {
  504. it('should attempt to cancel the subscription', async function () {
  505. this.client.cancelSubscription = sinon
  506. .stub()
  507. .resolves(this.recurlySubscription)
  508. const subscription =
  509. await this.RecurlyClient.promises.cancelSubscriptionByUuid(
  510. this.subscription.uuid
  511. )
  512. expect(subscription).to.deep.equal(this.recurlySubscription)
  513. expect(this.client.cancelSubscription).to.be.calledWith(
  514. 'uuid-' + this.subscription.uuid
  515. )
  516. })
  517. })
  518. describe('pauseSubscriptionByUuid', function () {
  519. it('should attempt to pause the subscription', async function () {
  520. this.client.pauseSubscription = sinon
  521. .stub()
  522. .resolves(this.recurlySubscription)
  523. const subscription =
  524. await this.RecurlyClient.promises.pauseSubscriptionByUuid(
  525. this.subscription.uuid,
  526. 3
  527. )
  528. expect(subscription).to.deep.equal(this.recurlySubscription)
  529. expect(this.client.pauseSubscription).to.be.calledWith(
  530. 'uuid-' + this.subscription.uuid,
  531. { remainingPauseCycles: 3 }
  532. )
  533. })
  534. })
  535. describe('previewSubscriptionChange', function () {
  536. describe('compute immediate charge', function () {
  537. it('only has charge invoice', async function () {
  538. this.client.previewSubscriptionChange.resolves({
  539. plan: { code: 'test_code', name: 'test name' },
  540. unitAmount: 0,
  541. invoiceCollection: {
  542. chargeInvoice: {
  543. subtotal: 100,
  544. tax: 20,
  545. total: 120,
  546. },
  547. },
  548. })
  549. const { immediateCharge } =
  550. await this.RecurlyClient.promises.previewSubscriptionChange(
  551. new PaymentProviderSubscriptionChangeRequest({
  552. subscription: this.subscription,
  553. timeframe: 'now',
  554. planCode: 'new-plan',
  555. })
  556. )
  557. expect(immediateCharge.subtotal).to.be.equal(100)
  558. expect(immediateCharge.tax).to.be.equal(20)
  559. expect(immediateCharge.total).to.be.equal(120)
  560. })
  561. it('credit invoice with imprecise float number', async function () {
  562. this.client.previewSubscriptionChange.resolves({
  563. plan: { code: 'test_code', name: 'test name' },
  564. unitAmount: 0,
  565. invoiceCollection: {
  566. chargeInvoice: {
  567. subtotal: 100.3,
  568. tax: 20.3,
  569. total: 120.3,
  570. },
  571. creditInvoices: [
  572. {
  573. subtotal: -20.1,
  574. tax: -4.1,
  575. total: -24.1,
  576. },
  577. ],
  578. },
  579. })
  580. const { immediateCharge } =
  581. await this.RecurlyClient.promises.previewSubscriptionChange(
  582. new PaymentProviderSubscriptionChangeRequest({
  583. subscription: this.subscription,
  584. timeframe: 'now',
  585. planCode: 'new-plan',
  586. })
  587. )
  588. expect(immediateCharge.subtotal).to.be.equal(80.2)
  589. expect(immediateCharge.tax).to.be.equal(16.2)
  590. expect(immediateCharge.total).to.be.equal(96.2)
  591. })
  592. })
  593. it('should throw SubtotalLimitExceededError', async function () {
  594. class ValidationError extends recurly.errors.ValidationError {
  595. constructor() {
  596. super()
  597. this.params = [{ param: 'subtotal_amount_in_cents' }]
  598. }
  599. }
  600. this.client.previewSubscriptionChange = sinon
  601. .stub()
  602. .throws(new ValidationError())
  603. await expect(
  604. this.RecurlyClient.promises.previewSubscriptionChange(
  605. new PaymentProviderSubscriptionChangeRequest({
  606. subscription: this.subscription,
  607. timeframe: 'now',
  608. planCode: 'new-plan',
  609. })
  610. )
  611. ).to.be.rejectedWith(this.Errors.SubtotalLimitExceededError)
  612. })
  613. it('should rethrow errors different than SubtotalLimitExceededError', async function () {
  614. this.client.previewSubscriptionChange = sinon.stub().throws(new Error())
  615. await expect(
  616. this.RecurlyClient.promises.previewSubscriptionChange(
  617. new PaymentProviderSubscriptionChangeRequest({
  618. subscription: this.subscription,
  619. timeframe: 'now',
  620. planCode: 'new-plan',
  621. })
  622. )
  623. ).to.be.rejectedWith(Error)
  624. })
  625. })
  626. describe('getPaymentMethod', function () {
  627. it('should throw MissingBillingInfoError', async function () {
  628. this.client.getBillingInfo = sinon
  629. .stub()
  630. .throws(new recurly.errors.NotFoundError())
  631. await expect(
  632. this.RecurlyClient.promises.getPaymentMethod(this.user._id)
  633. ).to.be.rejectedWith(this.Errors.MissingBillingInfoError)
  634. })
  635. it('should rethrow errors different than MissingBillingInfoError', async function () {
  636. this.client.getBillingInfo = sinon.stub().throws(new Error())
  637. await expect(
  638. this.RecurlyClient.promises.getPaymentMethod(this.user._id)
  639. ).to.be.rejectedWith(Error)
  640. })
  641. })
  642. describe('terminateSubscriptionByUuid', function () {
  643. it('should attempt to terminate the subscription', async function () {
  644. this.client.terminateSubscription = sinon
  645. .stub()
  646. .resolves(this.recurlySubscription)
  647. const subscription =
  648. await this.RecurlyClient.promises.terminateSubscriptionByUuid(
  649. this.subscription.uuid
  650. )
  651. expect(subscription).to.deep.equal(this.recurlySubscription)
  652. expect(this.client.terminateSubscription).to.be.calledWith(
  653. 'uuid-' + this.subscription.uuid
  654. )
  655. })
  656. })
  657. })