Skip to content

Commit

Permalink
feat: add redis-clusters to docker and adjust tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kkoomen committed Dec 22, 2022
1 parent f38ee80 commit 0f8ad1e
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 99 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisClusterService } from 'nestjs-throttler-storage-redis';

const nodes = [
{ host: 'localhost', port: 6379 },
{ host: 'localhost', port: 6380 }
{ host: '127.0.0.1', port: 7000 },
{ host: '127.0.0.1', port: 7001 },
...
{ host: '127.0.0.1', port: 7005 },
];

const options = {
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ services:
container_name: redis
ports:
- '6379:6379'

redis_cluster:
image: grokzen/redis-cluster
environment:
- 'IP=0.0.0.0'
ports:
- '7000-7050:7000-7050'
- '5000-5010:5000-5010'
3 changes: 2 additions & 1 deletion test/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard } from '@nestjs/throttler';
import { ClusterControllerModule } from './controllers/cluster-controller.module';
import { ControllerModule } from './controllers/controller.module';

@Module({
imports: [ControllerModule],
imports: [ControllerModule, ClusterControllerModule],
providers: [
{
provide: APP_GUARD,
Expand Down
22 changes: 22 additions & 0 deletions test/app/controllers/cluster-controller.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisClusterService } from '../../../src/throttler-storage-redis-cluster.service';
import { clusterNodes } from '../../utility/redis';
import { AppService } from '../app.service';
import { AppController } from './app.controller';
import { DefaultController } from './default.controller';
import { LimitController } from './limit.controller';

@Module({
imports: [
ThrottlerModule.forRoot({
limit: 5,
ttl: 60,
ignoreUserAgents: [/throttler-test/g],
storage: new ThrottlerStorageRedisClusterService(clusterNodes),
}),
],
controllers: [AppController, DefaultController, LimitController],
providers: [AppService],
})
export class ClusterControllerModule {}
217 changes: 122 additions & 95 deletions test/controller.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,120 +4,147 @@ import { ExpressAdapter } from '@nestjs/platform-express';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { Test, TestingModule } from '@nestjs/testing';
import { ThrottlerGuard } from '@nestjs/throttler';
import Redis, { Cluster } from 'ioredis';
import { ClusterControllerModule } from './app/controllers/cluster-controller.module';
import { ControllerModule } from './app/controllers/controller.module';
import { httPromise } from './utility/httpromise';
import { redis } from './utility/redis';
import { redis, cluster } from './utility/redis';

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

beforeAll(async () => {
await redis.flushall();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [ControllerModule],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
}).compile();
async function flushdb(redisOrCluster: Redis | Cluster) {
if (redisOrCluster instanceof Redis) {
await redisOrCluster.flushall();
} else {
await Promise.all(redisOrCluster.nodes('master').map(function (node) {
return node.flushall();
}));
}
}

app = moduleFixture.createNestApplication(adapter);
await app.listen(0);
});
describe.each`
instance | instanceType
${redis} | ${'single'}
${cluster} | ${'cluster'}
`('Redis $instanceType instance', ({ instance: redisOrCluster }: { instance: Redis | Cluster }) => {

afterAll(async () => {
await app.close();
if (adapter instanceof FastifyAdapter) {
await redis.quit();
if (redisOrCluster instanceof Cluster) {
redisOrCluster.disconnect();
}
});

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

beforeAll(async () => {
appUrl = await app.getUrl();
await flushdb(redisOrCluster);
const config = {
imports: [ControllerModule],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
};

if (redisOrCluster instanceof Cluster) {
config.imports = [ClusterControllerModule];
}

const moduleFixture: TestingModule = await Test.createTestingModule(config).compile();
app = moduleFixture.createNestApplication(adapter);
await app.listen(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+/,
});
afterAll(async () => {
await app.close();
});

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
${'GET'} | ${''} | ${2}
${'GET'} | ${'/higher'} | ${5}
`(
'$method $url',
async ({ method, url, limit }: { method: 'GET'; url: string; limit: 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+/,
/**
* Tests for setting `@Throttle()` at the class level and overriding at the method level
*/
describe('LimitController', () => {
it.each`
method | url | limit
${'GET'} | ${''} | ${2}
${'GET'} | ${'/higher'} | ${5}
`(
'$method $url',
async ({ method, url, limit }: { method: 'GET'; url: string; limit: 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+/,
});
}
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);
},
);
});
/**
* Tests for setting throttle values at the `forRoot` level
*/
describe('DefaultController', () => {
it('GET /default', async () => {
const response = await httPromise(appUrl + '/default');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': '5',
'x-ratelimit-remaining': '4',
'x-ratelimit-reset': /\d+/,
});
expect(errRes.status).toBe(429);
},
);
});
/**
* Tests for setting throttle values at the `forRoot` level
*/
describe('DefaultController', () => {
it('GET /default', async () => {
const response = await httPromise(appUrl + '/default');
expect(response.data).toEqual({ success: true });
expect(response.headers).toMatchObject({
'x-ratelimit-limit': '5',
'x-ratelimit-remaining': '4',
'x-ratelimit-reset': /\d+/,
});
});
});
Expand Down
12 changes: 11 additions & 1 deletion test/utility/redis.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import Redis from 'ioredis';
import Redis, { Cluster } from 'ioredis';

export const clusterNodes = [
{ host: '127.0.0.1', port: 7000 },
{ host: '127.0.0.1', port: 7001 },
{ host: '127.0.0.1', port: 7002 },
{ host: '127.0.0.1', port: 7003 },
{ host: '127.0.0.1', port: 7004 },
{ host: '127.0.0.1', port: 7005 },
]

export const cluster = new Cluster(clusterNodes);
export const redis = new Redis();

0 comments on commit 0f8ad1e

Please sign in to comment.