├── jest.config.js ├── src ├── index.ts ├── middleware │ └── index.ts ├── data │ └── index.ts ├── config │ ├── db.ts │ └── swagger.ts ├── models │ └── Product.model.ts ├── server.ts ├── __tests__ │ └── server.test.ts ├── handlers │ ├── product.ts │ └── __test__ │ │ └── product.test.ts └── router.ts ├── .gitignore ├── tsconfig.json └── package.json /jest.config.js: -------------------------------------------------------------------------------- 1 | const { createDefaultPreset } = require("ts-jest"); 2 | 3 | const tsJestTransformCfg = createDefaultPreset().transform; 4 | 5 | /** @type {import("jest").Config} **/ 6 | module.exports = { 7 | testEnvironment: "node", 8 | transform: { 9 | ...tsJestTransformCfg, 10 | }, 11 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import colors from 'colors' 2 | import server from "./server"; 3 | 4 | // If process.env.PORT is not set, default to 4000 5 | const port = process.env.PORT || 4000 6 | server.listen(port, () => { 7 | console.log(colors.cyan.bold(`REST API en el puerto ${port} http://localhost:${port}/`)) 8 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env 27 | coverage/ -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express" 2 | import { validationResult } from 'express-validator' 3 | 4 | export const handleInputErrors = (req: Request, res: Response, next: NextFunction) => { 5 | let errors = validationResult(req) 6 | if(!errors.isEmpty()) { 7 | return res.status(400).json({errors: errors.array()}) 8 | } 9 | 10 | next() 11 | } -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | import { exit } from 'node:process' 2 | import db from '../config/db' 3 | 4 | const clearDB = async () => { 5 | try { 6 | await db.sync({force: true}) 7 | console.log('Database cleared successfully') 8 | exit(0) 9 | } catch (error) { 10 | console.log(error) 11 | exit(1) 12 | } 13 | } 14 | 15 | if(process.argv[2] === '--clear') { 16 | clearDB() 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "./src", 5 | "lib": ["ESNext"], 6 | "target": "esnext", 7 | "moduleResolution": "nodenext", 8 | "module": "nodenext", 9 | "strict": false, 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | 16 | }, 17 | "include": ["src/**/*.ts"] 18 | } -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | // https://sequelize.org/docs/v6/getting-started/ 2 | // npm install --save sequelize 3 | // npm install --save pg pg-hstore 4 | // npm i sequelize-typescript 5 | import { Sequelize } from "sequelize-typescript"; 6 | // npm i dotenv 7 | import dotenv from 'dotenv' 8 | dotenv.config() 9 | 10 | // Database connection configuration (External Database URL) 11 | const db = new Sequelize(process.env.DATABASE_URL!, { 12 | models: [__dirname + '/../models/**/*.ts'], 13 | logging: false 14 | }) 15 | 16 | 17 | export default db -------------------------------------------------------------------------------- /src/models/Product.model.ts: -------------------------------------------------------------------------------- 1 | // https://sequelize.org/docs/v7/models/data-types/ 2 | import { Column, DataType, Default, Model, Table } from "sequelize-typescript"; 3 | 4 | @Table({ 5 | tableName: 'products' 6 | }) 7 | 8 | class Product extends Model { 9 | @Column({ 10 | type: DataType.STRING(100) 11 | }) 12 | declare name: string 13 | 14 | @Column({ 15 | type: DataType.FLOAT 16 | }) 17 | declare price: number 18 | 19 | @Default(true) 20 | @Column({ 21 | type: DataType.BOOLEAN 22 | }) 23 | declare availability: boolean 24 | } 25 | 26 | export default Product -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import router from "./router"; 3 | import db from "./config/db"; 4 | // npm i colors 5 | import colors from 'colors' 6 | import swaggerSpec, {swaggerUiOptions} from "./config/swagger"; 7 | import swaggerUi from 'swagger-ui-express' 8 | 9 | // DB connection 10 | export async function connectDB() { 11 | try { 12 | await db.authenticate() 13 | db.sync() 14 | //console.log(colors.blue("Database connected successfully")) 15 | } catch (error) { 16 | // console.log(error) 17 | console.log(colors.red.bold("Error connecting to the database")) 18 | } 19 | } 20 | connectDB() 21 | 22 | const server = express() 23 | 24 | // Read data from the form 25 | server.use(express.json()) 26 | 27 | server.use('/api/products', router) 28 | 29 | // Docs 30 | server.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerUiOptions)) 31 | 32 | export default server -------------------------------------------------------------------------------- /src/__tests__/server.test.ts: -------------------------------------------------------------------------------- 1 | import { connectDB } from '../server' 2 | import db from '../config/db' 3 | 4 | // Mock simulates a database connection failure by overriding db.authenticate. 5 | // It verifies that the connection error is correctly logged to the console. 6 | jest.mock('../config/db') 7 | 8 | describe('Database connection', () => { 9 | it('should handle database connection error', async () => { 10 | // Simulate a connection failure 11 | jest.spyOn(db, 'authenticate') 12 | .mockRejectedValueOnce(new Error('Database connection error')) 13 | 14 | // Spy on console.log to verify error logging 15 | const consoleSpy = jest.spyOn(console, 'log') 16 | 17 | await connectDB() 18 | 19 | // Expect an error message to be logged 20 | expect(consoleSpy).toHaveBeenCalledWith( 21 | expect.stringContaining('Error connecting to the database') 22 | ) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest_api_node_ts_server", 3 | "version": "1.0.0", 4 | "description": "REST API's with Express and TypeScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --detectOpenHandles", 8 | "test:coverage": "npm run pretest && jest --detectOpenHandles --coverage", 9 | "dev": "nodemon --exec ts-node src/index.ts", 10 | "pretest": "ts-node ./src/data --clear" 11 | }, 12 | "keywords": [ 13 | "AryDani" 14 | ], 15 | "author": "AryDaniel", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@types/express": "^4.17.20", 19 | "@types/jest": "^29.5.14", 20 | "@types/supertest": "^6.0.3", 21 | "@types/swagger-jsdoc": "^6.0.4", 22 | "@types/swagger-ui-express": "^4.1.8", 23 | "jest": "^29.7.0", 24 | "nodemon": "^3.1.10", 25 | "supertest": "^7.1.1", 26 | "ts-jest": "^29.3.4", 27 | "ts-node": "^10.9.2", 28 | "typescript": "^5.8.3" 29 | }, 30 | "dependencies": { 31 | "colors": "^1.4.0", 32 | "dotenv": "^16.5.0", 33 | "express": "^5.1.0", 34 | "express-validator": "^7.2.1", 35 | "pg": "^8.16.0", 36 | "pg-hstore": "^2.3.4", 37 | "sequelize": "^6.37.7", 38 | "sequelize-typescript": "^2.1.6", 39 | "swagger-jsdoc": "^6.2.8", 40 | "swagger-ui-express": "^5.0.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/config/swagger.ts: -------------------------------------------------------------------------------- 1 | import swaggerJSDoc from 'swagger-jsdoc' 2 | import { SwaggerUiOptions } from 'swagger-ui-express' 3 | 4 | const options: swaggerJSDoc.Options = { 5 | swaggerDefinition: { 6 | openapi: '3.0.0', 7 | tags: [ 8 | { 9 | name: 'Products', 10 | description: 'API operations related to products' 11 | } 12 | ], 13 | info: { 14 | title: 'REST API Node.js / Express / TypeScript', 15 | version: "1.0.0", 16 | description: "API Docs for Products" 17 | } 18 | }, 19 | apis: ['./src/router.ts'] 20 | } 21 | 22 | const swaggerSpec = swaggerJSDoc(options) 23 | 24 | const swaggerUiOptions: SwaggerUiOptions = { 25 | customCss: ` 26 | .topbar-wrapper .link { 27 | height: 60px; 28 | width: 60px; 29 | background-image: url('https://static-00.iconduck.com/assets.00/laravel-icon-497x512-uwybstke.png'); 30 | content: url(''); 31 | background-size: contain; 32 | background-repeat: no-repeat; 33 | } 34 | .swagger-ui .topbar { 35 | background-color: #2b3b45; 36 | } 37 | `, 38 | 39 | customfavIcon: 'https://static-00.iconduck.com/assets.00/laravel-icon-497x512-uwybstke.png', 40 | customSiteTitle: 'API Docs - Products', 41 | }; 42 | 43 | 44 | export default swaggerSpec 45 | export { 46 | swaggerUiOptions 47 | } -------------------------------------------------------------------------------- /src/handlers/product.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express" 2 | import Product from "../models/Product.model" 3 | 4 | export const getProducts = async (req: Request, res: Response) => { 5 | const products = await Product.findAll({ 6 | order: [ 7 | ['id', 'ASC'] // ASC or DESC 8 | ], 9 | //limit: 2 // Limit the number of products returned 10 | attributes: {exclude: ['createdAt', 'updatedAt']} // Exclude createdAt and updatedAt fields 11 | }) 12 | res.json({data: products}) 13 | } 14 | 15 | export const getProductById = async (req: Request, res: Response) => { 16 | const { id } = req.params 17 | const product = await Product.findByPk(id) 18 | 19 | if(!product) { 20 | return res.status(404).json({ 21 | error: 'Product not found' 22 | }) 23 | } 24 | res.json({data: product}) 25 | } 26 | 27 | export const createProduct = async (req: Request, res: Response) => { 28 | // Both of these methods work, but the second one is more concise 29 | 30 | // const product = new Product(req.body) 31 | // const savedProduct = await product.save() 32 | // res.json({ data: savedProduct}) 33 | 34 | // Validation 35 | // notEmpty is a validator that checks if the field is not empty 36 | // await check('name').notEmpty().withMessage('Name is required').run(req) 37 | // await check('price') 38 | // .isNumeric().withMessage('Invalid value') 39 | // .notEmpty().withMessage('Price is required') 40 | // .custom(value => value > 0).withMessage('Invalid price') 41 | // .run(req) 42 | 43 | // let errors = validationResult(req) 44 | // if(!errors.isEmpty()) { 45 | // return res.status(400).json({errors: errors.array()}) 46 | // } 47 | 48 | const product = await Product.create(req.body) 49 | res.status(201).json({ data: product}) 50 | 51 | } 52 | 53 | export const updateProduct = async (req: Request, res: Response) => { 54 | const { id } = req.params 55 | const product = await Product.findByPk(id) 56 | 57 | if(!product) { 58 | return res.status(404).json({ 59 | error: 'Product not found' 60 | }) 61 | } 62 | 63 | // update 64 | await product.update(req.body) 65 | await product.save() 66 | 67 | res.json({data: product}) 68 | } 69 | 70 | export const updateAvailability = async (req: Request, res: Response) => { 71 | const { id } = req.params 72 | const product = await Product.findByPk(id) 73 | 74 | if(!product) { 75 | return res.status(404).json({ 76 | error: 'Product not found' 77 | }) 78 | } 79 | 80 | // update 81 | product.availability = !product.dataValues.availability 82 | await product.save() 83 | 84 | res.json({data: product}) 85 | } 86 | 87 | export const deleteProduct = async (req: Request, res: Response) => { 88 | const { id } = req.params 89 | const product = await Product.findByPk(id) 90 | 91 | if(!product) { 92 | return res.status(404).json({ 93 | error: 'Product not found' 94 | }) 95 | } 96 | 97 | // A method that deletes this object from the database. It performs a DELETE operation. 98 | await product.destroy() 99 | res.json({data: 'Deleted Product'}) 100 | } -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express" 2 | import { createProduct, deleteProduct, getProductById, getProducts, updateAvailability, updateProduct } from "./handlers/product" 3 | import { body, param } from "express-validator" 4 | import { handleInputErrors } from "./middleware" 5 | 6 | /** 7 | * @swagger 8 | * components: 9 | * schemas: 10 | * Product: 11 | * type: object 12 | * properties: 13 | * id: 14 | * type: integer 15 | * description: The Product ID 16 | * example: 1 17 | * name: 18 | * type: string 19 | * description: The Product name 20 | * example: Monitor Curvo de 49 Pulgadas 21 | * price: 22 | * type: number 23 | * description: The Product price 24 | * example: 300 25 | * availability: 26 | * type: boolean 27 | * description: The Product availability 28 | * example: true 29 | */ 30 | 31 | /** 32 | * @swagger 33 | * /api/products/: 34 | * get: 35 | * summary: Get a list of products 36 | * tags: 37 | * - Products 38 | * description: Returns a list of products 39 | * responses: 40 | * 200: 41 | * description: Success response 42 | * content: 43 | * application/json: 44 | * schema: 45 | * type: array 46 | * items: 47 | * $ref: '#/components/schemas/Product' 48 | */ 49 | 50 | const router = Router() 51 | // Routing 52 | router.get('/', getProducts) 53 | 54 | /** 55 | * @swagger 56 | * /api/products/{id}: 57 | * get: 58 | * summary: Get a product by ID 59 | * tags: 60 | * - Products 61 | * description: Returns a product based on by its unique ID 62 | * parameters: 63 | * - in: path 64 | * name: id 65 | * description: The Product ID to retrieve 66 | * required: true 67 | * schema: 68 | * type: integer 69 | * responses: 70 | * 200: 71 | * description: Success response 72 | * content: 73 | * application/json: 74 | * schema: 75 | * $ref: '#/components/schemas/Product' 76 | * 404: 77 | * description: Product not found 78 | * 400: 79 | * description: Bad request - Invalid ID provided 80 | * 81 | */ 82 | router.get('/:id', 83 | param('id').isInt().withMessage('Invalid ID'), 84 | handleInputErrors, 85 | getProductById 86 | ) 87 | 88 | /** 89 | * @swagger 90 | * /api/products/: 91 | * post: 92 | * summary: Create a new product 93 | * tags: 94 | * - Products 95 | * description: Creates a new product with the provided details 96 | * requestBody: 97 | * required: true 98 | * content: 99 | * application/json: 100 | * schema: 101 | * type: object 102 | * properties: 103 | * name: 104 | * type: string 105 | * example: Monitor Curvo de 49 Pulgadas 106 | * price: 107 | * type: number 108 | * example: 300 109 | * availability: 110 | * type: boolean 111 | * example: true 112 | * responses: 113 | * 201: 114 | * description: Product updated successfully 115 | * content: 116 | * application/json: 117 | * schema: 118 | * $ref: '#/components/schemas/Product' 119 | * 400: 120 | * description: Bad request - Invalid input data 121 | */ 122 | router.post('/', 123 | // Validation 124 | body('name').notEmpty().withMessage('Name is required'), 125 | body('price') 126 | .isNumeric().withMessage('Invalid value') 127 | .notEmpty().withMessage('Price is required') 128 | .custom(value => value > 0).withMessage('Invalid price'), 129 | handleInputErrors, 130 | createProduct 131 | ) 132 | 133 | /** 134 | * @swagger 135 | * /api/products/{id}: 136 | * put: 137 | * summary: Update an existing product 138 | * tags: 139 | * - Products 140 | * description: Updates an existing product with the provided details 141 | * parameters: 142 | * - in: path 143 | * name: id 144 | * description: The Product ID to retrieve 145 | * required: true 146 | * schema: 147 | * type: integer 148 | * requestBody: 149 | * required: true 150 | * content: 151 | * application/json: 152 | * schema: 153 | * type: object 154 | * properties: 155 | * name: 156 | * type: string 157 | * example: Monitor Curvo de 49 Pulgadas 158 | * price: 159 | * type: number 160 | * example: 399 161 | * availability: 162 | * type: boolean 163 | * example: true 164 | * responses: 165 | * 201: 166 | * description: Product updated successfully 167 | * content: 168 | * application/json: 169 | * schema: 170 | * $ref: '#/components/schemas/Product' 171 | * 400: 172 | * description: Bad request - Invalid input data 173 | * 404: 174 | * description: Product not found 175 | */ 176 | router.put('/:id', 177 | 178 | // Validation 179 | param('id').isInt().withMessage('Invalid ID'), 180 | body('name').notEmpty().withMessage('Name is required'), 181 | body('price') 182 | .isNumeric().withMessage('Invalid value') 183 | .notEmpty().withMessage('Price is required') 184 | .custom(value => value > 0).withMessage('Invalid price'), 185 | body('availability').isBoolean().withMessage('Availability must be a boolean'), 186 | handleInputErrors, 187 | updateProduct 188 | ) 189 | 190 | // The PATCH method is used to update partially an existing resource in a [[Web service]] 191 | // It is used to update only the fields that are provided in the request 192 | /** 193 | * @swagger 194 | * /api/products/{id}: 195 | * patch: 196 | * summary: Update the availability of a product 197 | * tags: 198 | * - Products 199 | * description: Updates the availability of a product with the provided ID 200 | * parameters: 201 | * - in: path 202 | * name: id 203 | * description: The Product ID to update 204 | * required: true 205 | * schema: 206 | * type: integer 207 | * responses: 208 | * 200: 209 | * description: Product availability updated successfully 210 | * content: 211 | * application/json: 212 | * schema: 213 | * $ref: '#/components/schemas/Product' 214 | * 400: 215 | * description: Bad request - Invalid ID 216 | * 404: 217 | * description: Product not found 218 | */ 219 | router.patch('/:id', 220 | param('id').isInt().withMessage('Invalid ID'), 221 | handleInputErrors, 222 | updateAvailability 223 | ) 224 | 225 | /** 226 | * @swagger 227 | * /api/products/{id}: 228 | * delete: 229 | * summary: Delete a product by ID 230 | * tags: 231 | * - Products 232 | * description: Deletes a product based on its unique ID 233 | * parameters: 234 | * - in: path 235 | * name: id 236 | * description: The Product ID to delete 237 | * required: true 238 | * schema: 239 | * type: integer 240 | * responses: 241 | * 200: 242 | * description: Product deleted successfully 243 | * content: 244 | * application/json: 245 | * schema: 246 | * type: string 247 | * example: Product deleted successfully 248 | * 404: 249 | * description: Product not found 250 | */ 251 | router.delete('/:id', 252 | param('id').isInt().withMessage('Invalid ID'), 253 | handleInputErrors, 254 | deleteProduct 255 | ) 256 | 257 | export default router -------------------------------------------------------------------------------- /src/handlers/__test__/product.test.ts: -------------------------------------------------------------------------------- 1 | import server from "../../server"; 2 | import request from "supertest"; 3 | 4 | describe('POST /api/products', () => { 5 | it('Should display validation erros', async () => { 6 | const response = await request(server).post('/api/products').send({}) 7 | 8 | expect(response.status).toBe(400) 9 | // Check if the response has an errors property 10 | expect(response.body).toHaveProperty('errors') 11 | // Check if the errors array has 4 items 12 | expect(response.body.errors).toHaveLength(4) 13 | 14 | expect(response.status).not.toBe(404) 15 | expect(response.body.errors).not.toHaveLength(2) 16 | }) 17 | 18 | it('Should validate that the price is greater than 0', async () => { 19 | const response = await request(server).post('/api/products').send({ 20 | name: 'iPhone - test', 21 | price: 0 22 | }) 23 | 24 | expect(response.status).toBe(400) 25 | // Check if the response has an errors property 26 | expect(response.body).toHaveProperty('errors') 27 | // Check if the errors array has 4 items 28 | expect(response.body.errors).toHaveLength(1) 29 | 30 | expect(response.status).not.toBe(404) 31 | expect(response.body.errors).not.toHaveLength(2) 32 | }) 33 | 34 | it('Should validate that the price is a number and greater than 0', async () => { 35 | const response = await request(server).post('/api/products').send({ 36 | name: 'iPhone - test', 37 | price: "Hola" 38 | }) 39 | 40 | expect(response.status).toBe(400) 41 | // Check if the response has an errors property 42 | expect(response.body).toHaveProperty('errors') 43 | // Check if the errors array has 4 items 44 | expect(response.body.errors).toHaveLength(2) 45 | 46 | expect(response.status).not.toBe(404) 47 | expect(response.body.errors).not.toHaveLength(4) 48 | }) 49 | 50 | it('should create a new product', async () => { 51 | const response = await request(server).post('/api/products').send({ 52 | "name": "Mac - test", 53 | "price": 2800 54 | }) 55 | 56 | // Check if the response status is 201 Created 57 | expect(response.status).toBe(201) 58 | // Check if the response has a data property 59 | expect(response.body).toHaveProperty('data') 60 | 61 | expect(response.status).not.toBe(404) 62 | expect(response.status).not.toBe(200) 63 | expect(response.body).not.toHaveProperty('errors') 64 | }) 65 | }) 66 | 67 | describe('GET /api/products', () => { 68 | it('Should check if api/products url exists', async () => { 69 | const response = await request(server).get('/api/products') 70 | expect(response.status).not.toBe(404) 71 | }) 72 | 73 | it('GET a JSON response with products', async () => { 74 | const response = await request(server).get('/api/products') 75 | expect(response.status).toBe(200) 76 | expect(response.header['content-type']).toMatch(/json/) 77 | expect(response.body).toHaveProperty('data') 78 | expect(response.body.data).toHaveLength(1) 79 | 80 | expect(response.body).not.toHaveProperty('errors') 81 | }) 82 | }) 83 | 84 | 85 | describe('GET /api/products/:id', () => { 86 | it('Should return a 404 response for a non-existent product', async () => { 87 | const productId = 2000 88 | const response = await request(server).get(`/api/products/${productId}`) 89 | 90 | expect(response.status).toBe(404) 91 | expect(response.body).toHaveProperty('error') 92 | expect(response.body.error).toBe('Product not found') 93 | }) 94 | 95 | it('Should check a valid ID in the URL', async () => { 96 | const response = await request(server).get('/api/products/not-valid-url') 97 | 98 | expect(response.status).toBe(400) 99 | expect(response.body).toHaveProperty('errors') 100 | expect(response.body.errors).toHaveLength(1) 101 | expect(response.body.errors[0].msg).toBe('Invalid ID') 102 | }) 103 | 104 | it('get a JSON response for a single product', async () => { 105 | const response = await request(server).get('/api/products/1') 106 | 107 | expect(response.status).toBe(200) 108 | expect(response.body).toHaveProperty('data') 109 | }) 110 | }) 111 | 112 | describe('PUT /api/products/:id', () => { 113 | it('Should return an error for an invalid ID in the URL', async () => { 114 | const response = await request(server) 115 | .put('/api/products/not-valid-url') 116 | .send({ 117 | name: "Monitor TEST", 118 | availability: true, 119 | price: 300 120 | }) 121 | 122 | expect(response.status).toBe(400) 123 | expect(response.body).toHaveProperty('errors') 124 | expect(response.body.errors).toHaveLength(1) 125 | expect(response.body.errors[0].msg).toBe('Invalid ID') 126 | }) 127 | 128 | it('should display validation error message when updating a product', async() => { 129 | const response = await request(server) 130 | .put('/api/products/1') 131 | .send({}) 132 | 133 | expect(response.status).toBe(400) 134 | expect(response.body).toHaveProperty('errors') 135 | expect(response.body.errors).toBeTruthy() 136 | expect(response.body.errors).toHaveLength(5) 137 | 138 | expect(response.status).not.toBe(200) 139 | expect(response.body).not.toHaveProperty('data') 140 | }) 141 | }) 142 | 143 | describe('PUT /api/products/:id', () => { 144 | it('Should validate that the price is greater than 0', async() => { 145 | const response = await request(server) 146 | .put('/api/products/1') 147 | .send({ 148 | name: "Monitor TEST", 149 | availability: true, 150 | price: 0 151 | }) 152 | 153 | expect(response.status).toBe(400) 154 | expect(response.body).toHaveProperty('errors') 155 | // Check if the error message is present 156 | expect(response.body.errors).toBeTruthy() 157 | // Check if the errors array has 1 item 158 | expect(response.body.errors).toHaveLength(1) 159 | // Check if the error message is 'Invalid price' 160 | expect(response.body.errors[0].msg).toBe('Invalid price') 161 | 162 | expect(response.status).not.toBe(200) 163 | expect(response.body).not.toHaveProperty('data') 164 | }) 165 | 166 | it('Should return a 404 response for a non-existent product', async() => { 167 | const productId = 2000 168 | const response = await request(server) 169 | .put(`/api/products/${productId}`) 170 | .send({ 171 | name: "Monitor TEST", 172 | availability: true, 173 | price: 300 174 | }) 175 | 176 | expect(response.status).toBe(404) 177 | expect(response.body.error).toBe('Product not found') 178 | 179 | expect(response.status).not.toBe(200) 180 | expect(response.body).not.toHaveProperty('data') 181 | }) 182 | 183 | it('Should update an existing product with valid data', async() => { 184 | const response = await request(server) 185 | .put(`/api/products/1`) 186 | .send({ 187 | name: "Monitor TEST", 188 | availability: true, 189 | price: 300 190 | }) 191 | 192 | expect(response.status).toBe(200) 193 | expect(response.body).toHaveProperty('data') 194 | 195 | expect(response.status).not.toBe(400) 196 | expect(response.body).not.toHaveProperty('errors') 197 | }) 198 | }) 199 | 200 | describe('PATCH /api/products/:id', () => { 201 | it('Should return an error for an invalid ID in the URL', async () => { 202 | const response = await request(server).patch('/api/products/not-valid-url') 203 | 204 | expect(response.status).toBe(400) 205 | expect(response.body).toHaveProperty('errors') 206 | expect(response.body.errors).toHaveLength(1) 207 | expect(response.body.errors[0].msg).toBe('Invalid ID') 208 | }) 209 | 210 | it('Should return a 404 response for a non-existent product', async () => { 211 | const productId = 2000 212 | const response = await request(server).patch(`/api/products/${productId}`) 213 | 214 | expect(response.status).toBe(404) 215 | expect(response.body.error).toBe('Product not found') 216 | 217 | expect(response.status).not.toBe(200) 218 | expect(response.body).not.toHaveProperty('data') 219 | }) 220 | 221 | it('Should update the availability of an existing product', async () => { 222 | const response = await request(server).patch('/api/products/1') 223 | 224 | expect(response.status).toBe(200) 225 | expect(response.body).toHaveProperty('data') 226 | // Check if the availability has been toggled 227 | expect(response.body.data.availability).toBe(false) 228 | 229 | expect(response.status).not.toBe(404) 230 | expect(response.status).not.toBe(400) 231 | expect(response.body).not.toHaveProperty('errors') 232 | }) 233 | }) 234 | 235 | describe('DELETE /api/products/:id', () => { 236 | it('Should check a valid ID in the URL', async () => { 237 | const response = await request(server).delete('/api/products/not-valid-url') 238 | 239 | expect(response.status).toBe(400) 240 | expect(response.body).toHaveProperty('errors') 241 | expect(response.body.errors).toHaveLength(1) 242 | expect(response.body.errors[0].msg).toBe('Invalid ID') 243 | }) 244 | 245 | it('Should return a 404 response for a non-existent product', async () => { 246 | const productId = 2000 247 | const response = await request(server).delete(`/api/products/${productId}`) 248 | 249 | expect(response.status).toBe(404) 250 | expect(response.body.error).toBe('Product not found') 251 | }) 252 | 253 | it('Should delete an existing product', async () => { 254 | const response = await request(server).delete('/api/products/1') 255 | 256 | expect(response.status).toBe(200) 257 | expect(response.body).toHaveProperty('data') 258 | expect(response.body.data).toBe('Deleted Product') 259 | 260 | expect(response.status).not.toBe(404) 261 | expect(response.status).not.toBe(400) 262 | expect(response.body).not.toHaveProperty('errors') 263 | }) 264 | }) --------------------------------------------------------------------------------