Skip to content
Merged
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
238 changes: 235 additions & 3 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,28 @@ import { PasskeyEntity } from './entities/passkey.entity';
import { Challenge } from './entities/challenge.entity';
import { ConfigService } from '@nestjs/config';

jest.mock('@simplewebauthn/server', () => ({
generateRegistrationOptions: jest.fn(),
verifyRegistrationResponse: jest.fn(),
generateAuthenticationOptions: jest.fn(),
verifyAuthenticationResponse: jest.fn(),
}));

import {
verifyRegistrationResponse,
generateRegistrationOptions,
} from '@simplewebauthn/server';

describe('AuthService', () => {
let service: AuthService;
const existingUser = { username: 'TestUser', password: 'TestPw' };

const mockRepository = {
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(() => ({
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
Expand Down Expand Up @@ -75,7 +90,6 @@ describe('AuthService', () => {

describe('validateUser', () => {
it('should return User Data on correct credentials', async () => {
// Mock the repository to return a user
mockRepository.findOne.mockResolvedValue(existingUser);

// WHEN
Expand All @@ -89,7 +103,6 @@ describe('AuthService', () => {
});

it('should return null on nonexistent username', async () => {
// Mock the repository to return null
mockRepository.findOne.mockResolvedValue(null);

// WHEN
Expand All @@ -103,7 +116,6 @@ describe('AuthService', () => {
});

it('should return null on wrong password', async () => {
// Mock the repository to return a user
mockRepository.findOne.mockResolvedValue(existingUser);

// WHEN
Expand All @@ -116,4 +128,224 @@ describe('AuthService', () => {
await expect(result).resolves.toBeNull();
});
});

describe('createUser', () => {
it('should hash the password and save the user', async () => {
mockRepository.create = jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
.mockImplementation((user) => user);
mockRepository.save.mockResolvedValue({
id: 1,
email: 'test@example.com',
});

// await
const result = await service.createUser(
'test@example.com',
'password123',
);

// then
expect(mockRepository.create).toHaveBeenCalledWith({
email: 'test@example.com',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
password: expect.any(String),
});
expect(mockRepository.save).toHaveBeenCalled();
expect(result).toEqual({ id: 1, email: 'test@example.com' });
});
it('should throw an error if the repository throws an expection', async () => {
mockRepository.save.mockRejectedValue(new Error('Database Error'));

// when
const result = service.createUser(
'test@example.com',
'password123',
);

//then
await expect(result).rejects.toThrow('Database Error');
});
});

describe('changePassword', () => {
it('should hash the new password and update the user', async () => {
mockRepository.update.mockResolvedValue({ affected: 1 });

// when
await service.changePassword(1, 'newPassword123');

// then
expect(mockRepository.update).toHaveBeenCalledWith(1, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
password: expect.any(String),
});
});

it('should throw and error if the repository throws an exception', async () => {
mockRepository.update.mockRejectedValue(
new Error('Database Error'),
);

// when
const result = service.changePassword(1, 'newPassword123');

// then
await expect(result).rejects.toThrow('Database Error');
});
});

describe('generateRegistrationOptions', () => {
it('should generate registration options and save the challenge', async () => {
mockRepository.findOne.mockResolvedValue({
user_id: '1',
email: 'test@example.com',
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
mockRepository.create.mockImplementation((challenge) => ({
...challenge,
id: '1',
}));
mockRepository.save.mockResolvedValue({
id: '1',
});

(generateRegistrationOptions as jest.Mock).mockReturnValue({
challenge: 'testChallenge',
});

// WHEN
const result = await service.generateRegistrationOptions('1');

// THEN
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { user_id: '1' },
});
expect(mockRepository.create).toHaveBeenCalledWith({
user: { user_id: '1', email: 'test@example.com' },
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
challenge: expect.any(String),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expiresAt: expect.any(Date),
});
expect(mockRepository.save).toHaveBeenCalled();
expect(result).toEqual(expect.any(Object));
});

it('should throw an error if the user is not found', async () => {
mockRepository.findOne.mockResolvedValue(null);

// WHEN
const result = service.generateRegistrationOptions('1');

// THEN
await expect(result).rejects.toThrow('User not found');
});

it('should throw an error if the repository throws an exception', async () => {
mockRepository.findOne.mockRejectedValue(
new Error('Database Error'),
);

// WHEN
const result = service.generateRegistrationOptions('1');

// THEN
await expect(result).rejects.toThrow('Database Error');
});
});

describe('verifyRegistrationResponse', () => {
it('should verify the registration response and delete the challenge', async () => {
mockRepository.findOne.mockResolvedValue({
challenge: 'testChallenge',
id: 1,
});
mockRepository.delete.mockResolvedValue({ affected: 1 });

const mockVerification = { verified: true };
(verifyRegistrationResponse as jest.Mock).mockResolvedValue(
mockVerification,
);

// WHEN
const result = await service.verifyRegistrationResponse(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
{ id: 'testId' } as any,
'1',
);

// THEN
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { user: { user_id: '1' } },
});
expect(mockRepository.delete).toHaveBeenCalledWith(1);
expect(result).toEqual(mockVerification);
});
it('should throw an error if the challenge is not found', async () => {
mockRepository.findOne.mockResolvedValue(null);

const result = service.verifyRegistrationResponse(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
{ id: 'testId' } as any,
'1',
);

await expect(result).rejects.toThrow('Challenge not found');
});

it('should throw an error if deleting the challenge fails', async () => {
mockRepository.findOne.mockResolvedValue({
challenge: 'testChallenge',
id: 1,
});

(verifyRegistrationResponse as jest.Mock).mockResolvedValue({
verified: true,
});

mockRepository.delete.mockRejectedValue(
new Error('Database Error'),
);

const result = service.verifyRegistrationResponse(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
{ id: 'testId' } as any,
'1',
);

await expect(result).rejects.toThrow('Database Error');
});
it('should throw an error if required environment variables are missing', async () => {
mockRepository.findOne.mockResolvedValue({
challenge: 'testChallenge',
id: 1,
});

mockConfigService.get.mockImplementation((key: string) => {
const envVariables = {
RP_ORIGIN: 'testOrigin',
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return envVariables[key];
});

const mockVerification = { verified: true };
(verifyRegistrationResponse as jest.Mock).mockResolvedValue(
mockVerification,
);

const result = service.verifyRegistrationResponse(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
{ id: 'testId' } as any,
'1',
);

await expect(result).rejects.toThrow(
'Environment variable RP_ID is not set',
);
});
});
});
Loading