Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(backend): add integration tests on the backend #69

Merged
merged 6 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:typescript-sort-keys/recommended",
"plugin:jest/recommended"
"plugin:jest/recommended",
"turbo"
],
"plugins": [
"sort-destructure-keys",
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
MYSQL_DATABASE: test
MYSQL_PORT: 3306
DATABASE_URL: mysql://root:[email protected]:3306/test
TEST_DATABASE_URL: mysql://root:[email protected]:3306/test
CONVERTKIT_API_KEY: api_key
CONVERTKIT_FORM_ID: form_id
outputs:
Expand Down
12 changes: 11 additions & 1 deletion apps/backend/src/configs/exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ArgumentsHost, Catch } from '@nestjs/common';
import { AbstractHttpAdapter, BaseExceptionFilter } from '@nestjs/core';
import { GqlArgumentsHost, GqlContextType } from '@nestjs/graphql';
import { isAppError } from '@snipcode/utils';
import { Response } from 'express';
import { GraphQLError } from 'graphql';

@Catch()
Expand All @@ -23,7 +24,16 @@ export class ApplicationExceptionFilter extends BaseExceptionFilter {
});
}
} else {
// Handle HTTP exceptions
if (isAppError(exception)) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();

response.status(400).json({
code: exception.code,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
super.catch(exception, host);
}
}
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/features/app/app.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const prismaServiceMock = mock<PrismaService>();
const roleServiceMock = mock<RoleService>();
const userServiceMock = mock<UserService>();

const { ADMIN_PASSWORD } = process.env;

describe('Test App Service', () => {
let appService: AppService;
let roleService: RoleService;
Expand Down Expand Up @@ -46,6 +48,6 @@ describe('Test App Service', () => {

expect(roleService.loadRoles).toHaveBeenCalledTimes(1);
expect(userService.loadAdminUser).toHaveBeenCalledTimes(1);
expect(userService.loadAdminUser).toHaveBeenCalledWith(role, 'qwerty');
expect(userService.loadAdminUser).toHaveBeenCalledWith(role, ADMIN_PASSWORD);
});
});
3 changes: 2 additions & 1 deletion apps/backend/src/features/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AuthController } from './rest/auth.controller';
import { GithubService } from './services/github.service';

@Module({
providers: [AuthResolvers, GithubService, AuthController],
controllers: [AuthController],
providers: [AuthResolvers, GithubService],
})
export class AuthFeatureModule {}
176 changes: 25 additions & 151 deletions apps/backend/src/features/auth/graphql/auth.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PrismaService, RoleService, SessionService, UserService } from '@snipcode/domain';
import { generateJwtToken, isValidUUIDV4 } from '@snipcode/utils';
import { SessionService } from '@snipcode/domain';
import { isValidUUIDV4 } from '@snipcode/utils';
import request from 'supertest';

import { TestHelper } from '../../../utils/tests/helpers';
Expand All @@ -10,20 +10,14 @@ const graphqlEndpoint = '/graphql';
describe('Test Authentication', () => {
let server: TestServer;
let testHelper: TestHelper;
let prismaService: PrismaService;
let roleService: RoleService;
let sessionService: SessionService;
let userService: UserService;

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

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

testHelper = new TestHelper(prismaService, roleService, userService);
testHelper = new TestHelper(server.app, graphqlEndpoint);
});

