Skip to content

Commit

Permalink
test(api): Add tests for evaluate-api-rate-limit use-case
Browse files Browse the repository at this point in the history
  • Loading branch information
rifont committed Nov 14, 2023
1 parent dc4abd0 commit 944df01
Showing 1 changed file with 95 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { GetApiRateLimitConfiguration } from '../get-api-rate-limit-configuratio
import { SharedModule } from '../../../shared/shared.module';
import { RateLimitingModule } from '../../rate-limiting.module';

const mockApiRateLimitConfiguration = { burstAllowance: 0.1, refillInterval: 1 };
const mockApiRateLimitConfiguration = { burstAllowance: 0.2, refillInterval: 2 };
const mockDefaultLimit = 60;
const mockBurstLimit = mockDefaultLimit * (1 + mockApiRateLimitConfiguration.burstAllowance);
const mockBurstLimit = 72;
const mockRemaining = mockBurstLimit - 1;
const mockReset = Date.now() + 1000;
const mockReset = 1699954067112;
const mockApiRateLimitCategory = ApiRateLimitCategoryTypeEnum.GLOBAL;

describe('EvaluateApiRateLimit', async () => {
let useCase: EvaluateApiRateLimit;
Expand Down Expand Up @@ -45,13 +46,16 @@ describe('EvaluateApiRateLimit', async () => {
getApiRateLimitConfiguration = moduleRef.get<GetApiRateLimitConfiguration>(GetApiRateLimitConfiguration);
cacheService = moduleRef.get<ICacheService>(CacheService);

getApiRateLimitStub = sinon.stub(getApiRateLimit, 'execute' as any).resolves(mockDefaultLimit);
getApiRateLimitStub = sinon.stub(getApiRateLimit, 'execute').resolves(mockDefaultLimit);
getApiRateLimitConfigurationStub = sinon
.stub(getApiRateLimitConfiguration, 'defaultApiRateLimitConfiguration' as any)
.stub(getApiRateLimitConfiguration, 'defaultApiRateLimitConfiguration')
.value(mockApiRateLimitConfiguration);
// The first value is the remaining rate limit, the second value is the reset time
cacheServiceEvalStub = sinon.stub(cacheService, 'eval' as any).resolves([mockRemaining, mockReset]);
cacheServiceIsEnabledStub = sinon.stub(cacheService, 'cacheEnabled' as any).returns(true);
// This mock is slightly uncomfortable because it's dependent on the algorithm implementation,
// but it is required due to the cache having a hard dependency on running a Lua script which
// would require further mocking.
// The first value is the remaining rate limit, the second value is the reset time.
cacheServiceEvalStub = sinon.stub(cacheService, 'eval').resolves([mockRemaining, mockReset]);
cacheServiceIsEnabledStub = sinon.stub(cacheService, 'cacheEnabled').returns(true);
});

afterEach(() => {
Expand All @@ -62,63 +66,81 @@ describe('EvaluateApiRateLimit', async () => {
});

describe('Successful evaluation', () => {
it('should return success equal to true', async () => {
cacheServiceEvalStub;

it('should return a boolean success value', async () => {
const result = await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: ApiRateLimitCategoryTypeEnum.GLOBAL,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(result.success).to.be.true;
expect(typeof result.success).to.equal('boolean');
});

it('should return correct limit adjusted for burst capability', async () => {
cacheServiceEvalStub;

it('should return a non-zero limit', async () => {
const result = await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: ApiRateLimitCategoryTypeEnum.GLOBAL,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(result.limit).to.equal(mockBurstLimit);
expect(result.limit).to.be.greaterThan(0);
});

it('should return correct remaining tokens', async () => {
cacheServiceEvalStub;

it('should return a non-zero remaining tokens ', async () => {
const result = await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: ApiRateLimitCategoryTypeEnum.GLOBAL,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(result.remaining).to.equal(mockRemaining);
expect(result.remaining).to.be.greaterThan(0);
});

it('should return a reset greater than 0', async () => {
cacheServiceEvalStub;

const result = await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: ApiRateLimitCategoryTypeEnum.GLOBAL,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(result.reset).to.be.greaterThan(0);
});
});

describe('Successful invocation of cache methods', () => {
it('should call the cache service eval method', async () => {
await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(cacheServiceEvalStub.calledOnce).to.be.true;
});

it('should call the cache service cacheEnabled method', async () => {
await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(cacheServiceIsEnabledStub.calledOnce).to.be.true;
});
});

describe('Cache errors', () => {
it('should throw error when a cache operation fails', async () => {
cacheServiceEvalStub.throws(new Error());
Expand All @@ -128,7 +150,7 @@ describe('EvaluateApiRateLimit', async () => {
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: ApiRateLimitCategoryTypeEnum.GLOBAL,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);
throw new Error('Should not reach here');
Expand All @@ -145,7 +167,7 @@ describe('EvaluateApiRateLimit', async () => {
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: ApiRateLimitCategoryTypeEnum.GLOBAL,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);
throw new Error('Should not reach here');
Expand All @@ -154,4 +176,49 @@ describe('EvaluateApiRateLimit', async () => {
}
});
});

describe('Rate limit algorithm parameters', () => {
it('should call the rate limit algorithm with the correct refill rate', async () => {
const rateLimitAlgorithmSpy = sinon.spy(Ratelimit, 'tokenBucket');
const testRefillRate = mockDefaultLimit * mockApiRateLimitConfiguration.refillInterval;

await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(rateLimitAlgorithmSpy.getCall(0).args[0]).to.equal(testRefillRate);
});

it('should call the rate limit algorithm with the correct refill interval', async () => {
const rateLimitAlgorithmSpy = sinon.spy(Ratelimit, 'tokenBucket');

await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(rateLimitAlgorithmSpy.getCall(0).args[1]).to.equal(`${mockApiRateLimitConfiguration.refillInterval} s`);
});

it('should call the rate limit algorithm with the correct burst limit', async () => {
const rateLimitAlgorithmSpy = sinon.spy(Ratelimit, 'tokenBucket');

await useCase.execute(
EvaluateApiRateLimitCommand.create({
organizationId: session.organization._id,
environmentId: session.environment._id,
apiRateLimitCategory: mockApiRateLimitCategory,
})
);

expect(rateLimitAlgorithmSpy.getCall(0).args[2]).to.equal(mockBurstLimit);
});
});
});

0 comments on commit 944df01

Please sign in to comment.