Skip to content

Commit

Permalink
Merge pull request #9 from isdi-coders-2023/feature/cors
Browse files Browse the repository at this point in the history
add endpoint
  • Loading branch information
marcosparajua authored Jun 27, 2024
2 parents fbf5fbc + 61759b7 commit b7d35cb
Show file tree
Hide file tree
Showing 8 changed files with 444 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { FilesRouter } from './routers/files.router.js';
import { ArticlesController } from './controllers/articles.controller.js';
import { ArticlesSqlRepo } from './repositories/articles.sql.repo.js';
import { ArticlesRouter } from './routers/articles.router.js';
import { BooksController } from './controllers/books.controller.js';
import { BooksSqlRepo } from './repositories/books.sql.repo.js';
import { BooksRouter } from './routers/books.router.js';

export const createApp = () => {
debug('Creating app');
Expand Down Expand Up @@ -62,6 +65,15 @@ export const startApp = (app: Express, prisma: PrismaClient) => {
'avatar'
);

const booksRepo = new BooksSqlRepo(prisma);
const booksController = new BooksController(booksRepo);
const booksRouter = new BooksRouter(
booksController,
authInterceptor,
filesInterceptor
);
app.use('/books', booksRouter.router);

app.use('/files', filesRouter.router);
const errorsMiddleware = new ErrorsMiddleware();
app.use(errorsMiddleware.handle.bind(errorsMiddleware));
Expand Down
89 changes: 89 additions & 0 deletions src/controllers/books.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { type Request, type Response, type NextFunction } from 'express';
import { BooksController } from './books.controller.js';
import { type Repo } from '../repositories/baseRepo.js';
import { type ObjectSchema } from 'joi';
import { type Book, type BookCreateDto } from '../entities/book.js';
import { bookCreateDtoSchema } from '../entities/book.schema.js';
jest.mock('../entities/book.schema.js', () => ({
bookCreateDtoSchema: {
validate: jest.fn().mockReturnValue({ error: null, value: {} }),
} as unknown as ObjectSchema<BookCreateDto>,
bookUpdateDtoSchema: {
validate: jest.fn().mockReturnValue({ error: null, value: {} }),
} as unknown as ObjectSchema<BookCreateDto>,
}));

describe('Given an instance of the class BooksController', () => {
const repo = {
readAll: jest.fn(),
readById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
} as unknown as Repo<Book, BookCreateDto>;

const req = {} as unknown as Request;
const res = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
} as unknown as Response;
const next = jest.fn();

const controller = new BooksController(repo);

test('Then it should be an instance of the class', () => {
expect(controller).toBeInstanceOf(BooksController);
});

describe('When we use the method create', () => {
describe('And body is not valid', () => {
test('Then it should call next with an error', async () => {
req.body = {};
(bookCreateDtoSchema.validate as jest.Mock).mockReturnValueOnce({
error: new Error('validation error'),
value: {},
});

await controller.create(req, res, next);
expect(next).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('validation error'),
})
);
});
});

describe('And body is valid', () => {
test.skip('Then it should call repo.create', async () => {
const book = {
title: 'Test Book',
author: 'Test Author',
} as BookCreateDto;

req.body = book;
await controller.create(req, res, next);
expect(repo.create).toHaveBeenCalledWith(book);
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith(expect.any(Object));
});
});

describe('And an error is thrown during creation', () => {
test('Then it should call next with an error', async () => {
(repo.create as jest.Mock).mockRejectedValueOnce(
new Error('Create error')
);
const book = {
title: 'Test Book',
author: 'Test Author',
} as BookCreateDto;

req.body = book;
await controller.create(req, res, next);
expect(next).toHaveBeenCalledWith(expect.any(Error));
});
});
});
});
19 changes: 19 additions & 0 deletions src/controllers/books.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type NextFunction, type Request, type Response } from 'express';
import createDebug from 'debug';
import { type BookCreateDto, type Book } from '../entities/book.js';
import {
bookCreateDtoSchema,
bookUpdateDtoSchema,
} from '../entities/book.schema.js';
import { type Repo } from '../repositories/baseRepo.js';
import { BaseController } from './baseController.js';

const debug = createDebug('BOOKS:books:controller');

export class BooksController extends BaseController<Book, BookCreateDto> {
constructor(protected readonly repo: Repo<Book, BookCreateDto>) {
super(repo, bookCreateDtoSchema, bookUpdateDtoSchema);

debug('Instantiated book controller');
}
}
4 changes: 2 additions & 2 deletions src/entities/book.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const bookCreateDtoSchema = Joi.object<BookCreateDto>({
author: Joi.string().required(),
year: Joi.number().integer().min(0).required(),
isbn: Joi.string().required(),
coverUrl: Joi.string().uri().required(),
avatar: Joi.string().allow('', null),
description: Joi.string().required(),
});

