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 1 commit
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
Prev Previous commit
test(backend): integration test for github authentication
  • Loading branch information
tericcabrel committed Jun 9, 2024
commit e6c311021a49f9efcecbf070d6921b7f8adfce4d
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
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 {}
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
109 changes: 109 additions & 0 deletions apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as url from 'node:url';

import { SessionService } from '@snipcode/domain';
import { HttpResponse, http } from 'msw';
import { setupServer } from 'msw/node';
import request from 'supertest';

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

const mockServer = setupServer(
http.post('https://github.com/login/oauth/access_token', ({ request }) => {
const url = new URL(request.url);

const code = url.searchParams.get('code');
const clientId = url.searchParams.get('client_id');
const clientSecret = url.searchParams.get('client_secret');

if (!code || !clientId || !clientSecret) {
return HttpResponse.json({ message: 'Invalid request' }, { status: 400 });
}

if (code === 'valid_code') {
const data = {
access_token: 'valid_token',
};

return HttpResponse.json(data);
}

return HttpResponse.json({ message: 'Invalid token' }, { status: 401 });
}),

http.get('https://api.github.com/user', ({ request }) => {
const authHeader = request.headers.get('Authorization');

if (authHeader === 'token valid_token') {
const user: GitHubUserResponse = {
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
email: '[email protected]',
login: 'octocat',
name: 'monalisa octocat',
};

return HttpResponse.json(user);
}

return HttpResponse.json({ message: 'Invalid token' }, { status: 401 });
}),
);

describe('Test Authentication controller', () => {
let server: TestServer;
let testHelper: TestHelper;
let sessionService: SessionService;

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

sessionService = server.app.get<SessionService>(SessionService);

testHelper = new TestHelper(server.app);

mockServer.listen({
onUnhandledRequest: (req) => {
if (req.url.includes('127.0.0.1') || req.url.includes('localhost')) {
return;
}
console.error(`No request mock for [${req.method}] ${req.url}`);
},
});
});

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

afterEach(() => mockServer.resetHandlers());

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

test('Authenticate with GitHub', async () => {
const response = await request(server.app.getHttpServer())
.get('/auth/github/callback?code=valid_code')
.send({})
.expect(302);

const parsedUrl = url.parse(response.headers.location, true);

expect(`${parsedUrl.protocol}//${parsedUrl.host}${parsedUrl.pathname}`).toBe('http:https://localhost:7500/auth/success');
expect(parsedUrl.query.token).toBeDefined();

const sessionToken = parsedUrl.query.token as string;
const session = await sessionService.findByToken(sessionToken);

expect(session).toBeDefined();

const user = await testHelper.getAuthenticatedUser(sessionToken);

expect(user).toMatchObject({
id: expect.any(String),
rootFolderId: expect.any(String),
});
});
});
159 changes: 159 additions & 0 deletions apps/backend/src/features/auth/services/github.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { User } from '@snipcode/domain';
import { HttpResponse, http } from 'msw';
import { setupServer } from 'msw/node';

import { GithubService } from './github.service';
import { GitHubUserResponse } from '../types';

const server = setupServer(
http.get('https://api.github.com/user', ({ request }) => {
const authHeader = request.headers.get('Authorization');

if (authHeader === 'token valid_token') {
const user: GitHubUserResponse = {
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
email: '[email protected]',
login: 'octocat',
name: 'monalisa octocat',
};

return HttpResponse.json(user);
}

return HttpResponse.json({ message: 'Invalid token' }, { status: 401 });
}),

http.post('https://github.com/login/oauth/access_token', ({ request }) => {
const url = new URL(request.url);

const code = url.searchParams.get('code');
const clientId = url.searchParams.get('client_id');
const clientSecret = url.searchParams.get('client_secret');

if (!code || !clientId || !clientSecret) {
return HttpResponse.json({ message: 'Invalid request' }, { status: 400 });
}

if (code === 'valid_code') {
const data = {
access_token: 'valid_token',
};

return HttpResponse.json(data);
}

return HttpResponse.json({ message: 'Invalid token' }, { status: 401 });
}),
);

describe('Test GithubService', () => {
let githubService: GithubService;

beforeAll(() =>
server.listen({
onUnhandledRequest: (req) => console.error(`No request mock for [${req.method}] ${req.url}`),
}),
);

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
providers: [ConfigService, GithubService],
}).compile();

githubService = app.get<GithubService>(GithubService);
});

afterEach(() => server.resetHandlers());

afterAll(() => server.close());

it('should return user data when access token is valid', async () => {
const result = await githubService.retrieveGitHubUserData('valid_token');

expect(result).toEqual({
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
email: '[email protected]',
login: 'octocat',
name: 'monalisa octocat',
});
});

it('should throw an error when access token is invalid', async () => {
await expect(githubService.retrieveGitHubUserData('invalid_token')).rejects.toThrow();
});

it('should return access token when code is valid', async () => {
const result = await githubService.requestAccessTokenFromCode('valid_code');

expect(result).toEqual('valid_token');
});

it('should throw an error when code is invalid', async () => {
await expect(githubService.requestAccessTokenFromCode('invalid_code')).rejects.toThrow();
});

it('should generate user registration input from GitHub data', () => {
const data: GitHubUserResponse = {
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
email: '[email protected]',
login: 'octocat',
name: 'monalisa octocat',
};

const result = githubService.generateUserRegistrationInputFromGitHubData(data, 'role_id');

expect(result).toMatchObject({
_input: {
email: '[email protected]',
name: 'monalisa octocat',
oauthProvider: 'github',
pictureUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
roleId: 'role_id',
timezone: null,
username: 'octocat',
},
enabled: true,
hashedPassword: null,
userId: expect.any(String),
});
});

it('should generate user update input from GitHub data', () => {
const data: GitHubUserResponse = {
avatar_url: 'https://avatars.githubusercontent.com/u/2?v=4',
email: '[email protected]',
login: 'octocat',
name: 'monalisa octocat 2',
};

const user: User = {
createdAt: new Date(),
email: '[email protected]',
id: 'userId',
isEnabled: true,
name: 'octocat',
oauthProvider: 'github',
password: 'password',
pictureUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
roleId: 'roleId',
timezone: null,
updatedAt: new Date(),
username: 'octocat',
};

const result = githubService.generateUserUpdateInputFromGitHubData(user, data);

expect(result).toMatchObject({
_input: {
name: 'monalisa octocat 2',
oauthProvider: 'github',
pictureUrl: 'https://avatars.githubusercontent.com/u/2?v=4',
roleId: 'roleId',
timezone: null,
},
enabled: true,
});
});
});
Loading