Skip to content

Commit

Permalink
adds testing framework and signup tests
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Jun 4, 2024
1 parent 90498b6 commit 1c1d91e
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 3 deletions.
4 changes: 3 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-config.json -i"
},
"dependencies": {
"@nestjs/common": "^10.3.8",
Expand All @@ -27,6 +27,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"config": "^3.3.11",
"lodash": "4.17.21",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"shared": "workspace:*"
Expand All @@ -38,6 +39,7 @@
"@types/bcrypt": "5.0.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/lodash": "4.17.4",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
Expand Down
2 changes: 2 additions & 0 deletions api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(4000);
}

Expand Down
43 changes: 43 additions & 0 deletions api/test/auth/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TestManager } from '../utils/test-manager';
import { AuthFixtures } from './fixtures';

describe('Authentication (e2e)', () => {
let testManager: TestManager<any>;
let fixtures: AuthFixtures;
beforeAll(async () => {
testManager = await TestManager.createTestManager();
fixtures = new AuthFixtures(testManager);
});

afterEach(async () => {
await testManager.clearDatabase();
});

afterAll(async () => {
await testManager.close();
});
test(`it should throw validation errors`, async () => {
const response = await fixtures.WhenISignUpANewUserWithWrongPayload({
email: 'notanemail',
password: '12345',
});
fixtures.ThenIShouldReceiveValidationErrors(response);
});
test(`it should throw email already exist error`, async () => {
const user = await fixtures.GivenThereIsUserRegistered();
const response = await fixtures.WhenISignUpANewUserWithWrongPayload({
email: user.email,
password: '12345678',
});
fixtures.ThenIShouldReceiveAEmailAlreadyExistError(response);
});
test(`it should sign up a new user`, async () => {
const newUser = {
email: '[email protected]',
password: '12345678',
username: 'test',
};
await fixtures.WhenISignUpANewUser(newUser);
await fixtures.ThenANewUserAShouldBeCreated(newUser);
});
});
66 changes: 66 additions & 0 deletions api/test/auth/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { TestManager } from '../utils/test-manager';
import * as request from 'supertest';
import { SignUpDto } from '@shared/dto/auth/sign-up.dto';
import { createUser } from '../utils/entity-mocks';
import { User } from '@shared/dto/users/user.entity';

export class AuthFixtures {
testManager: TestManager<any>;

constructor(testManager: TestManager<any>) {
this.testManager = testManager;
}

async GivenThereIsUserRegistered(): Promise<User> {
const user = await createUser(this.testManager.getDataSource(), {
email: '[email protected]',
});
return user;
}

async WhenISignUpANewUserWithWrongPayload(
signUpDto: SignUpDto,
): Promise<request.Response> {
return request(this.testManager.getApp().getHttpServer())
.post('/auth/sign-up')
.send(signUpDto);
}

ThenIShouldReceiveValidationErrors(response: request.Response): void {
expect(response.status).toBe(400);
expect(response.body).toEqual({
message: [
'email must be an email',
'password must be longer than or equal to 8 characters',
],
error: 'Bad Request',
statusCode: 400,
});
}

ThenIShouldReceiveAEmailAlreadyExistError(response: request.Response) {
expect(response.status).toBe(409);
expect(response.body).toEqual({
message: 'Email [email protected] already exists',
error: 'Conflict',
statusCode: 409,
});
}

async WhenISignUpANewUser(signUpDto: SignUpDto): Promise<request.Response> {
return request(this.testManager.getApp().getHttpServer())
.post('/auth/sign-up')
.send(signUpDto);
}

async ThenANewUserAShouldBeCreated(newUser: Partial<User>) {
const user = await this.testManager
.getDataSource()
.getRepository(User)
.findOne({
where: { email: newUser.email },
});
expect(user.id).toBeDefined();
expect(user.email).toEqual(newUser.email);
}
}
File renamed without changes.
53 changes: 53 additions & 0 deletions api/test/utils/db-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { DataSource, EntityMetadata } from 'typeorm';
import { difference } from 'lodash';

export async function clearTestDataFromDatabase(
dataSource: DataSource,
): Promise<void> {
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const entityTableNames: string[] = dataSource.entityMetadatas
.filter(
(entityMetadata: EntityMetadata) =>
entityMetadata.tableType === 'regular' ||
entityMetadata.tableType === 'junction',
)
.map((entityMetadata: EntityMetadata) => entityMetadata.tableName);

await Promise.all(
entityTableNames.map((entityTableName: string) =>
queryRunner.query(`TRUNCATE TABLE "${entityTableName}" CASCADE`),
),
);

entityTableNames.push(dataSource.metadataTableName);
entityTableNames.push(
dataSource.options.migrationsTableName || 'migrations',
);
entityTableNames.push('spatial_ref_sys');

const databaseTableNames: string[] = (
await dataSource.query(
`SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'`,
)
).map((e: Record<string, any>) => e.table_name);

const tablesToDrop = difference(databaseTableNames, entityTableNames);

await Promise.all(
tablesToDrop.map((tableToDrop: string) =>
queryRunner.dropTable(tableToDrop),
),
);
await queryRunner.commitTransaction();
} catch (err) {
// rollback changes before throwing error
await queryRunner.rollbackTransaction();
throw err;
} finally {
// release query runner which is manually created
await queryRunner.release();
}
}
16 changes: 16 additions & 0 deletions api/test/utils/entity-mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { genSalt, hash } from 'bcrypt';
import { DataSource, DeepPartial, BaseEntity } from 'typeorm';
import { User } from '@shared/dto/users/user.entity';

export const createUser = async (
dataSource: DataSource,
additionalData: Partial<User>,
) => {
const salt = await genSalt();
const defaultData: DeepPartial<User> = {
email: '[email protected]',
password: await hash('12345678', salt),
};
const user = { ...defaultData, ...additionalData };
return dataSource.getRepository(User).save(user);
};
54 changes: 54 additions & 0 deletions api/test/utils/test-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AppModule } from '@api/app.module';
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { clearTestDataFromDatabase } from './db-helpers';

/**
* @description: Abstraction for NestJS testing workflow. For now its a basic implementation to create a test app, but can be extended to encapsulate
* common testing utilities
*/

export class TestManager<FixtureType> {
testApp: INestApplication;
dataSource: DataSource;
fixtures?: FixtureType;
constructor(
testApp: INestApplication,
dataSource: DataSource,
options?: { fixtures: FixtureType },
) {
this.testApp = testApp;
this.dataSource = dataSource;
this.fixtures = options?.fixtures;
}

static async createTestManager<FixtureType>(options?: {
fixtures: FixtureType;
}) {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const dataSource = moduleFixture.get<DataSource>(DataSource);
const testApp = moduleFixture.createNestApplication();
testApp.useGlobalPipes(new ValidationPipe());
await testApp.init();
return new TestManager<FixtureType>(testApp, dataSource);
}

async clearDatabase() {
await clearTestDataFromDatabase(this.dataSource);
}

getApp() {
return this.testApp;
}

getDataSource() {
return this.dataSource;
}

close() {
return this.testApp.close();
}
}
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion shared/contracts/auth.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { initContract } from '@ts-rest/core';
import { SignUpDto } from '@shared/dto/auth/sign-up.dto';

const contract = initContract();
export const countryContract = contract.router({
export const authContract = contract.router({
signUp: {
method: 'POST',
path: '/auth/sign-up',
Expand Down
2 changes: 1 addition & 1 deletion shared/dto/users/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Column({ nullable: true })
username: string;

@Column({ unique: true })
Expand Down

0 comments on commit 1c1d91e

Please sign in to comment.