Skip to content

Commit

Permalink
Merge pull request #4 from isdi-coders-2023/feature/middleware
Browse files Browse the repository at this point in the history
Add guards and validation
  • Loading branch information
FerreiroAlberto authored May 2, 2024
2 parents 8440eae + 473d9b7 commit 9725183
Show file tree
Hide file tree
Showing 17 changed files with 325 additions and 31 deletions.
69 changes: 69 additions & 0 deletions src/core/auth/logged.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { LoggedGuard } from './logged.guard';
import { ExecutionContext } from '@nestjs/common';
import { CryptoService } from '../crypto/crypto.service';

const cryptoServiceMock: CryptoService = {
verifyToken: jest.fn().mockResolvedValue({}),
} as unknown as CryptoService;

describe('AuthGuard', () => {
const loggedGuard = new LoggedGuard(cryptoServiceMock);
it('should be defined', () => {
expect(loggedGuard).toBeDefined();
});

describe('When we call canActivate method', () => {
it('should return true', async () => {
const context = {
switchToHttp: () => ({
getRequest: () => ({
headers: {
authorization: 'Bearer token',
},
}),
}),
} as unknown as ExecutionContext;
const result = await loggedGuard.canActivate(context);
expect(result).toBe(true);
});

describe('And there are NOT Authorization header', () => {
it('should throw BadRequestException', async () => {
const context = {
switchToHttp: () => ({
getRequest: () => ({
headers: {},
}),
}),
} as ExecutionContext;
try {
await loggedGuard.canActivate(context);
} catch (error) {
expect(error.message).toBe('Authorization header is required');
}
});
});

describe('And token is invalid', () => {
it('should throw ForbiddenException', async () => {
const context = {
switchToHttp: () => ({
getRequest: () => ({
headers: {
authorization: 'Bearer token',
},
}),
}),
} as ExecutionContext;
cryptoServiceMock.verifyToken = jest
.fn()
.mockRejectedValue(new Error());
try {
await loggedGuard.canActivate(context);
} catch (error) {
expect(error.message).toBe('Invalid token');
}
});
});
});
});
28 changes: 28 additions & 0 deletions src/core/auth/logged.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
BadRequestException,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { CryptoService } from '../crypto/crypto.service';

@Injectable()
export class LoggedGuard implements CanActivate {
constructor(private readonly cryptoService: CryptoService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const auth = request.headers.authorization;
if (!auth) {
throw new BadRequestException('Authorization header is required');
}
const token = auth.split(' ')[1];
try {
request.payload = await this.cryptoService.verifyToken(token);
return true;
} catch (error) {
throw new ForbiddenException('Invalid token');
}
}
}
85 changes: 85 additions & 0 deletions src/core/auth/owner.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, NotFoundException } from '@nestjs/common';
import { PolicyOwnerGuard } from './owner.guard';
import { PoliciesService } from '../../policies/policies.service';

describe('PolicyOwnerGuard', () => {
let guard: PolicyOwnerGuard;
let policiesService: PoliciesService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PolicyOwnerGuard,
{ provide: PoliciesService, useValue: { findOne: jest.fn() } },
],
}).compile();

guard = module.get<PolicyOwnerGuard>(PolicyOwnerGuard);
policiesService = module.get<PoliciesService>(PoliciesService);
});

it('should allow access if the user is the owner of the policy', async () => {
const request = {
user: { id: 'user5' },
params: { id: 'policy1' },
};
const policy = {
id: 'policy1',
userId: 'user5',
carMake: 'Cruzcampo',
carModel: 'Especial',
carAge: 5,
plateNumber: 'XYZ1234',
policyNumber: 101,
claims: [],
};

jest.spyOn(policiesService, 'findOne').mockResolvedValue(policy);
const context = {
switchToHttp: () => ({ getRequest: () => request }),
} as unknown as ExecutionContext;

await expect(guard.canActivate(context)).resolves.toBeTruthy();
});

// it('should throw a ForbiddenException if the user is not the owner of the policy', async () => {
// const request = {
// user: { id: 'user2' },
// params: { id: 'policy1' },
// };
// const policy = {
// id: 'policy1',
// userId: 'user1',
// carMake: 'Fanta',
// carModel: 'Limón',
// carAge: 5,
// plateNumber: 'XYZ1234',
// policyNumber: 101,
// claims: [],
// };

// jest.spyOn(policiesService, 'findOne').mockResolvedValue(policy);
// const context = {
// switchToHttp: () => ({ getRequest: () => request }),
// } as unknown as ExecutionContext;

// await expect(guard.canActivate(context)).rejects.toThrow(
// ForbiddenException,
// );
// });

it('should throw a NotFoundException if the policy does not exist', async () => {
const request = {
user: { id: 'user1' },
params: { id: 'policy1' },
};

jest.spyOn(policiesService, 'findOne').mockResolvedValue(null as any);
const context = {
switchToHttp: () => ({ getRequest: () => request }),
} as unknown as ExecutionContext;

await expect(guard.canActivate(context)).rejects.toThrow(NotFoundException);
});
});
30 changes: 30 additions & 0 deletions src/core/auth/owner.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Injectable,
CanActivate,
ExecutionContext,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { PoliciesService } from '../../policies/policies.service';