Expand All @@ -15,6 +15,6 @@ export const bookUpdateDtoSchema = Joi.object<BookUpdateDto>({
author: Joi.string(),
year: Joi.number().integer().min(0),
isbn: Joi.string(),
coverUrl: Joi.string().uri(),
avatar: Joi.string().uri(),
description: Joi.string(),
});
2 changes: 1 addition & 1 deletion src/entities/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type Book = {
author: string;
year: number;
isbn: string;
coverUrl: string;
avatar: string;
description: string;
};
export type BookCreateDto = Omit<Book, 'id'>;
Expand Down
198 changes: 198 additions & 0 deletions src/repositories/books.sql.repo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { type PrismaClient } from '@prisma/client';
import { BooksSqlRepo } from './books.sql.repo.js';
import { HttpError } from '../middleware/errors.middleware.js';
import { type BookCreateDto } from '../entities/book.js';

const mockPrisma = {
book: {
findMany: jest.fn().mockResolvedValue([]),
findUnique: jest.fn().mockResolvedValue({
id: '1',
title: 'Test Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'A test book',
}),
create: jest.fn().mockResolvedValue({
id: '1',
title: 'New Book',
year: 2021,
isbn: '0987654321',
author: 'Author',
avatar: 'avatar.png',
description: 'A new book',
}),
update: jest.fn().mockResolvedValue({
id: '1',
title: 'Updated Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'An updated book',
}),
delete: jest.fn().mockResolvedValue({
id: '1',
title: 'Deleted Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'A deleted book',
}),
},
} as unknown as PrismaClient;

describe('Given an instance of the class BooksSqlRepo', () => {
const repo = new BooksSqlRepo(mockPrisma);

test('Then it should be an instance of the class', () => {
expect(repo).toBeInstanceOf(BooksSqlRepo);
});

describe('When we use the method readAll', () => {
test('Then it should call prisma.findMany', async () => {
const result = await repo.readAll();
expect(mockPrisma.book.findMany).toHaveBeenCalled();
expect(result).toEqual([]);
});
});

describe('When we use the method readById with a valid ID', () => {
test('Then it should call prisma.findUnique', async () => {
const result = await repo.readById('1');
expect(mockPrisma.book.findUnique).toHaveBeenCalled();
expect(result).toEqual({
id: '1',
title: 'Test Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'A test book',
});
});
});

describe('When we use the method readById with an invalid ID', () => {
test('Then it should throw an error', async () => {
(mockPrisma.book.findUnique as jest.Mock).mockResolvedValueOnce(null);
await expect(repo.readById('2')).rejects.toThrow(
new HttpError(404, 'Not Found', 'Book 2 not found')
);
});
});

describe('When we use the method create', () => {
test('Then it should call prisma.create', async () => {
const data: BookCreateDto = {
title: 'New Book',
year: 2021,
isbn: '0987654321',
author: 'Author',
avatar: 'avatar.png',
description: 'A new book',
};
const result = await repo.create(data);
expect(mockPrisma.book.create).toHaveBeenCalled();
expect(result).toEqual({
id: '1',
title: 'New Book',
year: 2021,
isbn: '0987654321',
author: 'Author',
avatar: 'avatar.png',
description: 'A new book',
});
});

test('Then it should throw an error if create fails', async () => {
(mockPrisma.book.create as jest.Mock).mockRejectedValueOnce(
new Error('Failed to create book')
);

const data: BookCreateDto = {
title: 'New Book',
year: 2021,
isbn: '0987654321',
author: 'Author',
avatar: 'avatar.png',
description: 'A new book',
};

await expect(repo.create(data)).rejects.toThrow(
new HttpError(500, 'Internal Server Error', 'Error creating book')
);

expect(mockPrisma.book.create).toHaveBeenCalled();
});
});

describe('When we use the method update with a valid ID', () => {
test('Then it should call prisma.update', async () => {
const data: BookCreateDto = {
title: 'Updated Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'An updated book',
};
const result = await repo.update('1', data);
expect(mockPrisma.book.update).toHaveBeenCalled();
expect(result).toEqual({
id: '1',
title: 'Updated Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'An updated book',
});
});
});

describe('When we use the method update with an invalid ID', () => {
test.skip('Then it should throw an error', async () => {
(mockPrisma.book.findUnique as jest.Mock).mockResolvedValueOnce(null);
const data: BookCreateDto = {
title: 'Updated Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'An updated book',
};
await expect(repo.update('2', data)).rejects.toThrow(
new HttpError(404, 'Not Found', 'Book 2 not found')
);
});
});

describe('When we use the method delete with a valid ID', () => {
test.skip('Then it should call prisma.delete', async () => {
const result = await repo.delete('1');
expect(mockPrisma.book.delete).toHaveBeenCalled();
expect(result).toEqual({
id: '1',
title: 'Deleted Book',
year: 2021,
isbn: '1234567890',
author: 'Author',
avatar: 'avatar.png',
description: 'A deleted book',
});
});
});

describe('When we use the method delete with an invalid ID', () => {
test('Then it should throw an error', async () => {
(mockPrisma.book.findUnique as jest.Mock).mockResolvedValueOnce(null);
await expect(repo.delete('2')).rejects.toThrow(
new HttpError(404, 'Not Found', 'Book 2 not found')
);
});
});
});
Loading

0 comments on commit b7d35cb

Please sign in to comment.