homeresume
 
   

Redis as custom storage for NestJS rate limiter

September 14, 2021

Rate limiter is a common technique to protect against brute-force attacks. NestJS provides a module for it, the default storage is in-memory. Custom storage should be injected inside ThrottlerModule configuration.

// src/modules/app.module.ts
ThrottlerModule.forRootAsync({
imports: [ThrottlerStorageModule],
useFactory: (throttlerStorage: ThrottlerStorageService) => ({
ttl,
limit,
storage: throttlerStorage,
}),
inject: [ThrottlerStorageService],
}),

Custom storage needs to implement the ThrottlerStorage interface.

// src/modules/throttler-storage/throttler-storage.service.ts
@Injectable()
export class ThrottlerStorageService implements ThrottlerStorage {
constructor(@InjectRedis() private readonly throttlerStorageService: Redis) {}
async getRecord(key: string): Promise<number[]> {
// ...
}
async addRecord(key: string, ttl: number): Promise<void> {
// ...
}
}

Redis client should be configured inside RedisModule configuration.

// src/modules/throttler-storage/throttler-storage.module.ts
@Module({
imports: [
RedisModule.forRootAsync({
useFactory: (configService: ConfigService) => {
const redisUrl = configService.get('REDIS_URL');
return {
config: {
url: redisUrl
}
};
},
inject: [ConfigService]
})
],
providers: [ThrottlerStorageService],
exports: [ThrottlerStorageService]
})
export class ThrottlerStorageModule {}

Redis connection should be closed during the graceful shutdown.

// src/app/app.module.ts
@Injectable()
export class AppModule implements OnApplicationShutdown {
constructor(
// ...
@InjectRedis() private readonly redisConnection: Redis
) {}
async closeRedisConnection(): Promise<void> {
await this.redisConnection.quit();
this.logger.log('Redis connection is closed');
}
async onApplicationShutdown(signal: string): Promise<void> {
// ...
await Promise.all([
// ...
this.closeRedisConnection()
]).catch((error) => this.logger.error(error.message));
}
}

@liaoliaots/nestjs-redis library is used in the examples.

Postgres and Redis container services for e2e tests in Github actions

September 8, 2021

For e2e testing we need database connection, provisioning container service for Postgres database can be automated using Github actions. Environment variable for connection string for newly created database can be set in the step for running e2e tests. The same goes for Redis instance.

# ...
jobs:
build:
# Container must run in Linux-based operating systems
runs-on: ubuntu-latest
# Image from Docker hub
container: node:16.3.0-alpine3.13
# ...
strategy:
matrix:
# ...
database-name:
- e2e-testing-db
database-user:
- username
database-password:
- password
database-host:
- postgres
database-port:
- 5432
redis-host:
- redis
redis-port:
- 6379
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: ${{ matrix.database-name }}
POSTGRES_USER: ${{ matrix.database-user }}
POSTGRES_PASSWORD: ${{ matrix.database-password }}
ports:
- ${{ matrix.database-port }}:${{ matrix.database-port }}
# Set health checks to wait until postgres has started
options:
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
# ...
- run: npm run test:e2e
env:
DATABASE_URL: postgres://${{ matrix.database-user }}:${{ matrix.database-password }}@${{ matrix.database-host }}:${{ matrix.database-port }}/${{ matrix.database-name }}
REDIS_URL: redis://${{ matrix.redis-host }}:${{ matrix.redis-port }}
# ...

Testing custom repositories (NestJS/TypeORM)

September 5, 2021

Custom repositories extend base repository class and enrich it with several additional methods.

// user.repository.ts
@Injectable()
@EntityRepository(UserEntity)
export class UserRepository extends Repository<UserEntity> {
async getById(id: string): Promise<User> {
return this.findOne({ id });
}
// ...
}

Custom repository can be injected into the service using the InjectRepository decorator.

export class UserService {
constructor(
@InjectRepository(UserRepository)
private readonly userRepository: UserRepository,
) {}
async getById(id: string): Promise<User> {
return this.userRepository.getById(id);
}
// ...
}

Instantiation responsibility can be delegated to Nest by passing repository class to the forFeature method.

@Module({
imports: [
TypeOrmModule.forFeature([UserRepository]),
// ...
],
providers: [UserService],
// ...
})
export class UserModule {}

In order to properly test the custom repository, some of the methods has to be mocked.

describe('UserRepository', () => {
let userRepository: UserRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserRepository],
}).compile();
userRepository = module.get<UserRepository>(UserRepository);
});
describe('getById', () => {
it('should return found user', async () => {
const id = 'id';
const user = {
id,
};
const findOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(user as UserEntity);
const foundUser = await userRepository.getById(id);
expect(foundUser).toEqual(user);
expect(findOneSpy).toHaveBeenCalledWith(user);
});
});
});

