Skip to content
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

test(api): pet controller full coverage #3

Merged
merged 1 commit into from
Aug 5, 2024
Merged
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,html}\"",
"format:check": "prettier --list-different \"src/**/*.{js,jsx,ts,tsx,css,scss,html}\"",
"kill:port": "npx kill-port 8000",
"lint": "eslint **/*.ts",
"lint": "eslint --fix --ext .ts,.tsx,.js,.jsx .",
"start": "NODE_ENV=production node dist/index.js",
"test": "jest",
"test:ci": "jest --ci --reporters=default --reporters=jest-junit",
"test": "jest --runInBand",
"test:ci": "jest --ci --reporters=default --reporters=jest-junit --runInBand",
"tsc": "tsc --noEmit"
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion prisma/schema/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
previewFeatures = ["prismaSchemaFolder", "metrics"]

}

datasource db {
Expand Down
12 changes: 11 additions & 1 deletion src/controllers/__mocks__/user.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { Role, User } from '@prisma/client';

export const user: Omit<User, 'id' | 'createdAt' | 'updatedAt'> = {
type UserWithoutPrismaKeys = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;

export const user: UserWithoutPrismaKeys = {
email: 'testuser@test.com',
firstName: 'test',
lastName: 'user',
password: 'password',
role: Role.USER,
};

export const user2: UserWithoutPrismaKeys = {
email: 'marblestest@test.com',
firstName: 'marbles',
lastName: 'cat',
password: 'password12345',
role: Role.USER,
};
7 changes: 5 additions & 2 deletions src/controllers/__tests__/authController.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import supertest from 'supertest';
import { v4 } from 'uuid';
import { db } from '../../db/prisma';
import { LoginRequest, RegisterRequest } from '../../requests/auth';
import server from '../../server';

Expand Down Expand Up @@ -34,7 +35,10 @@ describe('auth', () => {
lastName: 'test',
password: 'password',
};
await supertest(app).post('/api/auth/register').send(user);

await db.user.create({
data: user,
});

const { body, statusCode } = await supertest(app)
.post('/api/auth/register')
Expand Down Expand Up @@ -171,7 +175,6 @@ describe('auth', () => {
password: 'password',
};
await supertest(app).post('/api/auth/register').send(user);

await supertest(app).post('/api/auth/login').send({
email: user.email,
password: user.password,
Expand Down
225 changes: 221 additions & 4 deletions src/controllers/__tests__/petController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import supertest from 'supertest';
import { db } from '../../db/prisma';
import server from '../../server';
import { pets } from '../__mocks__/pet';
import { user } from '../__mocks__/user';
import { user, user2 } from '../__mocks__/user';

describe('pet', () => {
const app = server.init();
Expand Down Expand Up @@ -68,7 +68,224 @@ describe('pet', () => {
expect(statusCode).toBe(200);
});

// describe('createPet', () => {
// test('', () => {});
// });
describe('createPet', () => {
test('unauthenticated user cannot create pet', async () => {
const userCookie = '';

const { statusCode, body } = await supertest(app)
.post('/api/pets')
.set('Cookie', userCookie)
.send(pets[0]);

expect(statusCode).toEqual(401);

expect(body).toEqual({
code: 'Forbidden',
errors: [],
message: 'You are not authorized to perform this action',
statusCode: 401,
title: 'Forbidden',
type: 'Forbidden',
});
});

test('authenticated user can create pet', async () => {
await supertest(app).post('/api/auth/register').send(user);

const { headers, statusCode } = await supertest(app)
.post('/api/auth/login')
.send({
email: user.email,
password: user.password,
});

expect(statusCode).toBe(200);

const cookieValue = headers['set-cookie'][0].split(';')[0].split('=')[1];
const cookieName = 'connect.sid';

const { body, statusCode: petStatusCode } = await supertest(app)
.post('/api/pets')
.set('Cookie', `${cookieName}=${cookieValue}`)
.send(pets[0]);

expect(petStatusCode).toBe(201);
expect(body).toEqual({
...pets[0],
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
creatorId: expect.any(String),
});
});
});

describe('updatePet', () => {
test('authenticated user can update their pet', async () => {
await supertest(app).post('/api/auth/register').send(user);

const { headers, statusCode } = await supertest(app)
.post('/api/auth/login')
.send({
email: user.email,
password: user.password,
});

expect(statusCode).toBe(200);

const cookieValue = headers['set-cookie'][0].split(';')[0].split('=')[1];
const cookieName = 'connect.sid';

const { body: createdPet } = await supertest(app)
.post('/api/pets')
.set('Cookie', `${cookieName}=${cookieValue}`)
.send(pets[0]);

const { statusCode: updatedPetStatusCode, body: updatedPetBody } =
await supertest(app)
.put(`/api/pets/${createdPet.id}`)
.set('Cookie', `${cookieName}=${cookieValue}`)
.send({
...pets[0],
name: 'Marbles',
});

expect(updatedPetStatusCode).toBe(200);
expect(updatedPetBody).toEqual({
...pets[0],
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
creatorId: createdPet.creatorId,
name: 'Marbles',
});
});

test("authenticated user cannot update another user's pet", async () => {
// user 1
await supertest(app).post('/api/auth/register').send(user);

const { headers, statusCode } = await supertest(app)
.post('/api/auth/login')
.send({
email: user.email,
password: user.password,
});

expect(statusCode).toBe(200);

const cookieValue = headers['set-cookie'][0].split(';')[0].split('=')[1];
const cookieName = 'connect.sid';

// create a pet authenticated as user1
const { body: createdPet } = await supertest(app)
.post('/api/pets')
.set('Cookie', `${cookieName}=${cookieValue}`)
.send(pets[0]);

// user 2
await supertest(app).post('/api/auth/register').send(user2);

const { headers: headers2, statusCode: statusCode2 } = await supertest(
app,
)
.post('/api/auth/login')
.send({
email: user2.email,
password: user2.password,
});
expect(statusCode2).toBe(200);

const cookieValue2 = headers2['set-cookie'][0]
.split(';')[0]
.split('=')[1];

const { statusCode: updatedStatusCode, body: updatedBody } =
await supertest(app)
.put(`/api/pets/${createdPet.id}`)
.set('Cookie', `${cookieName}=${cookieValue2}`)
.send({
...pets[0],
name: `Updated by ${user2.firstName}`,
});

expect(updatedStatusCode).toBe(401);
expect(updatedBody).toEqual({
code: 'forbidden',
errors: [],
message: 'You are not authorized to perform this action',
statusCode: 401,
title: 'You are not authorized to perform this action',
type: 'Forbidden',
});
});
});

describe('deletePet', () => {
test('unautheticated user cannot delete pet', async () => {
await supertest(app).post('/api/auth/register').send(user);

const { headers, statusCode } = await supertest(app)
.post('/api/auth/login')
.send({
email: user.email,
password: user.password,
});

expect(statusCode).toBe(200);

const cookieValue = headers['set-cookie'][0].split(';')[0].split('=')[1];
const cookieName = 'connect.sid';

// create a pet authenticated as user1
const { body: createdPet } = await supertest(app)
.post('/api/pets')
.set('Cookie', `${cookieName}=${cookieValue}`)
.send(pets[0]);

const { body, statusCode: deleteStatusCode } = await supertest(
app,
).delete(`/api/pets/${createdPet.id}`);

expect(deleteStatusCode).toBe(401);

expect(body).toEqual({
code: 'Forbidden',
errors: [],
message: 'You are not authorized to perform this action',
statusCode: 401,
title: 'Forbidden',
type: 'Forbidden',
});
});

test('authenticated user can delete their own pet', async () => {
await supertest(app).post('/api/auth/register').send(user);

const { headers, statusCode } = await supertest(app)
.post('/api/auth/login')
.send({
email: user.email,
password: user.password,
});

expect(statusCode).toBe(200);

const cookieValue = headers['set-cookie'][0].split(';')[0].split('=')[1];
const cookieName = 'connect.sid';

// create a pet authenticated as user1
const { body: createdPet } = await supertest(app)
.post('/api/pets')
.set('Cookie', `${cookieName}=${cookieValue}`)
.send(pets[0]);

const { body, statusCode: deleteStatusCode } = await supertest(app)
.delete(`/api/pets/${createdPet.id}`)
.set('Cookie', `${cookieName}=${cookieValue}`);

expect(deleteStatusCode).toBe(200);
expect(body).toEqual('');
});
});
});
9 changes: 4 additions & 5 deletions src/controllers/petController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,20 @@ export default class PetController {
statusCode: 404,
});
}

return res.status(200).json(pet);
}

async createPet(req: CreatePetRequest, res: Response) {
const pet = req.body;
const newPet = await this.petService.createPet(pet, req.session.userId);

const newPet = await this.petService.createPet(
req.body,
req.session.userId,
);
return res.status(201).json(newPet);
}

async updatePet(req: UpdatePetRequest, res: Response) {
const pet = req.body;
const updatedPet = await this.petService.updatePet(req.params.id, pet);

return res.status(200).json(updatedPet);
}

Expand Down
15 changes: 15 additions & 0 deletions src/errors/ForbiddenError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import ApiError from './ApiError';

export default class ForbiddenError extends ApiError {
constructor(err?: ApiError) {
super({
title: err?.title ?? 'You are not authorized to perform this action',
type: 'Forbidden',
statusCode: 401,
code: err?.code ?? 'Forbidden',
message: err?.message ?? 'You are not authorized to perform this action',
errors: err?.errors ?? [],
stack: process.env.NODE_ENV === 'development' ? err?.stack : undefined,
});
}
}
11 changes: 11 additions & 0 deletions src/errors/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ErrorRequestHandler } from 'express';
import logger from '../utils/logger';
import ApiError from './ApiError';
import BadRequestError from './BadRequestError';
import ForbiddenError from './ForbiddenError';
import InternalServerError from './InternalServerError';
import NotFoundError from './NotFoundError';

Expand Down Expand Up @@ -39,6 +40,16 @@ const errorHandler: ErrorRequestHandler<{}, ApiError | string> = (
});
}

if (err instanceof ForbiddenError) {
return new ApiError({
title: 'You are not authorized to perform this action',
statusCode: 401,
message: 'You are not authorized to perform this action',
type: 'Forbidden',
code: 'Forbidden',
});
}

logger.error(`An unexpected error occurred: err -> ${err}`);

return new InternalServerError();
Expand Down
7 changes: 7 additions & 0 deletions src/errors/pet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const petErrorCodes = {
PetNotFound: 'PetNotFound',
PetsNotFound: 'PetsNotFound',
PetUpdateNotAuthorised: 'PetUpdateNotAuthorised',
} as const;

export type PetErrorCode = (typeof petErrorCodes)[keyof typeof petErrorCodes];
Loading
Loading