Skip to content

Commit

Permalink
test: write integration tests for authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
tericcabrel committed Jun 3, 2024
1 parent d6f8f03 commit 67856a7
Show file tree
Hide file tree
Showing 25 changed files with 659 additions and 199 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Run tests
run: |
yarn prisma migrate dev --schema=packages/domain/prisma/schema.prisma
yarn test
yarn test --runInBand --coverage
preview-frontend:
runs-on: ubuntu-latest
Expand Down
19 changes: 19 additions & 0 deletions apps/backend/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
NODE_ENV=test
APP_VERSION=1.1.0
HOST=http:https://localhost
PORT=7509
INTROSPECTION_ENABLED=false
DATABASE_URL="mysql:https://root:[email protected]:3313/snipcode"
ADMIN_PASSWORD=qwerty
CONVERTKIT_API_KEY=convertkit-api-key
CONVERTKIT_FORM_ID=convertkit-form-id
CONVERTKIT_TAG_ID=convertkit-tag-id
GITHUB_CLIENT_ID=github-client-id
GITHUB_CLIENT_SECRET=github-client-secret
WEB_APP_URL=http:https://localhost:7500
WEB_AUTH_SUCCESS_URL=http:https://localhost:7500/auth/success
WEB_AUTH_ERROR_URL=http:https://localhost:7500/auth/error
SESSION_LIFETIME=90
SENTRY_DSN=sentry-dsn
SENTRY_ENABLED=false
SNIPPET_RENDERER_API_URL=http:https://localhost:3000/dev
6 changes: 1 addition & 5 deletions apps/backend/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
module.exports = {
root: true,
extends: '../../.eslintrc.json',
ignorePatterns: [
'jest.config.ts',
'.eslintrc.js',
'graphql.schema.ts'
],
ignorePatterns: ['jest.config.ts', '.eslintrc.js', 'graphql.schema.ts', 'generated.ts'],
parserOptions: {
ecmaVersion: 2023,
sourceType: 'module',
Expand Down
8 changes: 5 additions & 3 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
"gql:gen:types": "ts-node scripts/generate-graphql-types.ts",
"lint": "eslint \"{src,scripts}/**/*.ts\" --fix",
"prod": "node dist/main",
"test": "jest --watch",
"test:coverage": "jest --coverage",
"test": "yarn workspace @snipcode/domain db:test && dotenv -e .env.test -- jest --watchAll --runInBand",
"test:coverage": "yarn test --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:it": "jest integration.spec.ts"
"test:it": "yarn test integration.spec.ts"
},
"dependencies": {
"@apollo/server": "4.10.4",
Expand All @@ -37,8 +37,10 @@
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.8",
"@ngneat/falso": "7.2.0",
"@types/express": "4.17.21",
"@types/supertest": "6.0.2",
"dotenv-cli": "7.4.2",
"source-map-support": "0.5.21",
"supertest": "7.0.0",
"ts-loader": "9.5.1",
Expand Down
23 changes: 0 additions & 23 deletions apps/backend/src/features/app/app.controller.spec.ts

This file was deleted.

22 changes: 22 additions & 0 deletions apps/backend/src/features/app/app.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import request from 'supertest';

import { TestServer, startTestServer } from '../../utils/tests/server';

describe('Application Common Endpoints', () => {
let server: TestServer;

beforeAll(async () => {
server = await startTestServer();
});

afterAll(async () => {
await server.close();
});

test('The entry endpoint returns a Hello world message', async () => {
const response = await request(server.app.getHttpServer()).get('/').expect(200);

expect(response.body).toEqual({});
expect(response.text).toBe('Hello from Snipcode!');
});
});
51 changes: 51 additions & 0 deletions apps/backend/src/features/app/app.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService, RoleService, UserService } from '@snipcode/domain';
import { mock } from 'jest-mock-extended';

import { AppService } from './app.service';

const prismaServiceMock = mock<PrismaService>();
const roleServiceMock = mock<RoleService>();
const userServiceMock = mock<UserService>();

describe('Test App Service', () => {
let appService: AppService;
let roleService: RoleService;
let userService: UserService;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
providers: [
{ provide: PrismaService, useValue: prismaServiceMock },
ConfigService,
{ provide: UserService, useValue: userServiceMock },
{ provide: RoleService, useValue: roleServiceMock },
AppService,
],
}).compile();

appService = app.get<AppService>(AppService);
roleService = app.get<RoleService>(RoleService);
userService = app.get<UserService>(UserService);
});

