Skip to content

set up tests in server and added auth unit tests #203

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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions server/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/$1',
},
};

export default config;
13 changes: 11 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
"start:dev": "nest start --watch",
"start:docker": "docker-compose up --build",
"start:server": "nest start --watch",
"start:client": "cd client && npm run start"
"start:client": "cd client && npm run start",
"test": "jest",
"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"
},
"dependencies": {
"@google-cloud/local-auth": "^3.0.1",
Expand Down Expand Up @@ -58,7 +63,11 @@
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
"typescript": "^5.1.3",
"@nestjs/testing": "^10.0.0",
"@types/jest": "^29.5.0",
"jest": "^29.5.0",
"ts-jest": "^29.1.0"
},
"engines": {
"node": ">=18.0.0"
Expand Down
149 changes: 149 additions & 0 deletions server/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { EncryptionService } from './encryption.service';
import { Logger } from '@nestjs/common';
import { Response } from 'express';

describe('AuthController', () => {
let controller: AuthController;

const mockAuthService = {
login: jest.fn(),
refreshAppToken: jest.fn(),
logout: jest.fn(),
getCookieOptions: jest.fn(),
};

const mockEncryptionService = {
decrypt: jest.fn(),
encrypt: jest.fn(),
};

const mockGoogleApiService = {
getOAuthUrl: jest.fn(),
getOAuthClient: jest.fn(),
};

const mockResponse = {
cookie: jest.fn(),
clearCookie: jest.fn(),
} as unknown as Response;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
Logger,
{
provide: AuthService,
useValue: mockAuthService,
},
{
provide: EncryptionService,
useValue: mockEncryptionService,
},
{
provide: 'GoogleApiService',
useValue: mockGoogleApiService,
},
],
}).compile();

controller = module.get<AuthController>(AuthController);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('oAuthCallback', () => {
const mockCode = 'test-code';
const mockLoginResponse = {
accessToken: 'encrypted-access-token',
accessTokenIv: 'access-iv',
refreshToken: 'encrypted-refresh-token',
refreshTokenIv: 'refresh-iv',
hd: 'test.com',
email: '[email protected]',
};

it('should handle OAuth callback successfully', async () => {
mockAuthService.login.mockResolvedValue(mockLoginResponse);
mockAuthService.getCookieOptions.mockReturnValue({ httpOnly: true });

const result = await controller.oAuthCallback(mockCode, mockResponse);

expect(result.data).toBe(true);
expect(mockResponse.cookie).toHaveBeenCalledTimes(6);
expect(mockAuthService.login).toHaveBeenCalledWith(mockCode);
});
});

describe('logout', () => {
it('should clear cookies and revoke token when requested', async () => {
const mockRequest = {
cookies: {
accessToken: 'encrypted-token',
accessTokenIv: 'iv',
},
};

mockEncryptionService.decrypt.mockResolvedValue('decrypted-token');
mockGoogleApiService.getOAuthClient.mockReturnValue({ setCredentials: jest.fn() });
mockAuthService.logout.mockResolvedValue(true);

await controller.logout(mockRequest as any, mockResponse, true);

expect(mockResponse.clearCookie).toHaveBeenCalledTimes(6); // Updated to match actual number of cookies being cleared
expect(mockAuthService.logout).toHaveBeenCalled();
});

it('should only clear session cookies when not revoking token', async () => {
await controller.logout({ cookies: {} } as any, mockResponse, false);

expect(mockResponse.clearCookie).toHaveBeenCalledTimes(4);
expect(mockAuthService.logout).not.toHaveBeenCalled();
});
});

describe('getOAuthUrl', () => {
it('should return OAuth URL for web client', () => {
const mockUrl = 'https://oauth-url';
mockGoogleApiService.getOAuthUrl.mockReturnValue(mockUrl);

const result = controller.getOAuthUrl('web');

expect(result.data).toBe(mockUrl);
expect(mockGoogleApiService.getOAuthUrl).toHaveBeenCalledWith('web');
});
});

describe('refreshAppToken', () => {
it('should refresh access token successfully', async () => {
const mockRequest = {
cookies: {
refreshToken: 'encrypted-refresh',
refreshTokenIv: 'iv',
},
};

mockEncryptionService.decrypt.mockResolvedValue('decrypted-token');
mockAuthService.refreshAppToken.mockResolvedValue('new-token');
mockEncryptionService.encrypt.mockResolvedValue({
encryptedData: 'new-encrypted-token',
iv: 'new-iv',
});
mockAuthService.getCookieOptions.mockReturnValue({ httpOnly: true });

const result = await controller.refreshAppToken(mockRequest as any, mockResponse);

expect(result.data).toBe(true);
expect(mockResponse.cookie).toHaveBeenCalledTimes(2);
});
});
});
117 changes: 117 additions & 0 deletions server/src/auth/auth.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { EncryptionService } from './encryption.service';
import { Test, TestingModule } from '@nestjs/testing';

describe('AuthGuard', () => {
let guard: AuthGuard;

const mockEncryptionService = {
decrypt: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthGuard,
{
provide: EncryptionService,
useValue: mockEncryptionService,
},
],
}).compile();

guard = module.get<AuthGuard>(AuthGuard);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(guard).toBeDefined();
});

describe('canActivate', () => {
let mockExecutionContext: ExecutionContext;
let mockHttpContext;

beforeEach(() => {
mockHttpContext = {
getRequest: jest.fn(),
getResponse: jest.fn(),
getNext: jest.fn(),
};

mockExecutionContext = {
switchToHttp: () => mockHttpContext,
getClass: jest.fn(),
getHandler: jest.fn(),
getArgs: jest.fn(),
getArgByIndex: jest.fn(),
getType: jest.fn(),
switchToRpc: jest.fn(),
switchToWs: jest.fn(),
};
});

it('should throw UnauthorizedException when no access token found', async () => {
const mockRequest = {
cookies: {},
};
mockHttpContext.getRequest.mockReturnValue(mockRequest);

await expect(guard.canActivate(mockExecutionContext)).rejects.toThrow(
new UnauthorizedException('No access token found')
);
});

it('should throw UnauthorizedException when no access token IV found', async () => {
const mockRequest = {
cookies: {
accessToken: 'token',
},
};
mockHttpContext.getRequest.mockReturnValue(mockRequest);

await expect(guard.canActivate(mockExecutionContext)).rejects.toThrow(
new UnauthorizedException('No access token found')
);
});

it('should throw UnauthorizedException when decryption fails', async () => {
const mockRequest = {
cookies: {
accessToken: 'encrypted-token',
accessTokenIv: 'iv',
},
};
mockHttpContext.getRequest.mockReturnValue(mockRequest);
mockEncryptionService.decrypt.mockRejectedValue(new Error('Decryption failed'));

await expect(guard.canActivate(mockExecutionContext)).rejects.toThrow(
new UnauthorizedException('Invalid access token')
);
});

it('should allow request when valid tokens are present', async () => {
const mockRequest = {
cookies: {
accessToken: 'encrypted-token',
accessTokenIv: 'iv',
hd: 'domain.com',
email: '[email protected]',
},
};
mockHttpContext.getRequest.mockReturnValue(mockRequest);
mockEncryptionService.decrypt.mockResolvedValue('decrypted-token');

const result = await guard.canActivate(mockExecutionContext);

expect(result).toBe(true);
expect(mockRequest['accessToken']).toBe('decrypted-token');
expect(mockRequest['hd']).toBe('domain.com');
expect(mockRequest['email']).toBe('[email protected]');
});
});
});
Loading