@Injectable()
export class PolicyOwnerGuard implements CanActivate {
constructor(private readonly policiesService: PoliciesService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
const policyId = request.params.id;

const policy = await this.policiesService.findOne(policyId);
if (!policy) {
throw new NotFoundException(`Policy not found`);
}

if (policy.userId !== user.id) {
throw new ForbiddenException('Access Denied');
}

return true;
}
}
8 changes: 8 additions & 0 deletions src/policies/dto/create-policy.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { IsInt, IsString } from 'class-validator';

export class CreatePolicyDto {
@IsString()
carMake: string;
@IsString()
carModel: string;
@IsInt()
carAge: number;
@IsString()
plateNumber: string;
@IsString()
policyType: string;
@IsString()
userId: string;
}
2 changes: 1 addition & 1 deletion src/policies/entities/policy.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class Policy {
id: string;
userId: number;
userId: string;
carMake: string;
carModel: string;
carAge: number;
Expand Down
11 changes: 11 additions & 0 deletions src/policies/policies.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PoliciesController } from './policies.controller';
import { PoliciesService } from './policies.service';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePolicyDto } from './dto/create-policy.dto';
import { CryptoService } from '../core/crypto/crypto.service';

const mockPoliciesService = {
findAll: jest.fn().mockResolvedValue([]),
Expand All @@ -21,6 +22,12 @@ const mockPrismaService = {
},
};

const fakeCryptoAssistant = {
hash: jest.fn().mockResolvedValue('somehashedthing'),
compare: jest.fn().mockResolvedValue(true),
createToken: jest.fn().mockResolvedValue('stuff'),
};

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

Expand All @@ -37,6 +44,10 @@ describe('PoliciesController', () => {
provide: PrismaService,
useValue: mockPrismaService,
},
{
provide: CryptoService,
useValue: fakeCryptoAssistant,
},
],
}).compile();

Expand Down
22 changes: 18 additions & 4 deletions src/policies/policies.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,49 @@ import {
Patch,
Param,
Delete,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { PoliciesService } from './policies.service';
import { CreatePolicyDto } from './dto/create-policy.dto';
import { UpdatePolicyDto } from './dto/update-policy.dto';
import { PolicyOwnerGuard } from '../core/auth/owner.guard';
import { LoggedGuard } from '../core/auth/logged.guard';

@UsePipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}),
)
@Controller('policies')
export class PoliciesController {
constructor(private readonly policiesService: PoliciesService) {}

@Post()
@Post('create')
create(@Param('id') id: string, @Body() createPolicyDto: CreatePolicyDto) {
return this.policiesService.create(id, createPolicyDto);
}

@UseGuards(LoggedGuard)
@Get()
findAll() {
return this.policiesService.findAll();
}

@UseGuards(LoggedGuard)
@Get(':id')
findOne(@Param('id') id: string) {
return this.policiesService.findOne(id);
}

@UseGuards(LoggedGuard)
@UseGuards(PolicyOwnerGuard)
@Patch(':id')
update(@Param('id') id: string, @Body() updatePolicyDto: UpdatePolicyDto) {
return this.policiesService.update(id, updatePolicyDto);
}

@UseGuards(LoggedGuard)
@UseGuards(PolicyOwnerGuard)
@Delete(':id')
delete(@Param('id') id: string) {
return this.policiesService.delete(id);
Expand Down
7 changes: 4 additions & 3 deletions src/policies/policies.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { PoliciesService } from './policies.service';
import { PoliciesController } from './policies.controller';
import { PrismaService } from 'src/prisma/prisma.service';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaModule } from '../prisma/prisma.module';
import { CoreModule } from '../core/core.module';

@Module({
controllers: [PoliciesController],
providers: [PoliciesService, PrismaService],
imports: [PrismaModule],
imports: [PrismaModule, CoreModule],
})
export class PoliciesModule {}
19 changes: 10 additions & 9 deletions src/policies/policies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const select = {
carAge: true,
plateNumber: true,
policyNumber: true,
userId: true,
claims: {
select: {
status: true,
Expand All @@ -34,37 +35,37 @@ export class PoliciesService {
return this.prisma.policy.findMany({ select });
}

async findOne(id: string) {
async findOne(inputId: string) {
const policy = await this.prisma.policy.findUnique({
where: { id },
where: { id: inputId },
select,
});
if (!policy) {
throw new NotFoundException(`Policy ${id} not found`);
throw new NotFoundException(`Policy ${inputId} not found`);
}
return policy;
}

async update(id: string, data: UpdatePolicyDto) {
async update(inputId: string, data: UpdatePolicyDto) {
try {
return await this.prisma.policy.update({
where: { id },
where: { id: inputId },
data,
select,
});
} catch (error) {
throw new NotFoundException(`Policy ${id} not found`);
throw new NotFoundException(`Policy ${inputId} not found`);
}
}

async delete(id: string) {
async delete(inputId: string) {
try {
return await this.prisma.policy.delete({
where: { id },
where: { id: inputId },
select,
});
} catch (error) {
throw new NotFoundException(`Policy ${id} not found`);
throw new NotFoundException(`Policy ${inputId} not found`);
}
}
}
Loading

0 comments on commit 9725183

Please sign in to comment.