Skip to content

Commit

Permalink
fix: use ceil instead of floor when calculating expire and block expire
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcdo29 committed Aug 8, 2024
1 parent fe981c1 commit 25919fc
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 114 deletions.
4 changes: 2 additions & 2 deletions src/throttler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export class ThrottlerStorageService implements ThrottlerStorage, OnApplicationS
* Get the expiration time in seconds from a single record.
*/
private getExpirationTime(key: string): number {
return Math.floor((this.storage.get(key).expiresAt - Date.now()) / 1000);
return Math.ceil((this.storage.get(key).expiresAt - Date.now()) / 1000);
}

/**
* Get the block expiration time in seconds from a single record.
*/
private getBlockExpirationTime(key: string): number {
return Math.floor((this.storage.get(key).blockExpiresAt - Date.now()) / 1000);
return Math.ceil((this.storage.get(key).blockExpiresAt - Date.now()) / 1000);
}

/**
Expand Down
253 changes: 141 additions & 112 deletions test/controller.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,163 +1,192 @@
import { INestApplication } from '@nestjs/common';
import { Controller, INestApplication, Post } from '@nestjs/common';
import { AbstractHttpAdapter, APP_GUARD } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { Test, TestingModule } from '@nestjs/testing';
import { ThrottlerGuard } from '../src';
import { setTimeout } from 'node:timers/promises';
import { Throttle, ThrottlerGuard } from '../src';
import { THROTTLER_OPTIONS } from '../src/throttler.constants';
import { ControllerModule } from './app/controllers/controller.module';
import { httPromise } from './utility/httpromise';
import { setTimeout } from 'node:timers/promises';

jest.setTimeout(45000);

describe.each`
adapter | adapterName
${new ExpressAdapter()} | ${'Express'}
${new FastifyAdapter()} | ${'Fastify'}
`('$adapterName Throttler', ({ adapter }: { adapter: AbstractHttpAdapter }) => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [ControllerModule],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
}).compile();
`(
'$adapterName Throttler',
({ adapter }: { adapter: AbstractHttpAdapter; adapterName: string }) => {
@Controller('test/throttle')
class ThrottleTestController {
@Throttle({ default: { limit: 1, ttl: 1000, blockDuration: 1000 } })
@Post()
async testThrottle() {
return {
code: 'THROTTLE_TEST',
};
}
}
let app: INestApplication;

app = moduleFixture.createNestApplication(adapter);
await app.listen(0);
});
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [ControllerModule],
controllers: [ThrottleTestController],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
}).compile();

afterAll(async () => {
await app.close();
});
app = moduleFixture.createNestApplication(adapter);
await app.listen(0);
});

describe('controllers', () => {
let appUrl: string;
beforeAll(async () => {
appUrl = await app.getUrl();
afterAll(async () => {
await app.close();
});

/**
* Tests for setting `@Throttle()` at the method level and for ignore routes
*/
describe('AppController', () => {
it('GET /ignored', async () => {
const response = await httPromise(appUrl + '/ignored');
expect(response.data).toEqual({ ignored: true });
expect(response.headers).not.toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /^\d+$/,
});
describe('controllers', () => {
let appUrl: string;
beforeAll(async () => {
appUrl = await app.getUrl();
});
it('GET /ignore-user-agents', async () => {
const response = await httPromise(appUrl + '/ignore-user-agents', 'GET', {
'user-agent': 'throttler-test/0.0.0',

/**
* Tests for setting `@Throttle()` at the method level and for ignore routes
*/
describe('AppController', () => {
it('GET /ignored', async () => {
const response = await httPromise(appUrl + '/ignored');
expect(response.data).toEqual({ ignored: true });
expect(response.headers).not.toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /^\d+$/,
});
});
expect(response.data).toEqual({ ignored: true });
expect(response.headers).not.toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /^\d+$/,
it('GET /ignore-user-agents', async () => {
const response = await httPromise(appUrl + '/ignore-user-agents', 'GET', {
'user-agent': 'throttler-test/0.0.0',
});
expect(response.data).toEqual({ ignored: true });
expect(response.headers).not.toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /^\d+$/,
});
});
});
it('GET /', async () => {
const response = await httPromise(appUrl + '/');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /^\d+$/,
it('GET /', async () => {
const response = await httPromise(appUrl + '/');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /^\d+$/,
});
});
});
});
/**
* Tests for setting `@Throttle()` at the class level and overriding at the method level
*/
describe('LimitController', () => {
it.each`
method | url | limit | blockDuration
${'GET'} | ${''} | ${2} | ${5000}
${'GET'} | ${'/higher'} | ${5} | ${15000}
`(
'$method $url',
async ({
method,
url,
limit,
blockDuration,
}: {
method: 'GET';
url: string;
limit: number;
blockDuration: number;
}) => {
for (let i = 0; i < limit; i++) {
/**
* Tests for setting `@Throttle()` at the class level and overriding at the method level
*/
describe('LimitController', () => {
it.each`
method | url | limit | blockDuration
${'GET'} | ${''} | ${2} | ${5000}
${'GET'} | ${'/higher'} | ${5} | ${15000}
`(
'$method $url',
async ({
method,
url,
limit,
blockDuration,
}: {
method: 'GET';
url: string;
limit: number;
blockDuration: number;
}) => {
for (let i = 0; i < limit; i++) {
const response = await httPromise(appUrl + '/limit' + url, method);
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': limit.toString(),
'x-ratelimit-remaining': (limit - (i + 1)).toString(),
'x-ratelimit-reset': /^\d+$/,
});
}
const errRes = await httPromise(appUrl + '/limit' + url, method);
expect(errRes.data).toMatchObject({ statusCode: 429, message: /ThrottlerException/ });
expect(errRes.headers).toMatchObject({
'retry-after': /^\d+$/,
});
expect(errRes.status).toBe(429);
await setTimeout(blockDuration);
const response = await httPromise(appUrl + '/limit' + url, method);
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': limit.toString(),
'x-ratelimit-remaining': (limit - 1).toString(),
'x-ratelimit-reset': /^\d+$/,
});
},
);
});
/**
* Tests for setting throttle values at the `forRoot` level
*/
describe('DefaultController', () => {
it('GET /default', async () => {
const limit = 5;
const blockDuration = 20000; // 20 second
for (let i = 0; i < limit; i++) {
const response = await httPromise(appUrl + '/default');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': limit.toString(),
'x-ratelimit-remaining': (limit - (i + 1)).toString(),
'x-ratelimit-reset': /^\d+$/,
});
}
const errRes = await httPromise(appUrl + '/limit' + url, method);
const errRes = await httPromise(appUrl + '/default');
expect(errRes.data).toMatchObject({ statusCode: 429, message: /ThrottlerException/ });
expect(errRes.headers).toMatchObject({
'retry-after': /^\d+$/,
});
expect(errRes.status).toBe(429);
await setTimeout(blockDuration);
const response = await httPromise(appUrl + '/limit' + url, method);
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': limit.toString(),
'x-ratelimit-remaining': (limit - 1).toString(),
'x-ratelimit-reset': /^\d+$/,
});
},
);
});
/**
* Tests for setting throttle values at the `forRoot` level
*/
describe('DefaultController', () => {
it('GET /default', async () => {
const limit = 5;
const blockDuration = 20000; // 20 second
for (let i = 0; i < limit; i++) {
const response = await httPromise(appUrl + '/default');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': limit.toString(),
'x-ratelimit-remaining': (limit - (i + 1)).toString(),
'x-ratelimit-remaining': (limit - 1).toString(),
'x-ratelimit-reset': /^\d+$/,
});
}
const errRes = await httPromise(appUrl + '/default');
expect(errRes.data).toMatchObject({ statusCode: 429, message: /ThrottlerException/ });
expect(errRes.headers).toMatchObject({
'retry-after': /^\d+$/,
});
expect(errRes.status).toBe(429);
await setTimeout(blockDuration);
const response = await httPromise(appUrl + '/default');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': limit.toString(),
'x-ratelimit-remaining': (limit - 1).toString(),
'x-ratelimit-reset': /^\d+$/,
});

describe('ThrottlerTestController', () => {
it('GET /test/throttle', async () => {
const makeRequest = async () => httPromise(appUrl + '/test/throttle', 'POST', {}, {});
const res1 = await makeRequest();
expect(res1.status).toBe(201);
await setTimeout(1000);
const res2 = await makeRequest();
expect(res2.status).toBe(201);
const res3 = await makeRequest();
expect(res3.status).toBe(429);
const res4 = await makeRequest();
expect(res4.status).toBe(429);
});
});
});
});
});
},
);
describe('SkipIf suite', () => {
it('should skip throttling if skipIf returns true', async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
Expand Down

0 comments on commit 25919fc

Please sign in to comment.