RecurlyClientTests.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const recurly = require('recurly')
  4. const SandboxedModule = require('sandboxed-module')
  5. const {
  6. RecurlySubscriptionChangeRequest,
  7. RecurlySubscriptionAddOnUpdate,
  8. } = require('../../../../app/src/Features/Subscription/RecurlyEntities')
  9. const MODULE_PATH = '../../../../app/src/Features/Subscription/RecurlyClient'
  10. describe('RecurlyClient', function () {
  11. beforeEach(function () {
  12. this.settings = {
  13. apis: {
  14. recurly: {
  15. apiKey: 'nonsense',
  16. privateKey: 'private_nonsense',
  17. },
  18. },
  19. plans: [],
  20. features: [],
  21. }
  22. this.user = { _id: '123456', email: 'joe@example.com', first_name: 'Joe' }
  23. this.subscriptionChange = { id: 'subscription-change-123' }
  24. this.recurlyAccount = new recurly.Account()
  25. Object.assign(this.recurlyAccount, { code: this.user._id })
  26. this.subscriptionAddOn = {
  27. code: 'addon-code',
  28. name: 'My Add-On',
  29. quantity: 1,
  30. unitPrice: 2,
  31. preTaxTotal: 2,
  32. }
  33. this.subscription = {
  34. id: 'subscription-id',
  35. userId: 'user-id',
  36. currency: 'EUR',
  37. planCode: 'plan-code',
  38. planName: 'plan-name',
  39. planPrice: 13,
  40. addOns: [this.subscriptionAddOn],
  41. subtotal: 15,
  42. taxRate: 0.1,
  43. taxAmount: 1.5,
  44. total: 16.5,
  45. periodStart: new Date(),
  46. periodEnd: new Date(),
  47. }
  48. this.recurlySubscription = {
  49. uuid: this.subscription.id,
  50. account: {
  51. code: this.subscription.userId,
  52. },
  53. plan: {
  54. code: this.subscription.planCode,
  55. name: this.subscription.planName,
  56. },
  57. addOns: [
  58. {
  59. addOn: {
  60. code: this.subscriptionAddOn.code,
  61. name: this.subscriptionAddOn.name,
  62. },
  63. quantity: this.subscriptionAddOn.quantity,
  64. unitAmount: this.subscriptionAddOn.unitPrice,
  65. },
  66. ],
  67. unitAmount: this.subscription.planPrice,
  68. subtotal: this.subscription.subtotal,
  69. taxInfo: { rate: this.subscription.taxRate },
  70. tax: this.subscription.taxAmount,
  71. total: this.subscription.total,
  72. currency: this.subscription.currency,
  73. currentPeriodStartedAt: this.subscription.periodStart,
  74. currentPeriodEndsAt: this.subscription.periodEnd,
  75. }
  76. this.recurlySubscriptionChange = new recurly.SubscriptionChange()
  77. Object.assign(this.recurlySubscriptionChange, this.subscriptionChange)
  78. this.UserGetter = {
  79. promises: {
  80. getUser: sinon.stub().callsFake(userId => {
  81. if (userId === this.user._id) {
  82. return this.user
  83. }
  84. }),
  85. },
  86. }
  87. let client
  88. this.client = client = {
  89. getAccount: sinon.stub(),
  90. }
  91. this.recurly = {
  92. errors: recurly.errors,
  93. Client: function () {
  94. return client
  95. },
  96. }
  97. return (this.RecurlyClient = SandboxedModule.require(MODULE_PATH, {
  98. globals: {
  99. console,
  100. },
  101. requires: {
  102. '@overleaf/settings': this.settings,
  103. recurly: this.recurly,
  104. '@overleaf/logger': {
  105. err: sinon.stub(),
  106. error: sinon.stub(),
  107. warn: sinon.stub(),
  108. log: sinon.stub(),
  109. debug: sinon.stub(),
  110. },
  111. '../User/UserGetter': this.UserGetter,
  112. },
  113. }))
  114. })
  115. describe('initalizing recurly client with undefined API key parameter', function () {
  116. it('should create a client without error', function () {
  117. let testClient
  118. expect(() => {
  119. testClient = new recurly.Client(undefined)
  120. }).to.not.throw()
  121. expect(testClient).to.be.instanceOf(recurly.Client)
  122. })
  123. })
  124. describe('getAccountForUserId', function () {
  125. it('should return an Account if one exists', async function () {
  126. this.client.getAccount = sinon.stub().resolves(this.recurlyAccount)
  127. await expect(
  128. this.RecurlyClient.promises.getAccountForUserId(this.user._id)
  129. )
  130. .to.eventually.be.an.instanceOf(recurly.Account)
  131. .that.has.property('code', this.user._id)
  132. })
  133. it('should return nothing if no account found', async function () {
  134. this.client.getAccount = sinon
  135. .stub()
  136. .throws(new recurly.errors.NotFoundError())
  137. expect(
  138. this.RecurlyClient.promises.getAccountForUserId('nonsense')
  139. ).to.eventually.equal(undefined)
  140. })
  141. it('should re-throw caught errors', async function () {
  142. this.client.getAccount = sinon.stub().throws()
  143. await expect(
  144. this.RecurlyClient.promises.getAccountForUserId(this.user._id)
  145. ).to.eventually.be.rejectedWith(Error)
  146. })
  147. })
  148. describe('createAccountForUserId', function () {
  149. it('should return the Account as created by recurly', async function () {
  150. this.client.createAccount = sinon.stub().resolves(this.recurlyAccount)
  151. await expect(
  152. this.RecurlyClient.promises.createAccountForUserId(this.user._id)
  153. )
  154. .to.eventually.be.an.instanceOf(recurly.Account)
  155. .that.has.property('code', this.user._id)
  156. })
  157. it('should throw any API errors', async function () {
  158. this.client.createAccount = sinon.stub().throws()
  159. await expect(
  160. this.RecurlyClient.promises.createAccountForUserId(this.user._id)
  161. ).to.eventually.be.rejectedWith(Error)
  162. })
  163. })
  164. describe('getSubscription', function () {
  165. it('should return the subscription found by recurly', async function () {
  166. this.client.getSubscription = sinon
  167. .stub()
  168. .withArgs('uuid-subscription-id')
  169. .resolves(this.recurlySubscription)
  170. const subscription = await this.RecurlyClient.promises.getSubscription(
  171. this.subscription.id
  172. )
  173. expect(subscription).to.deep.equal(this.subscription)
  174. })
  175. it('should throw any API errors', async function () {
  176. this.client.getSubscription = sinon.stub().throws()
  177. await expect(
  178. this.RecurlyClient.promises.getSubscription(this.user._id)
  179. ).to.eventually.be.rejectedWith(Error)
  180. })
  181. })
  182. describe('applySubscriptionChangeRequest', function () {
  183. beforeEach(function () {
  184. this.client.createSubscriptionChange = sinon
  185. .stub()
  186. .resolves(this.recurlySubscriptionChange)
  187. })
  188. it('handles plan changes', async function () {
  189. await this.RecurlyClient.promises.applySubscriptionChangeRequest(
  190. new RecurlySubscriptionChangeRequest({
  191. subscription: this.subscription,
  192. timeframe: 'now',
  193. planCode: 'new-plan',
  194. })
  195. )
  196. expect(this.client.createSubscriptionChange).to.be.calledWith(
  197. 'uuid-subscription-id',
  198. { timeframe: 'now', planCode: 'new-plan' }
  199. )
  200. })
  201. it('handles add-on changes', async function () {
  202. await this.RecurlyClient.promises.applySubscriptionChangeRequest(
  203. new RecurlySubscriptionChangeRequest({
  204. subscription: this.subscription,
  205. timeframe: 'now',
  206. addOnUpdates: [
  207. new RecurlySubscriptionAddOnUpdate({
  208. code: 'new-add-on',
  209. quantity: 2,
  210. unitPrice: 8.99,
  211. }),
  212. ],
  213. })
  214. )
  215. expect(this.client.createSubscriptionChange).to.be.calledWith(
  216. 'uuid-subscription-id',
  217. {
  218. timeframe: 'now',
  219. addOns: [{ code: 'new-add-on', quantity: 2, unitAmount: 8.99 }],
  220. }
  221. )
  222. })
  223. it('should throw any API errors', async function () {
  224. this.client.createSubscriptionChange = sinon.stub().throws()
  225. await expect(
  226. this.RecurlyClient.promises.applySubscriptionChangeRequest({
  227. subscription: this.subscription,
  228. })
  229. ).to.eventually.be.rejectedWith(Error)
  230. })
  231. })
  232. describe('removeSubscriptionChange', function () {
  233. beforeEach(function () {
  234. this.client.removeSubscriptionChange = sinon.stub().resolves()
  235. })
  236. it('should attempt to remove a pending subscription change', async function () {
  237. this.RecurlyClient.promises.removeSubscriptionChange(
  238. this.subscription.id,
  239. {}
  240. )
  241. expect(this.client.removeSubscriptionChange).to.be.calledWith(
  242. this.subscription.id
  243. )
  244. })
  245. it('should throw any API errors', async function () {
  246. this.client.removeSubscriptionChange = sinon.stub().throws()
  247. await expect(
  248. this.RecurlyClient.promises.removeSubscriptionChange(
  249. this.subscription.id,
  250. {}
  251. )
  252. ).to.eventually.be.rejectedWith(Error)
  253. })
  254. describe('removeSubscriptionChangeByUuid', function () {
  255. it('should attempt to remove a pending subscription change', async function () {
  256. this.RecurlyClient.promises.removeSubscriptionChangeByUuid(
  257. this.subscription.uuid,
  258. {}
  259. )
  260. expect(this.client.removeSubscriptionChange).to.be.calledWith(
  261. 'uuid-' + this.subscription.uuid
  262. )
  263. })
  264. it('should throw any API errors', async function () {
  265. this.client.removeSubscriptionChange = sinon.stub().throws()
  266. await expect(
  267. this.RecurlyClient.promises.removeSubscriptionChangeByUuid(
  268. this.subscription.id,
  269. {}
  270. )
  271. ).to.eventually.be.rejectedWith(Error)
  272. })
  273. })
  274. })
  275. describe('reactivateSubscriptionByUuid', function () {
  276. it('should attempt to reactivate the subscription', async function () {
  277. this.client.reactivateSubscription = sinon
  278. .stub()
  279. .resolves(this.recurlySubscription)
  280. const subscription =
  281. await this.RecurlyClient.promises.reactivateSubscriptionByUuid(
  282. this.subscription.uuid
  283. )
  284. expect(subscription).to.deep.equal(this.recurlySubscription)
  285. expect(this.client.reactivateSubscription).to.be.calledWith(
  286. 'uuid-' + this.subscription.uuid
  287. )
  288. })
  289. })
  290. describe('cancelSubscriptionByUuid', function () {
  291. it('should attempt to cancel the subscription', async function () {
  292. this.client.cancelSubscription = sinon
  293. .stub()
  294. .resolves(this.recurlySubscription)
  295. const subscription =
  296. await this.RecurlyClient.promises.cancelSubscriptionByUuid(
  297. this.subscription.uuid
  298. )
  299. expect(subscription).to.deep.equal(this.recurlySubscription)
  300. expect(this.client.cancelSubscription).to.be.calledWith(
  301. 'uuid-' + this.subscription.uuid
  302. )
  303. })
  304. })
  305. })