Skip to content

Commit

Permalink
test(backend): integration test for github authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
tericcabrel committed Jun 9, 2024
1 parent a53c069 commit 7a26b7e
Show file tree
Hide file tree
Showing 11 changed files with 571 additions and 26 deletions.
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

0 comments on commit 7a26b7e

Please sign in to comment.