AiFeatureUsageRateLimiter.test.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { expect, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. const ObjectId = mongodb.ObjectId
  5. vi.mock('../../../../../app/src/Features/Errors/Errors.js', () => {
  6. return vi.importActual('../../../../../app/src/Features/Errors/Errors.js')
  7. })
  8. const modulePath =
  9. '../../../../app/src/infrastructure/rate-limiters/AiFeatureUsageRateLimiter.mjs'
  10. describe('AiFeatureUsageRateLimiter', function () {
  11. beforeEach(async function (ctx) {
  12. ctx.userId = new ObjectId().toString()
  13. ctx.UserFeatureUsageModel = {
  14. findOneAndUpdate: sinon.stub().returns({
  15. exec: sinon.stub().resolves({
  16. features: {
  17. aiFeatureUsage: {
  18. usage: 0,
  19. periodStart: new Date(),
  20. },
  21. },
  22. }),
  23. }),
  24. findOne: sinon.stub().returns({
  25. exec: sinon.stub().resolves({
  26. features: {
  27. aiFeatureUsage: {
  28. usage: 0,
  29. periodStart: new Date(),
  30. },
  31. },
  32. }),
  33. }),
  34. }
  35. ctx.user = {
  36. features: { aiUsageQuota: 'basic' },
  37. writefull: { isPremium: false },
  38. }
  39. ctx.userWithOLBundle = {
  40. features: { aiUsageQuota: 'unlimited' },
  41. writefull: { isPremium: false },
  42. }
  43. ctx.userWithOLBundleThroughWf = {
  44. features: { aiUsageQuota: 'basic' },
  45. writefull: { isPremium: true },
  46. }
  47. ctx.UserGetter = {
  48. promises: {
  49. getUser: sinon.stub().resolves(ctx.user),
  50. },
  51. }
  52. ctx.settings = {
  53. writefull: {
  54. quotaTierGranted: 'unlimited',
  55. },
  56. aiFeatures: {
  57. freeTrialQuota: 'basic',
  58. unlimitedQuota: 'unlimited',
  59. },
  60. quotaGrants: {
  61. ai: {
  62. basic: 5,
  63. unlimited: 200,
  64. },
  65. },
  66. }
  67. ctx.SplitTestHandler = {
  68. promises: {
  69. featureFlagEnabledForUser: sinon.stub().resolves(true),
  70. },
  71. }
  72. vi.doMock('@overleaf/settings', () => ({
  73. default: ctx.settings,
  74. }))
  75. vi.doMock('../../../../app/src/models/UserFeatureUsage', () => ({
  76. UserFeatureUsage: ctx.UserFeatureUsageModel,
  77. }))
  78. vi.doMock('../../../../app/src/Features/User/UserGetter.mjs', () => ({
  79. default: ctx.UserGetter,
  80. }))
  81. vi.doMock(
  82. '../../../../app/src/Features/SplitTests/SplitTestHandler.mjs',
  83. () => ({
  84. default: ctx.SplitTestHandler,
  85. })
  86. )
  87. const module = await import(modulePath)
  88. ctx.AiFeatureUsageRateLimiter = module.default
  89. })
  90. describe('useFeature', function () {
  91. describe('with some remaining allowance left', function () {
  92. it('should suceed', async function (ctx) {
  93. const res = { set: () => null }
  94. await expect(
  95. ctx.AiFeatureUsageRateLimiter.useFeature(ctx.userId, res, 1)
  96. ).to.not.be.rejected
  97. })
  98. it('should succeed with cost=0', async function (ctx) {
  99. const res = { set: () => null }
  100. await expect(
  101. ctx.AiFeatureUsageRateLimiter.useFeature(ctx.userId, res, 0)
  102. ).to.not.be.rejected
  103. })
  104. it('should succeed with default cost when cost is omitted', async function (ctx) {
  105. const res = { set: () => null }
  106. await expect(ctx.AiFeatureUsageRateLimiter.useFeature(ctx.userId, res))
  107. .to.not.be.rejected
  108. })
  109. })
  110. describe('with 0 allowance left', function () {
  111. beforeEach(function (ctx) {
  112. ctx.UserFeatureUsageModel.findOneAndUpdate = sinon.stub().returns({
  113. exec: sinon.stub().resolves({
  114. features: {
  115. aiFeatureUsage: {
  116. usage: ctx.settings.quotaGrants.ai.unlimited + 1,
  117. periodStart: new Date(),
  118. },
  119. },
  120. }),
  121. })
  122. })
  123. it('should be rejected with TooManyRequestsError', async function (ctx) {
  124. const res = { set: () => null }
  125. await expect(
  126. ctx.AiFeatureUsageRateLimiter.useFeature(ctx.userId, res, 1)
  127. ).to.be.rejectedWith('aiFeatureUsage rate limit exceeded')
  128. })
  129. })
  130. })
  131. describe('getRemainingFeatureUses', function () {
  132. beforeEach(async function (ctx) {
  133. ctx.UserFeatureUsageModel.findOneAndUpdate = sinon.stub().returns({
  134. exec: sinon.stub().resolves({
  135. features: {
  136. aiFeatureUsage: {
  137. usage: 0,
  138. periodStart: new Date(),
  139. },
  140. },
  141. }),
  142. })
  143. ctx.UserGetter.promises.getUser = sinon.stub()
  144. })
  145. it('should give higher usage for OL assist bundle owners', async function (ctx) {
  146. ctx.UserGetter.promises.getUser = sinon
  147. .stub()
  148. .resolves(ctx.userWithOLBundle)
  149. const usages =
  150. await ctx.AiFeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  151. await expect(usages.aiFeatureUsage.remainingUsage).to.equal(
  152. ctx.settings.quotaGrants.ai.unlimited
  153. )
  154. })
  155. it('should give higher usage for assist bundle owners who have the feature via Writefull', async function (ctx) {
  156. ctx.UserGetter.promises.getUser = sinon
  157. .stub()
  158. .resolves(ctx.userWithOLBundleThroughWf)
  159. const usages =
  160. await ctx.AiFeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  161. await expect(usages.aiFeatureUsage.remainingUsage).to.equal(
  162. ctx.settings.quotaGrants.ai.unlimited
  163. )
  164. })
  165. it('should calculate remaining usages for free users', async function (ctx) {
  166. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user)
  167. const usages =
  168. await ctx.AiFeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  169. await expect(usages.aiFeatureUsage.remainingUsage).to.equal(
  170. ctx.settings.quotaGrants.ai.basic
  171. )
  172. })
  173. })
  174. describe('decrementFeatureUsage', function () {
  175. it('should call findOneAndUpdate to decrement usage', async function (ctx) {
  176. const res = { set: () => null }
  177. await ctx.AiFeatureUsageRateLimiter.decrementFeatureUsage(
  178. ctx.userId,
  179. res,
  180. 1
  181. )
  182. expect(ctx.UserFeatureUsageModel.findOneAndUpdate).to.have.been.called
  183. })
  184. it('should accept a custom cost parameter', async function (ctx) {
  185. const res = { set: () => null }
  186. await expect(
  187. ctx.AiFeatureUsageRateLimiter.decrementFeatureUsage(ctx.userId, res, 3)
  188. ).to.not.be.rejected
  189. })
  190. it('should use default cost of 1 when cost is omitted', async function (ctx) {
  191. const res = { set: () => null }
  192. await expect(
  193. ctx.AiFeatureUsageRateLimiter.decrementFeatureUsage(ctx.userId, res)
  194. ).to.not.be.rejected
  195. })
  196. })
  197. })