Skip to content
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
44 changes: 44 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"http-status-codes": "^2.2.0",
"joi": "^17.9.2",
"morgan": "^1.10.0",
"ts-jest": "^29.1.1",
"winston": "^3.10.0"
Expand Down
18 changes: 18 additions & 0 deletions src/business/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,21 @@ export async function findBySku(sku: number): Promise<IProduct> {

return productParser(response[0]);
}

export async function update(sku: number, payload: IProduct): Promise<IProduct> {
const productIndex: number = product.getProductIndex(sku);

if (productIndex === -1) {
throw new Error("Product not found");
}
payload.sku = sku;
const response = product.update(productIndex, payload);
return productParser(response);
}

export async function remove(sku: number): Promise<void> {
if (!product.checkExists(sku)) {
throw new Error("Product not found");
}
product.remove(sku);
}
41 changes: 38 additions & 3 deletions src/controllers/product.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { createBusiness, findAll, findBySku, remove, update } from "../business/product";
import { StatusCodes } from "http-status-codes";
import { logger } from "../utils/winston";
import { createBusiness, findAll, findBySku } from "../business/product";

export async function createController(
req: Request,
Expand All @@ -12,9 +12,8 @@ export async function createController(
const response = await createBusiness(req.body);
return res.status(StatusCodes.CREATED).json(response);
} catch (error) {
logger.error(error);

if (error.message === "Product already exists") {
logger.error(error.message);
return res.status(StatusCodes.CONFLICT).json({ message: error.message });
}
next(error);
Expand Down Expand Up @@ -44,9 +43,45 @@ export async function findBySkuController(
return res.status(StatusCodes.OK).json(response);
} catch (error) {
if (error.message === "Product not found") {
logger.info(error.message);
return res.status(StatusCodes.NOT_FOUND).json({ message: error.message });
}
next(error);
}
}

export async function updateController(
req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
try {
const response = await update(parseInt(req.params.sku), req.body);
return res.status(StatusCodes.OK).json(response);
} catch (error) {
if (error.message === "Product not found") {
logger.error(error);
return res.status(StatusCodes.NOT_FOUND).json({ message: error.message });
}
next(error);
}
}

export async function removeController(
req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
try {
await remove(parseInt(req.params.sku));
return res.status(StatusCodes.NO_CONTENT).send();
} catch (error) {
if (error.message === "Product not found") {
logger.error(error.message);
return res.status(StatusCodes.NOT_FOUND).json({ message: error.message });
}
next(error);
}
}


7 changes: 7 additions & 0 deletions src/interfaces/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Schema } from 'joi'

export interface IValidator {
params?: Schema
query?: Schema
body?: Schema
}
32 changes: 32 additions & 0 deletions src/middlewares/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Request, Response, NextFunction } from "express";
import { IValidator } from "../interfaces/validator";
import { StatusCodes } from "http-status-codes";

class Validator {
static schema: IValidator;

static validate(schema: IValidator) {
return this.makeValidation.bind(schema);
}

static makeValidation(req: Request, res: Response, next: NextFunction) {
const errors = [];
const schema = this;

Object.keys(schema).map((key) => {
const { error } = schema[key].validate(req[key]);

if (error) errors.push({ type: `${key} validation`, error });

return key;
});

if (errors.length === 0) {
return next();
}

return res.status(StatusCodes.BAD_REQUEST).json({ message: "Validation Error", errors });
}
}

export { Validator };
9 changes: 7 additions & 2 deletions src/routes/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import {
createController,
findAllController,
findBySkuController,
updateController,
removeController,
} from "../controllers/product";
import { Schema } from "../schemas/product";

const routes = Router();

routes.post("/", createController);
routes.post("/", Schema.create, createController);
routes.get("/", findAllController);
routes.get("/:sku", findBySkuController);
routes.get("/:sku", Schema.bySku, findBySkuController);
routes.put("/:sku", Schema.bySku, Schema.update, updateController);
routes.delete("/:sku", Schema.bySku, removeController);

export default routes;
50 changes: 50 additions & 0 deletions src/schemas/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Joi from "joi";
import { Validator } from "../middlewares/validator";

class Schema extends Validator {
static productBody = {
name: Joi.string().required(),
inventory: Joi.object({
warehouses: Joi.array().items(
Joi.object({
locality: Joi.string().required(),
quantity: Joi.number().required(),
type: Joi.string().required().valid("ECOMMERCE", "PHYSICAL_STORE"),
})
),
}),
};

static get bySku() {
const schema = {
params: Joi.object({
sku: Joi.number().min(1).required(),
}),
};

return this.validate(schema);
}

static get create() {
const schema = {
body: Joi.object({
sku: Joi.number().required(),
...this.productBody
}),
};

return this.validate(schema);
}

static get update() {
const schema = {
body: Joi.object({
...this.productBody
}),
};

return this.validate(schema);
}
}

export { Schema };
87 changes: 87 additions & 0 deletions tests/integration/products/fail.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import request from "supertest";
import app from "../../../src/app";
import * as mocks from "./mocks/products-mock";
import * as business from "../../../src/business/product";

describe("/api/v1/product endpoint fail test", () => {
test("[POST] should return 500 when try create a product", async () => {
jest.spyOn(business, "createBusiness").mockRejectedValue(new Error("Erro test"))


const res = await request(app)
.post("/api/v1/products")
.send(mocks.productOne);

expect(res.statusCode).toEqual(500);
expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("message", "Erro test");
});

test("[GET] should return 400 when get product by id with wrong value", async () => {
const res = await request(app).get("/api/v1/products/aaaa");

expect(res.statusCode).toEqual(400);
})

test("[GET] should return 500 when try to get all products", async () => {
jest.spyOn(business, "findAll").mockRejectedValue(new Error("Erro test"))


const res = await request(app)
.get("/api/v1/products")

expect(res.statusCode).toEqual(500);
expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("message", "Erro test");
});

test("[GET] should return 500 when try to get product by sku", async () => {
jest.spyOn(business, "findBySku").mockRejectedValue(new Error("Erro test"))


const res = await request(app)
.get("/api/v1/products/784")

expect(res.statusCode).toEqual(500);
expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("message", "Erro test");
});

test("[PUT] should return 500 when try to update a product by sku", async () => {
jest.spyOn(business, "update").mockRejectedValue(new Error("Erro test"))


const res = await request(app)
.put("/api/v1/products/784").send({name: "Malbec",
inventory: {
warehouses: [
{
locality: "SP",
quantity: 10,
type: "ECOMMERCE",
},
{
locality: "FRANCA",
quantity: 15,
type: "PHYSICAL_STORE",
},
],
},})

expect(res.statusCode).toEqual(500);
expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("message", "Erro test");
});

test("[DELETE] should return 500 when try to remove product by sku", async () => {
jest.spyOn(business, "remove").mockRejectedValue(new Error("Erro test"))


const res = await request(app)
.delete("/api/v1/products/784")

expect(res.statusCode).toEqual(500);
expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("message", "Erro test");
});
});
Loading