Spies and mocking with Jest

August 19, 2021

Unit testing, in addition to output testing, includes the usage of spies and mocking. Spies are functions that let you spy on the behavior of functions that are called indirectly by some other code. Spy can be created by using jest.fn(). Mocking is injecting test values into the code during the tests. Some of the use cases will be presented below.

  • Async function and its resolved value can be mocked using mockResolvedValue. Another way to mock it is by using mockImplementation and providing a function as an argument.
const calculationService = {
calculate: jest.fn(),
};
jest.spyOn(calculationService, "calculate").mockResolvedValue(value);
jest.spyOn(calculationService, "calculate").mockImplementation(async (a) => Promise.resolve(a));
  • Rejected async function can be mocked using mockRejectedValue and mockImplementation.
jest.spyOn(calculationService, "calculate").mockRejectedValue(value);
jest.spyOn(calculationService, "calculate")
.mockImplementation(async () => Promise.reject(new Error(errorMessage)));
await expect(calculateSomething(calculationService)).rejects.toThrowError(Error);
  • Sync function and its return value can be mocked using mockReturnValue and mockImplementation.
jest.spyOn(calculationService, "calculate").mockReturnValue(value);
jest.spyOn(calculationService, "calculate").mockImplementation((a) => a);
  • Chained methods can be mocked using mockReturnThis.
// calculationService.get().calculate();
jest.spyOn(calculationService, "get").mockReturnThis();
  • Async and sync functions which are called multiple times can be mocked with different values using mockResolvedValueOnce and mockReturnValueOnce respectively and mockImplementationOnce.
jest.spyOn(calculationService, "calculate").mockResolvedValueOnce(value)
.mockResolvedValueOnce(otherValue);
jest.spyOn(calculationService, "calculate").mockReturnValueOnce(value)
.mockReturnValueOnce(otherValue);
jest.spyOn(calculationService, "calculate").mockImplementationOnce((a) => a + 3)
.mockImplementationOnce((a) => a + 5);
  • External modules can be mocked similarly as spies. For the following example let's suppose axios package is already used in one function. The following example represents a test file where axios is mocked using jest.mock().
import axios from 'axios';
jest.mock('axios');
// within test case
axios.get.mockResolvedValue(data);
  • Manual mocks are resolved by writing corresponding modules in __mocks__ directory, e.g. fs/promises mock will be stored in __mocks__/fs/promises.js file. fs/promises mock will be resolved using jest.mock() in the test file.
jest.mock('fs/promises');
  • To assert called arguments for a mocked function, an assertion can be done using toHaveBeenCalledWith matcher.
expect(calculationService.calculate).toHaveBeenCalledWith(firstArgument, secondArgument);
  • To assert skipped call for a mocked function, an assertion can be done using not.toHaveBeenCalled matcher.
expect(calculationService.calculate).not.toHaveBeenCalled();
  • To assert called arguments for the exact call when a mocked function is called multiple times, an assertion can be done using toHaveBeenNthCalledWith matcher.
argumentsList.forEach((argumentList, index) => {
expect(calculationService.calculate).toHaveBeenNthCalledWith(
index + 1,
argumentList,
);
});
  • Mocks should be reset to their initial implementation before each test case.
beforeEach(() => {
jest.resetAllMocks();
});

Server-Sent Events 101

August 18, 2021

Server-Sent Events (SSE) is a unidirectional communication between client and server where the server sends the events in text/event-stream format to the client once the client-server connection is established by the client. The client initiates the connection with the server using EventSource API. Previously mentioned API can also be used for listening to the events from the server, listening for the errors, and closing the connection.

const eventSource = new EventSource(url);
eventSource.onmessage = ({ data }) => {
const eventData = JSON.parse(data);
// handling the data from the server
};
eventSource.onerror = () => {
// error handling
};
eventSource.close();

A server can filter clients by query parameter and send them only the appropriate events. In the following example server sends the events only to a specific client distinguished by its e-mail address.

@Sse('sse')
sse(@Query() sseQuery: SseQueryDto): Observable<MessageEvent> {
const subject$ = new Subject();
this.eventService.on(FILTER_VERIFIED, data => {
if (sseQuery.email !== data.email) return;
subject$.next({ isVerifiedFilter: true });
});
return subject$.pipe(
map((data: MessageEventData): MessageEvent => ({ data })),
);
}
2020

 

© 2021