beforeEach(async () => {
Expand Down Expand Up @@ -81,7 +75,7 @@ describe('Test Authentication', () => {
},
};

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

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
Expand Down Expand Up @@ -127,11 +121,10 @@ describe('Test Authentication', () => {
}
`;

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

const variables = {
Expand Down Expand Up @@ -163,11 +156,10 @@ describe('Test Authentication', () => {
}
`;

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

const variables = {
Expand All @@ -186,14 +178,14 @@ describe('Test Authentication', () => {
expect(error.message).toEqual('Your account is disabled!');
});

test('Returns when retrieving the authenticated user without an authentication token', async () => {
test('Returns an error when retrieving the authenticated user without an authentication token', async () => {
const authenticatedUserQuery = `
query AuthenticatedUser {
authenticatedUser {
id
}
query AuthenticatedUser {
authenticatedUser {
id
}
`;
}
`;

const response = await request(server.app.getHttpServer())
.post(graphqlEndpoint)
Expand All @@ -207,71 +199,13 @@ describe('Test Authentication', () => {
});

test('Retrieve the authenticated user', async () => {
const signUpQuery = `
mutation SignupUser($input: SignupUserInput!) {
signupUser(input: $input) {
__typename
message
userId
}
}
`;

const signUpVariables = {
input: {
email: '[email protected]',
name: 'John Doe',
password: 'password',
},
};

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

const confirmationToken = generateJwtToken({
expiresIn: '1h',
payload: { userId: signUpResponse.body.data.signupUser.userId },
secret: process.env.JWT_SECRET,
});

const confirmUserQuery = `
mutation ConfirmUser($token: String!) {
confirmUser(token: $token) {
message
}
}
`;

const confirmUserVariables = {
token: confirmationToken,
const input = {
email: '[email protected]',
name: 'John Doe',
password: 'password',
};

await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query: confirmUserQuery, variables: confirmUserVariables })
.expect(200);

const loginQuery = `
mutation LoginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
token
}
}
`;

const loginVariables = {
email: signUpVariables.input.email,
password: signUpVariables.input.password,
};

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

const authToken = loginResponse.body.data.loginUser.token;
const { authToken, user } = await testHelper.createAuthenticatedUser({ ...input });

const authenticatedUserQuery = `
query AuthenticatedUser {
Expand Down Expand Up @@ -306,10 +240,10 @@ describe('Test Authentication', () => {

expect(authenticatedUser).toMatchObject({
createdAt: expect.any(Number),
email: loginVariables.email,
id: signUpResponse.body.data.signupUser.userId,
email: input.email,
id: user.id,
isEnabled: true,
name: signUpVariables.input.name,
name: input.name,
oauthProvider: 'email',
pictureUrl: null,
role: {
Expand All @@ -325,72 +259,12 @@ describe('Test Authentication', () => {
});

test('Log out the authenticated user', async () => {
const signUpQuery = `
mutation SignupUser($input: SignupUserInput!) {
signupUser(input: $input) {
__typename
message
userId
}
}
`;

const signUpVariables = {
input: {
email: '[email protected]',
name: 'Jane Doe',
password: 'password',
},
};

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

const confirmationToken = generateJwtToken({
expiresIn: '1h',
payload: { userId: signUpResponse.body.data.signupUser.userId },
secret: process.env.JWT_SECRET,
const { authToken, user } = await testHelper.createAuthenticatedUser({
email: '[email protected]',
name: 'Jane Doe',
password: 'password',
});

const confirmUserQuery = `
mutation ConfirmUser($token: String!) {
confirmUser(token: $token) {
message
}
}
`;

const confirmUserVariables = {
token: confirmationToken,
};

await request(server.app.getHttpServer())
.post(graphqlEndpoint)
.send({ query: confirmUserQuery, variables: confirmUserVariables })
.expect(200);

const loginQuery = `
mutation LoginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
token
}
}
`;

const loginVariables = {
email: signUpVariables.input.email,
password: signUpVariables.input.password,
};

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

const authToken = loginResponse.body.data.loginUser.token;

const authenticatedUserQuery = `
query AuthenticatedUser {
authenticatedUser {
Expand All @@ -407,7 +281,7 @@ describe('Test Authentication', () => {

const { authenticatedUser } = response.body.data;

expect(authenticatedUser.id).toEqual(signUpResponse.body.data.signupUser.userId);
expect(authenticatedUser.id).toEqual(user.id);

const logoutQuery = `
mutation LogoutUser {
Expand Down
2 changes: 0 additions & 2 deletions apps/backend/src/features/auth/graphql/auth.resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ export class AuthResolvers {
@Mutation('logoutUser')
@UseGuards(AuthGuard)
async logoutUser(@UserId() userId: string | undefined): Promise<boolean> {
console.log('user logged out', userId);

if (!userId) {
return false;
}
Expand Down
15 changes: 5 additions & 10 deletions apps/backend/src/features/auth/rest/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,11 @@ export class AuthController {
const webAuthSuccessUrl = this.configService.get<string>('WEB_AUTH_SUCCESS_URL');
const webAuthErrorUrl = this.configService.get<string>('WEB_AUTH_ERROR_URL');

const authResponse = await this.githubService.requestAccessTokenFromCode(requestToken);
const accessToken = await this.githubService.requestAccessTokenFromCode(requestToken);

const { access_token } = authResponse.data;
const githubUserData = await this.githubService.retrieveGitHubUserData(accessToken);

const userResponse = await this.githubService.retrieveGitHubUserData(access_token);

const userExist = await this.userService.findByEmail(userResponse.data.email);
const userExist = await this.userService.findByEmail(githubUserData.email);

if (userExist) {
const sessionInput = new CreateSessionInput({
Expand All @@ -49,7 +47,7 @@ export class AuthController {
});
const session = await this.sessionService.create(sessionInput);

const updateUserInput = this.githubService.generateUserUpdateInputFromGitHubData(userExist, userResponse.data);
const updateUserInput = this.githubService.generateUserUpdateInputFromGitHubData(userExist, githubUserData);

await this.userService.update(userExist, updateUserInput);

Expand All @@ -64,10 +62,7 @@ export class AuthController {
return res.redirect(webAuthErrorUrl);
}

const createUserInput = this.githubService.generateUserRegistrationInputFromGitHubData(
userResponse.data,
roleUser.id,
);
const createUserInput = this.githubService.generateUserRegistrationInputFromGitHubData(githubUserData, roleUser.id);

const createdUser = await this.userService.create(createUserInput);

Expand Down
Loading