FeatureUsageRateLimiter.sequential.test.mjs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { expect, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. import {
  6. connectionPromise,
  7. cleanupTestDatabase,
  8. } from '../../../../app/src/infrastructure/mongodb.mjs'
  9. import { UserFeatureUsage } from '../../../../app/src/models/UserFeatureUsage.mjs'
  10. const { TooManyRequestsError } = Errors
  11. const ObjectId = mongodb.ObjectId
  12. vi.mock('../../../../app/src/Features/Errors/Errors.js', () => {
  13. return vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  14. })
  15. // NOTE: Needs to be an allowed field in UserFeatureUsageSchema
  16. const MOCKED_FEATURE_NAME = 'aiWorkbench'
  17. const modulePath =
  18. '../../../../app/src/infrastructure/rate-limiters/FeatureUsageRateLimiter'
  19. describe('FeatureUsageRateLimiter', function () {
  20. beforeAll(async function () {
  21. await connectionPromise
  22. })
  23. beforeEach(cleanupTestDatabase)
  24. beforeEach(async function (ctx) {
  25. ctx.userId = new ObjectId().toString()
  26. const FeatureUsageRateLimiterClass = (await import(modulePath)).default
  27. ctx._getAllowanceStub = sinon.stub()
  28. class FeatureUsageTestRateLimiter extends FeatureUsageRateLimiterClass {
  29. constructor() {
  30. super(MOCKED_FEATURE_NAME)
  31. }
  32. _getAllowance = ctx._getAllowanceStub
  33. }
  34. ctx.FeatureUsageRateLimiter = new FeatureUsageTestRateLimiter()
  35. })
  36. describe('useFeature', function () {
  37. beforeEach(function (ctx) {
  38. ctx._getAllowanceStub.resolves(100)
  39. })
  40. describe('with no usage', function (ctx) {
  41. it('should succeed', async function (ctx) {
  42. const res = { set: () => null }
  43. await expect(ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res, 1))
  44. .to.not.be.rejected
  45. })
  46. })
  47. describe('with some remaining allowance left', function () {
  48. beforeEach(async function (ctx) {
  49. await UserFeatureUsage.create({
  50. _id: ctx.userId,
  51. features: {
  52. [MOCKED_FEATURE_NAME]: { usage: 50, periodStart: new Date() },
  53. },
  54. })
  55. })
  56. it('should suceed', async function (ctx) {
  57. const res = { set: () => null }
  58. await expect(ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res, 1))
  59. .to.not.be.rejected
  60. })
  61. })
  62. describe('with 0 allowance left', function () {
  63. beforeEach(async function (ctx) {
  64. await UserFeatureUsage.create({
  65. _id: ctx.userId,
  66. features: {
  67. [MOCKED_FEATURE_NAME]: { usage: 101, periodStart: new Date() },
  68. },
  69. })
  70. })
  71. it('should be rejected with TooManyRequestsError', async function (ctx) {
  72. const res = { set: () => null }
  73. await expect(
  74. ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res, 1)
  75. ).to.be.rejectedWith(TooManyRequestsError)
  76. })
  77. })
  78. describe('with cost=0', function () {
  79. beforeEach(async function (ctx) {
  80. await UserFeatureUsage.create({
  81. _id: ctx.userId,
  82. features: {
  83. [MOCKED_FEATURE_NAME]: { usage: 50, periodStart: new Date() },
  84. },
  85. })
  86. })
  87. it('should not increment usage', async function (ctx) {
  88. const res = { set: () => null }
  89. await ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res, 0)
  90. const usages =
  91. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  92. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(50)
  93. })
  94. it('should still be rejected when over the limit', async function (ctx) {
  95. await UserFeatureUsage.findOneAndUpdate(
  96. { _id: ctx.userId },
  97. { $set: { [`features.${MOCKED_FEATURE_NAME}.usage`]: 101 } }
  98. )
  99. const res = { set: () => null }
  100. await expect(
  101. ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res, 0)
  102. ).to.be.rejectedWith(TooManyRequestsError)
  103. })
  104. })
  105. describe('with cost greater than 1', function () {
  106. it('should increment usage by the specified cost', async function (ctx) {
  107. const res = { set: () => null }
  108. await ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res, 5)
  109. const usages =
  110. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  111. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(95)
  112. })
  113. })
  114. describe('with default cost parameter', function () {
  115. it('should increment usage by 1 when cost is omitted', async function (ctx) {
  116. const res = { set: () => null }
  117. await ctx.FeatureUsageRateLimiter.useFeature(ctx.userId, res)
  118. const usages =
  119. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  120. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(99)
  121. })
  122. })
  123. })
  124. describe('getRemainingFeatureUses', function () {
  125. beforeEach(function (ctx) {
  126. ctx._getAllowanceStub.resolves(100)
  127. })
  128. describe('with no usage', function () {
  129. it('should return the whole allowance', async function (ctx) {
  130. const usages =
  131. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  132. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(100)
  133. })
  134. })
  135. describe('with some usage', function () {
  136. beforeEach(async function (ctx) {
  137. await UserFeatureUsage.create({
  138. _id: ctx.userId,
  139. features: {
  140. [MOCKED_FEATURE_NAME]: { usage: 30, periodStart: new Date() },
  141. },
  142. })
  143. })
  144. it('should return the correct remaining allowance', async function (ctx) {
  145. const usages =
  146. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  147. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(70)
  148. })
  149. })
  150. })
  151. describe('decrementFeatureUsage', function () {
  152. describe('with some usage', function () {
  153. beforeEach(async function (ctx) {
  154. await UserFeatureUsage.create({
  155. _id: ctx.userId,
  156. features: {
  157. [MOCKED_FEATURE_NAME]: { usage: 30, periodStart: new Date() },
  158. },
  159. })
  160. ctx._getAllowanceStub.resolves(100)
  161. })
  162. it('should decrement usage by 1 when cost is 1', async function (ctx) {
  163. const res = { set: () => null }
  164. await ctx.FeatureUsageRateLimiter.decrementFeatureUsage(
  165. ctx.userId,
  166. res,
  167. 1
  168. )
  169. const usages =
  170. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  171. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(71)
  172. })
  173. it('should decrement usage by the specified cost', async function (ctx) {
  174. const res = { set: () => null }
  175. await ctx.FeatureUsageRateLimiter.decrementFeatureUsage(
  176. ctx.userId,
  177. res,
  178. 5
  179. )
  180. const usages =
  181. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  182. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(75)
  183. })
  184. it('should decrement usage by 1 when cost is omitted (default)', async function (ctx) {
  185. const res = { set: () => null }
  186. await ctx.FeatureUsageRateLimiter.decrementFeatureUsage(ctx.userId, res)
  187. const usages =
  188. await ctx.FeatureUsageRateLimiter.getRemainingFeatureUses(ctx.userId)
  189. expect(usages[MOCKED_FEATURE_NAME].remainingUsage).to.equal(71)
  190. })
  191. })
  192. })
  193. })