RecurlyClientTests.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  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. const result = await this.RecurlyClient.promises.createAccountForUserId(
  182. this.user._id
  183. )
  184. expect(result).to.has.property('code', this.user._id)
  185. })
  186. it('should throw any API errors', async function () {
  187. this.client.createAccount = sinon.stub().throws()
  188. await expect(
  189. this.RecurlyClient.promises.createAccountForUserId(this.user._id)
  190. ).to.eventually.be.rejectedWith(Error)
  191. })
  192. })
  193. describe('getActiveCouponsForUserId', function () {
  194. it('should return an empty array if no coupons returned', async function () {
  195. this.client.listActiveCouponRedemptions.returns({
  196. each: async function* () {},
  197. })
  198. const coupons =
  199. await this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  200. expect(coupons).to.deep.equal([])
  201. })
  202. it('should return a coupons returned by recurly', async function () {
  203. const recurlyCoupon = {
  204. coupon: {
  205. code: 'coupon-code',
  206. name: 'Coupon Name',
  207. hostedPageDescription: 'hosted page description',
  208. invoiceDescription: 'invoice description',
  209. },
  210. }
  211. this.client.listActiveCouponRedemptions.returns({
  212. each: async function* () {
  213. yield recurlyCoupon
  214. },
  215. })
  216. const coupons =
  217. await this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  218. const expectedCoupons = [
  219. new PaymentProviderCoupon({
  220. code: 'coupon-code',
  221. name: 'Coupon Name',
  222. description: 'hosted page description',
  223. }),
  224. ]
  225. expect(coupons).to.deep.equal(expectedCoupons)
  226. })
  227. it('should not throw for Recurly not found error', async function () {
  228. this.client.listActiveCouponRedemptions = sinon
  229. .stub()
  230. .throws(new recurly.errors.NotFoundError())
  231. const coupons =
  232. await this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  233. expect(coupons).to.deep.equal([])
  234. })
  235. it('should throw any other API errors', async function () {
  236. this.client.listActiveCouponRedemptions = sinon.stub().throws()
  237. await expect(
  238. this.RecurlyClient.promises.getActiveCouponsForUserId('some-user')
  239. ).to.eventually.be.rejectedWith(Error)
  240. })
  241. })
  242. describe('getCustomerManagementLink', function () {
  243. it('should throw if recurly token is not returned', async function () {
  244. this.client.getAccount.resolves({})
  245. await expect(
  246. this.RecurlyClient.promises.getCustomerManagementLink(
  247. '12345',
  248. 'account-management',
  249. 'en-US'
  250. )
  251. ).to.be.rejectedWith('recurly account does not have hosted login token')
  252. })
  253. it('should generate the correct account management url', async function () {
  254. this.client.getAccount.resolves({
  255. hostedLoginToken: '987654321',
  256. })
  257. const result =
  258. await this.RecurlyClient.promises.getCustomerManagementLink(
  259. '12345',
  260. 'account-management',
  261. 'en-US'
  262. )
  263. expect(result).to.equal('https://test.recurly.com/account/987654321')
  264. })
  265. it('should generate the correct billing details url', async function () {
  266. this.client.getAccount.resolves({
  267. hostedLoginToken: '987654321',
  268. })
  269. const result =
  270. await this.RecurlyClient.promises.getCustomerManagementLink(
  271. '12345',
  272. 'billing-details',
  273. 'en-US'
  274. )
  275. expect(result).to.equal(
  276. 'https://test.recurly.com/account/billing_info/edit?ht=987654321'
  277. )
  278. })
  279. })
  280. describe('getSubscription', function () {
  281. it('should return the subscription found by recurly', async function () {
  282. this.client.getSubscription = sinon
  283. .stub()
  284. .withArgs('uuid-subscription-id')
  285. .resolves(this.recurlySubscription)
  286. const subscription = await this.RecurlyClient.promises.getSubscription(
  287. this.subscription.id
  288. )
  289. expect(subscription).to.deep.equal(this.subscription)
  290. })
  291. it('should throw any API errors', async function () {
  292. this.client.getSubscription = sinon.stub().throws()
  293. await expect(
  294. this.RecurlyClient.promises.getSubscription(this.user._id)
  295. ).to.eventually.be.rejectedWith(Error)
  296. })
  297. })
  298. describe('getSubscriptionForUser', function () {
  299. it("should return null if the account doesn't exist", async function () {
  300. this.client.listAccountSubscriptions.returns({
  301. // eslint-disable-next-line require-yield
  302. each: async function* () {
  303. throw new recurly.errors.NotFoundError('account not found')
  304. },
  305. })
  306. const subscription =
  307. await this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  308. expect(subscription).to.be.null
  309. })
  310. it("should return null if the account doesn't have subscriptions", async function () {
  311. this.client.listAccountSubscriptions.returns({
  312. each: async function* () {},
  313. })
  314. const subscription =
  315. await this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  316. expect(subscription).to.be.null
  317. })
  318. it('should return the subscription if the account has one subscription', async function () {
  319. const recurlySubscription = this.recurlySubscription
  320. this.client.listAccountSubscriptions.returns({
  321. each: async function* () {
  322. yield recurlySubscription
  323. },
  324. })
  325. const subscription =
  326. await this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  327. expect(subscription).to.deep.equal(this.subscription)
  328. })
  329. it('should throw an error if the account has more than one subscription', async function () {
  330. const recurlySubscription = this.recurlySubscription
  331. this.client.listAccountSubscriptions.returns({
  332. each: async function* () {
  333. yield recurlySubscription
  334. yield { another: 'subscription' }
  335. },
  336. })
  337. await expect(
  338. this.RecurlyClient.promises.getSubscriptionForUser('some-user')
  339. ).to.be.rejected
  340. })
  341. })
  342. describe('applySubscriptionChangeRequest', function () {
  343. beforeEach(function () {
  344. this.client.createSubscriptionChange = sinon
  345. .stub()
  346. .resolves(this.recurlySubscriptionChange)
  347. })
  348. it('handles plan changes', async function () {
  349. await this.RecurlyClient.promises.applySubscriptionChangeRequest(
  350. new PaymentProviderSubscriptionChangeRequest({
  351. subscription: this.subscription,
  352. timeframe: 'now',
  353. planCode: 'new-plan',
  354. })
  355. )
  356. expect(this.client.createSubscriptionChange).to.be.calledWith(
  357. 'uuid-subscription-id',
  358. { timeframe: 'now', planCode: 'new-plan' }
  359. )
  360. })
  361. it('handles add-on changes', async function () {
  362. await this.RecurlyClient.promises.applySubscriptionChangeRequest(
  363. new PaymentProviderSubscriptionChangeRequest({
  364. subscription: this.subscription,
  365. timeframe: 'now',
  366. addOnUpdates: [
  367. new PaymentProviderSubscriptionAddOnUpdate({
  368. code: 'new-add-on',
  369. quantity: 2,
  370. unitPrice: 8.99,
  371. }),
  372. ],
  373. })
  374. )
  375. expect(this.client.createSubscriptionChange).to.be.calledWith(
  376. 'uuid-subscription-id',
  377. {
  378. timeframe: 'now',
  379. addOns: [{ code: 'new-add-on', quantity: 2, unitAmount: 8.99 }],
  380. }
  381. )
  382. })
  383. it('should throw any API errors', async function () {
  384. this.client.createSubscriptionChange = sinon.stub().throws()
  385. await expect(
  386. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  387. subscription: this.subscription,
  388. })
  389. ).to.eventually.be.rejectedWith(Error)
  390. })
  391. it('should throw SubtotalLimitExceededError', async function () {
  392. class ValidationError extends recurly.errors.ValidationError {
  393. constructor() {
  394. super()
  395. this.params = [{ param: 'subtotal_amount_in_cents' }]
  396. }
  397. }
  398. this.client.createSubscriptionChange = sinon
  399. .stub()
  400. .throws(new ValidationError())
  401. await expect(
  402. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  403. subscription: this.subscription,
  404. })
  405. ).to.be.rejectedWith(this.Errors.SubtotalLimitExceededError)
  406. })
  407. it('should rethrow errors different than SubtotalLimitExceededError', async function () {
  408. this.client.createSubscriptionChange = sinon.stub().throws(new Error())
  409. await expect(
  410. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  411. subscription: this.subscription,
  412. })
  413. ).to.be.rejectedWith(Error)
  414. })
  415. })
  416. describe('updateSubscriptionDetails', function () {
  417. beforeEach(function () {
  418. this.client.updateSubscription = sinon
  419. .stub()
  420. .resolves({ id: this.subscription.id })
  421. })
  422. it('handles subscription update', async function () {
  423. await this.RecurlyClient.promises.updateSubscriptionDetails(
  424. new PaymentProviderSubscriptionUpdateRequest({
  425. subscription: this.subscription,
  426. poNumber: '012345',
  427. termsAndConditions: 'T&C',
  428. })
  429. )
  430. expect(this.client.updateSubscription).to.be.calledWith(
  431. 'uuid-subscription-id',
  432. { poNumber: '012345', termsAndConditions: 'T&C' }
  433. )
  434. })
  435. it('should throw any API errors', async function () {
  436. this.client.updateSubscription = sinon.stub().throws()
  437. await expect(
  438. this.RecurlyClient.promises.updateSubscriptionDetails({
  439. subscription: this.subscription,
  440. })
  441. ).to.eventually.be.rejectedWith(Error)
  442. })
  443. })
  444. describe('removeSubscriptionChange', function () {
  445. beforeEach(function () {
  446. this.client.removeSubscriptionChange = sinon.stub().resolves()
  447. })
  448. it('should attempt to remove a pending subscription change', async function () {
  449. this.RecurlyClient.promises.removeSubscriptionChange(
  450. this.subscription.id,
  451. {}
  452. )
  453. expect(this.client.removeSubscriptionChange).to.be.calledWith(
  454. this.subscription.id
  455. )
  456. })
  457. it('should throw any API errors', async function () {
  458. this.client.removeSubscriptionChange = sinon.stub().throws()
  459. await expect(
  460. this.RecurlyClient.promises.removeSubscriptionChange(
  461. this.subscription.id,
  462. {}
  463. )
  464. ).to.eventually.be.rejectedWith(Error)
  465. })
  466. describe('removeSubscriptionChangeByUuid', function () {
  467. it('should attempt to remove a pending subscription change', async function () {
  468. this.RecurlyClient.promises.removeSubscriptionChangeByUuid(
  469. this.subscription.uuid,
  470. {}
  471. )
  472. expect(this.client.removeSubscriptionChange).to.be.calledWith(
  473. 'uuid-' + this.subscription.uuid
  474. )
  475. })
  476. it('should throw any API errors', async function () {
  477. this.client.removeSubscriptionChange = sinon.stub().throws()
  478. await expect(
  479. this.RecurlyClient.promises.removeSubscriptionChangeByUuid(
  480. this.subscription.id,
  481. {}
  482. )
  483. ).to.eventually.be.rejectedWith(Error)
  484. })
  485. })
  486. })
  487. describe('reactivateSubscriptionByUuid', function () {
  488. it('should attempt to reactivate the subscription', async function () {
  489. this.client.reactivateSubscription = sinon
  490. .stub()
  491. .resolves(this.recurlySubscription)
  492. const subscription =
  493. await this.RecurlyClient.promises.reactivateSubscriptionByUuid(
  494. this.subscription.uuid
  495. )
  496. expect(subscription).to.deep.equal(this.recurlySubscription)
  497. expect(this.client.reactivateSubscription).to.be.calledWith(
  498. 'uuid-' + this.subscription.uuid
  499. )
  500. })
  501. })
  502. describe('cancelSubscriptionByUuid', function () {
  503. it('should attempt to cancel the subscription', async function () {
  504. this.client.cancelSubscription = sinon
  505. .stub()
  506. .resolves(this.recurlySubscription)
  507. const subscription =
  508. await this.RecurlyClient.promises.cancelSubscriptionByUuid(
  509. this.subscription.uuid
  510. )
  511. expect(subscription).to.deep.equal(this.recurlySubscription)
  512. expect(this.client.cancelSubscription).to.be.calledWith(
  513. 'uuid-' + this.subscription.uuid
  514. )
  515. })
  516. it('should terminate subscription when cancellation fails due to being in last cycle of paused term', async function () {
  517. const validationError = new recurly.errors.ValidationError()
  518. validationError.message =
  519. 'Cannot cancel a paused subscription in the last cycle of the term'
  520. this.client.cancelSubscription = sinon.stub().throws(validationError)
  521. this.client.terminateSubscription = sinon
  522. .stub()
  523. .resolves(this.recurlySubscription)
  524. const subscription =
  525. await this.RecurlyClient.promises.cancelSubscriptionByUuid(
  526. this.subscription.uuid
  527. )
  528. expect(this.client.cancelSubscription).to.be.calledWith(
  529. 'uuid-' + this.subscription.uuid
  530. )
  531. expect(this.client.terminateSubscription).to.be.calledWith(
  532. 'uuid-' + this.subscription.uuid
  533. )
  534. expect(subscription).to.deep.equal(this.recurlySubscription)
  535. })
  536. })
  537. describe('pauseSubscriptionByUuid', function () {
  538. it('should attempt to pause the subscription', async function () {
  539. this.client.pauseSubscription = sinon
  540. .stub()
  541. .resolves(this.recurlySubscription)
  542. const subscription =
  543. await this.RecurlyClient.promises.pauseSubscriptionByUuid(
  544. this.subscription.uuid,
  545. 3
  546. )
  547. expect(subscription).to.deep.equal(this.recurlySubscription)
  548. expect(this.client.pauseSubscription).to.be.calledWith(
  549. 'uuid-' + this.subscription.uuid,
  550. { remainingPauseCycles: 3 }
  551. )
  552. })
  553. })
  554. describe('previewSubscriptionChange', function () {
  555. describe('compute immediate charge', function () {
  556. it('only has charge invoice', async function () {
  557. this.client.previewSubscriptionChange.resolves({
  558. plan: { code: 'test_code', name: 'test name' },
  559. unitAmount: 0,
  560. invoiceCollection: {
  561. chargeInvoice: {
  562. subtotal: 100,
  563. tax: 20,
  564. total: 120,
  565. },
  566. },
  567. })
  568. const { immediateCharge } =
  569. await this.RecurlyClient.promises.previewSubscriptionChange(
  570. new PaymentProviderSubscriptionChangeRequest({
  571. subscription: this.subscription,
  572. timeframe: 'now',
  573. planCode: 'new-plan',
  574. })
  575. )
  576. expect(immediateCharge.subtotal).to.be.equal(100)
  577. expect(immediateCharge.tax).to.be.equal(20)
  578. expect(immediateCharge.total).to.be.equal(120)
  579. })
  580. it('credit invoice with imprecise float number', async function () {
  581. this.client.previewSubscriptionChange.resolves({
  582. plan: { code: 'test_code', name: 'test name' },
  583. unitAmount: 0,
  584. invoiceCollection: {
  585. chargeInvoice: {
  586. subtotal: 100.3,
  587. tax: 20.3,
  588. total: 120.3,
  589. },
  590. creditInvoices: [
  591. {
  592. subtotal: -20.1,
  593. tax: -4.1,
  594. total: -24.1,
  595. },
  596. ],
  597. },
  598. })
  599. const { immediateCharge } =
  600. await this.RecurlyClient.promises.previewSubscriptionChange(
  601. new PaymentProviderSubscriptionChangeRequest({
  602. subscription: this.subscription,
  603. timeframe: 'now',
  604. planCode: 'new-plan',
  605. })
  606. )
  607. expect(immediateCharge.subtotal).to.be.equal(80.2)
  608. expect(immediateCharge.tax).to.be.equal(16.2)
  609. expect(immediateCharge.total).to.be.equal(96.2)
  610. })
  611. })
  612. it('should throw SubtotalLimitExceededError', async function () {
  613. class ValidationError extends recurly.errors.ValidationError {
  614. constructor() {
  615. super()
  616. this.params = [{ param: 'subtotal_amount_in_cents' }]
  617. }
  618. }
  619. this.client.previewSubscriptionChange = sinon
  620. .stub()
  621. .throws(new ValidationError())
  622. await expect(
  623. this.RecurlyClient.promises.previewSubscriptionChange(
  624. new PaymentProviderSubscriptionChangeRequest({
  625. subscription: this.subscription,
  626. timeframe: 'now',
  627. planCode: 'new-plan',
  628. })
  629. )
  630. ).to.be.rejectedWith(this.Errors.SubtotalLimitExceededError)
  631. })
  632. it('should rethrow errors different than SubtotalLimitExceededError', async function () {
  633. this.client.previewSubscriptionChange = sinon.stub().throws(new Error())
  634. await expect(
  635. this.RecurlyClient.promises.previewSubscriptionChange(
  636. new PaymentProviderSubscriptionChangeRequest({
  637. subscription: this.subscription,
  638. timeframe: 'now',
  639. planCode: 'new-plan',
  640. })
  641. )
  642. ).to.be.rejectedWith(Error)
  643. })
  644. })
  645. describe('getPaymentMethod', function () {
  646. it('should throw MissingBillingInfoError', async function () {
  647. this.client.getBillingInfo = sinon
  648. .stub()
  649. .throws(new recurly.errors.NotFoundError())
  650. await expect(
  651. this.RecurlyClient.promises.getPaymentMethod(this.user._id)
  652. ).to.be.rejectedWith(this.Errors.MissingBillingInfoError)
  653. })
  654. it('should rethrow errors different than MissingBillingInfoError', async function () {
  655. this.client.getBillingInfo = sinon.stub().throws(new Error())
  656. await expect(
  657. this.RecurlyClient.promises.getPaymentMethod(this.user._id)
  658. ).to.be.rejectedWith(Error)
  659. })
  660. })
  661. describe('terminateSubscriptionByUuid', function () {
  662. it('should attempt to terminate the subscription', async function () {
  663. this.client.terminateSubscription = sinon
  664. .stub()
  665. .resolves(this.recurlySubscription)
  666. const subscription =
  667. await this.RecurlyClient.promises.terminateSubscriptionByUuid(
  668. this.subscription.uuid
  669. )
  670. expect(subscription).to.deep.equal(this.recurlySubscription)
  671. expect(this.client.terminateSubscription).to.be.calledWith(
  672. 'uuid-' + this.subscription.uuid
  673. )
  674. })
  675. })
  676. })