test('load default roles and users on service initialization', async () => {
const role = {
createdAt: new Date(),
description: 'Administrator',
id: '1',
level: 1,
name: 'admin' as const,
updatedAt: new Date(),
};

roleServiceMock.findByName.mockResolvedValueOnce(role);

await expect(appService.onModuleInit()).resolves.not.toThrow();

expect(roleService.loadRoles).toHaveBeenCalledTimes(1);
expect(userService.loadAdminUser).toHaveBeenCalledTimes(1);
expect(userService.loadAdminUser).toHaveBeenCalledWith(role, 'qwerty');
});
});
4 changes: 0 additions & 4 deletions apps/backend/src/features/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ export class AppService implements OnModuleInit {

const adminRole = await this.roleService.findByName('admin');

if (!adminRole) {
throw new Error('[Data Loader]: Role administrator not found');
}

const adminPassword = this.configService.get<string>('ADMIN_PASSWORD');

await this.userService.loadAdminUser(adminRole, adminPassword);
Expand Down
172 changes: 172 additions & 0 deletions apps/backend/src/features/auth/graphql/auth.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { PrismaService, RoleService, UserService } from '@snipcode/domain';
import { isValidUUIDV4 } from '@snipcode/utils';
import request from 'supertest';

import { TestHelper } from '../../../utils/tests/helpers';
import { TestServer, startTestServer } from '../../../utils/tests/server';

const graphqlEndpoint = '/graphql';

describe('Test Authentication', () => {
let server: TestServer;
let testHelper: TestHelper;
let prismaService: PrismaService;
let roleService: RoleService;
let userService: UserService;

beforeAll(async () => {
server = await startTestServer();

prismaService = server.app.get<PrismaService>(PrismaService);
userService = server.app.get<UserService>(UserService);
roleService = server.app.get<RoleService>(RoleService);

testHelper = new TestHelper(prismaService, roleService, userService);
});

beforeEach(async () => {
await testHelper.cleanDatabase();
});

afterAll(async () => {
await server.close();
});

test('Register a user', async () => {
const query = `
mutation SignupUser($input: SignupUserInput!) {
signupUser(input: $input) {
__typename
message
}
}
`;
const variables = {
input: {
email: '[email protected]',
name: 'John Doe',
password: 'password',
},
};

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query, variables })
.expect(200);

expect(response.body.data.signupUser).toMatchObject({
__typename: 'SignupUserResult',
message: 'Account created successfully!',
});
});

test('Register a user with an existing email address will returns an error', async () => {
const query = `
mutation SignupUser($input: SignupUserInput!) {
signupUser(input: $input) {
__typename
message
}
}
`;
const variables = {
input: {
email: '[email protected]',
name: 'John Doe',
password: 'password',
},
};

await testHelper.createTestUser({ email: variables.input.email });

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query, variables })
.expect(200);

expect(response.body.errors[0].extensions.code).toEqual('EMAIL_ALREADY_TAKEN');
expect(response.body.errors[0].message).toEqual('The email address is already taken');
});

test('Returns an error when authenticating with bad credentials', async () => {
const query = `
mutation LoginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
token
}
}
`;
const variables = {
email: '[email protected]',
password: '123456',
};

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query, variables })
.expect(200);

expect(response.body.errors[0].extensions.code).toEqual('LOGIN_FAILED');
expect(response.body.errors[0].message).toEqual('Invalid email address or password.');
});

test('Returns a token when authenticating with correct credentials', async () => {
const query = `
mutation LoginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
token
}
}
`;

await testHelper.createTestUser({
email: '[email protected]',
isEnabled: true,
password: 'password',
role: 'user',
});

const variables = {
email: '[email protected]',
password: 'password',
};

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query, variables })
.expect(200);

expect(response.body.data.loginUser.token).toBeDefined();
expect(isValidUUIDV4(response.body.data.loginUser.token)).toBe(true);
});

test('Returns an error message when trying to authenticate with a disabled account', async () => {
const query = `
mutation LoginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
token
}
}
`;

await testHelper.createTestUser({
email: '[email protected]',
isEnabled: false,
password: 'password',
role: 'user',
});

const variables = {
email: '[email protected]',
password: 'password',
};

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query, variables })
.expect(200);

expect(response.body.errors[0].extensions.code).toEqual('ACCOUNT_DISABLED');
expect(response.body.errors[0].message).toEqual('Your account is disabled!');
});
});
21 changes: 3 additions & 18 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,17 @@ import './configs/instrument';

import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import * as Sentry from '@sentry/node';

import { AppModule } from './app.module';
import { ApplicationExceptionFilter } from './configs/exception.filter';
import { setupAppServer } from './server';
import { EnvironmentVariables } from './types/common';
import { CORS_APOLLO_STUDIO_URL } from './utils/constants';

const bootstrap = async () => {
const app = await NestFactory.create(AppModule);

const { httpAdapter } = app.get(HttpAdapterHost);
const logger = new Logger('NestApplication');

Sentry.setupNestErrorHandler(app, new ApplicationExceptionFilter(httpAdapter));
const app = await setupAppServer({ logger });

const configService = app.get(ConfigService<EnvironmentVariables, true>);

app.enableCors({
credentials: true,
origin: [configService.get('WEB_APP_URL'), CORS_APOLLO_STUDIO_URL],
});

app.enableShutdownHooks();

const logger = new Logger('NestApplication');

const port = configService.get<number>('PORT');
const host = configService.get<string>('HOST');

Expand Down

0 comments on commit 67856a7

Please sign in to comment.