Skip to content

Commit

Permalink
Add tests for Auth module (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
Odraxs authored Nov 29, 2023
1 parent cea7f37 commit 5eb2de6
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 31 deletions.
53 changes: 49 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nestjs/testing": "^10.2.10",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.96",
"@types/express": "^4.17.17",
Expand All @@ -54,6 +54,8 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"jest-extended": "^4.0.2",
"jest-mock-extended": "^3.0.5",
"prettier": "^3.0.0",
"prisma": "^5.6.0",
"source-map-support": "^0.5.21",
Expand Down
16 changes: 9 additions & 7 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from 'src/common/services/prisma.service';
import { PrismaService } from '../common/services/prisma.service';
import { SignupDto } from './dtos/signup.dto';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
Expand Down Expand Up @@ -59,16 +59,18 @@ export class AuthService {
name: true,
},
});
if (
user === null ||
!bcrypt.compareSync(loginDto.password, user.passwordHash)
)
throw new UnauthorizedException();

const validatePassword = await bcrypt.compareSync(
loginDto.password,
user!.passwordHash,
);

if (user === null || !validatePassword) throw new UnauthorizedException();

const payload: JwtPayload = {
id: user.id,
email: user.email,
name: user.name,
name: user.name!,
};
return this.jwtService.signAsync(payload);
}
Expand Down
2 changes: 1 addition & 1 deletion src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService, JwtPayload } from '../auth.service';
import { AuthUser } from '../auth-user';
import config from 'src/common/config';
import config from '../../common/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
Expand Down
147 changes: 147 additions & 0 deletions src/auth/test/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { DeepMockProxy, mockDeep } from 'jest-mock-extended';
import { UserService } from './../../user/user.service';
import { AuthService } from '../auth.service';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { PrismaService } from '../../common/services/prisma.service';
import { Test, TestingModule } from '@nestjs/testing';
import config from '../../common/config';
import * as bcrypt from 'bcrypt';
import { Prisma } from '@prisma/client';
import { ConflictException, UnauthorizedException } from '@nestjs/common';

describe('AuthService', () => {
let service: AuthService;

//mocks
let spyUserService: UserService;
let spyJwtService: JwtService;
let spyPrismaService: DeepMockProxy<PrismaService>;

const date = new Date();
const user = {
id: 'fcd2fa2d-f5f4-4ed0-9d75-f3ca6ddd4c21',
name: 'John',
email: '[email protected]',
passwordHash: 'hashedPassword',
createdAt: date,
updatedAt: date,
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
JwtModule.register({
secret: config.jwt.secret,
signOptions: {
expiresIn: config.jwt.expiresIn,
},
}),
],
providers: [
AuthService,
UserService,
{
provide: PrismaService,
useFactory: () => mockDeep<PrismaService>(),
},
],
}).compile();

service = module.get<AuthService>(AuthService);
spyUserService = module.get<UserService>(UserService);
spyJwtService = module.get<JwtService>(JwtService);
spyPrismaService = module.get(
PrismaService,
) as DeepMockProxy<PrismaService>;
});

it('should be defined', () => {
expect(service).toBeDefined();
expect(spyUserService).toBeDefined();
expect(spyJwtService).toBeDefined();
expect(spyPrismaService).toBeDefined();
});

describe('signup', () => {
const signUpData = {
name: 'John',
email: '[email protected]',
passwordHash: 'securePassword',
};

it('should create a new user', async () => {
jest.spyOn(bcrypt, 'hash').mockResolvedValue(user.passwordHash);
spyPrismaService.user.create.mockResolvedValue(user);

await service.signup(signUpData);
expect(spyPrismaService.user.create).toHaveBeenCalledTimes(1);
expect(spyPrismaService.user.create).toHaveBeenCalledWith({
data: {
name: signUpData.name,
email: signUpData.email,
passwordHash: user.passwordHash,
},
select: null,
});
});

it('should return an error for a duplicated email', async () => {
spyPrismaService.user.create.mockRejectedValueOnce(
new Prisma.PrismaClientKnownRequestError('Some error', {
code: 'P2002',
clientVersion: '4.12.0',
}),
);

await expect(service.signup(signUpData)).rejects.toThrow(
ConflictException,
);
});
});

describe('login', () => {
const loginInfo = {
email: '[email protected]',
password: 'hashedPassword',
};
const testJWT =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImMwYzE0Mzc2LTlmYWYtNGQ5Yi04MThiLTEzMjQwNWE0NTYyNyIsImVtYWlsIjoiamhvbkBkb2UuY29tIiwibmFtZSI6Ikpob24iLCJpYXQiOjE3MDExMjExNDQsImV4cCI6MTcwMTIwNzU0NH0.yXpbpk1NLglAMI3ypGWollhsBmWfxuL2K1KyZCt_zDY';

it('should allow an user to login', async () => {
jest.spyOn(bcrypt, 'compareSync').mockResolvedValue(true);
jest.spyOn(spyJwtService, 'signAsync').mockResolvedValue(testJWT);
spyPrismaService.user.findFirst.mockResolvedValue(user);

expect(await service.login(loginInfo)).toStrictEqual(testJWT);
expect(spyPrismaService.user.findFirst).toHaveBeenCalledTimes(1);
});

it('should retrieve an unauthorized error', async () => {
spyPrismaService.user.findFirst.mockResolvedValue(user);
jest.spyOn(bcrypt, 'compareSync').mockResolvedValue(false);

await expect(service.login(loginInfo)).rejects.toThrow(
UnauthorizedException,
);
});
});

describe('validateUser', () => {
const payload = {
id: user.id,
name: user.name,
email: user.email,
};
it('should return a valid user', async () => {
spyPrismaService.user.findUnique.mockResolvedValue(user);
expect(await service.validateUser(payload)).toStrictEqual(user);
});

it('should throw an unauthorized error', async () => {
spyPrismaService.user.findUnique.mockResolvedValue(null);
await expect(service.validateUser(payload)).rejects.toThrow(
UnauthorizedException,
);
});
});
});
2 changes: 1 addition & 1 deletion src/program/program.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/common/services/prisma.service';
import { PrismaService } from '../common/services/prisma.service';
import { ProgramDto } from './dtos/program.dto';
import { ProgramExecFactory } from './programExec/ProgramExecFactory';
import { IProgramExec } from './programExec/IProgramExec';
Expand Down
12 changes: 3 additions & 9 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import {
Param,
UseGuards,
Body,
NotFoundException,
} from '@nestjs/common';
import { UserService } from './user.service';
import { AuthGuard } from '@nestjs/passport';
import { UserResponseDto } from './dtos/userResponse.dto';
import { ProgramDto } from '../program/dtos/program.dto';
import { ProgramService } from '../program/program.service';
import { ProgramExecResultDto } from 'src/program/dtos/programExecResult.dto';
import { ProgramExecResultDto } from '../program/dtos/programExecResult.dto';

@Controller('user')
export class UserController {
Expand All @@ -28,7 +27,7 @@ export class UserController {
@UseGuards(AuthGuard())
async getUser(@Param('id') id: string): Promise<UserResponseDto> {
const { email, name } = await this.userService.getUserById(id);
return new UserResponseDto(email, name);
return new UserResponseDto(email, name!);
}

@Post('program')
Expand All @@ -37,12 +36,7 @@ export class UserController {
async postProgram(
@Body() programDto: ProgramDto,
): Promise<ProgramExecResultDto> {
const user = await this.userService.getUserById(programDto.userId);
if (!user) {
throw new NotFoundException(
`User with id: ${programDto.userId} not found`,
);
}
await this.userService.getUserById(programDto.userId);
return await this.programService.storeExecuteProgram(programDto);
}
}
4 changes: 2 additions & 2 deletions src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { UserService } from './user.service';
import { UserController } from './user.controller';
import { PrismaService } from '../common/services/prisma.service';
import { PassportModule } from '@nestjs/passport';
import { ProgramService } from 'src/program/program.service';
import { ProgramExecFactory } from 'src/program/programExec/ProgramExecFactory';
import { ProgramService } from '../program/program.service';
import { ProgramExecFactory } from '../program/programExec/ProgramExecFactory';

@Module({
imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
Expand Down
15 changes: 10 additions & 5 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { Injectable } from '@nestjs/common';
import { AuthUser } from 'src/auth/auth-user';
import { PrismaService } from 'src/common/services/prisma.service';
import { Injectable, NotFoundException } from '@nestjs/common';
import { AuthUser } from '../auth/auth-user';
import { PrismaService } from '../common/services/prisma.service';

@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}

public async getUserById(id: string): Promise<AuthUser | null> {
return this.prisma.user.findUnique({
public async getUserById(id: string): Promise<AuthUser> {
const user = await this.prisma.user.findUnique({
where: { id },
});

if (!user) {
throw new NotFoundException(`User with id: ${id} not found`);
}
return user;
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"strictNullChecks": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
Expand Down

0 comments on commit 5eb2de6

Please sign in to comment.