diff --git a/api/package.json b/api/package.json index e0d59d5..47a1fd5 100644 --- a/api/package.json +++ b/api/package.json @@ -16,7 +16,8 @@ "cors": "^2.8.5", "express": "^4.21.2", "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1" + "swagger-ui-express": "^5.0.1", + "zod": "^4.4.3" }, "devDependencies": { "@types/express": "^5.0.0", diff --git a/api/src/routes/product.test.ts b/api/src/routes/product.test.ts new file mode 100644 index 0000000..f6ffc5b --- /dev/null +++ b/api/src/routes/product.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import express from 'express'; +import productRouter, { resetProducts } from './product'; +import { products as seedProducts } from '../seedData'; + +let app: express.Express; + +describe('Product API', () => { + beforeEach(() => { + app = express(); + app.use(express.json()); + app.use('/products', productRouter); + resetProducts(); + }); + + it('should create a new product with valid fields', async () => { + const newProduct = { + productId: 99, + supplierId: 1, + name: 'Test Product', + description: 'A test product', + price: 49.99, + sku: 'TEST-001', + unit: 'piece', + imgName: 'test.png', + }; + const response = await request(app).post('/products').send(newProduct); + expect(response.status).toBe(201); + expect(response.body).toEqual(newProduct); + }); + + it('should return 400 when name is missing', async () => { + const invalidProduct = { + productId: 99, + supplierId: 1, + price: 49.99, + }; + const response = await request(app).post('/products').send(invalidProduct); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Validation failed'); + }); + + it('should return 400 when price is missing', async () => { + const invalidProduct = { + productId: 99, + supplierId: 1, + name: 'Test Product', + }; + const response = await request(app).post('/products').send(invalidProduct); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Validation failed'); + }); + + it('should return 400 when supplierId is missing', async () => { + const invalidProduct = { + productId: 99, + name: 'Test Product', + price: 49.99, + }; + const response = await request(app).post('/products').send(invalidProduct); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Validation failed'); + }); + + it('should return 400 when price is not a positive number', async () => { + const invalidProduct = { + productId: 99, + supplierId: 1, + name: 'Test Product', + price: -10, + }; + const response = await request(app).post('/products').send(invalidProduct); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('error', 'Validation failed'); + }); + + it('should get all products', async () => { + const response = await request(app).get('/products'); + expect(response.status).toBe(200); + expect(response.body.length).toBe(seedProducts.length); + }); + + it('should get a product by ID', async () => { + const response = await request(app).get('/products/1'); + expect(response.status).toBe(200); + expect(response.body).toEqual(seedProducts[0]); + }); + + it('should return 404 for non-existing product', async () => { + const response = await request(app).get('/products/999'); + expect(response.status).toBe(404); + }); +}); diff --git a/api/src/routes/product.ts b/api/src/routes/product.ts index 0c3b8bf..a0144f7 100644 --- a/api/src/routes/product.ts +++ b/api/src/routes/product.ts @@ -100,6 +100,7 @@ */ import express from 'express'; +import { z } from 'zod'; import { Product } from '../models/product'; import { products as seedProducts } from '../seedData'; @@ -107,11 +108,27 @@ const router = express.Router(); let products: Product[] = [...seedProducts]; +// Add reset function for testing +export const resetProducts = () => { + products = [...seedProducts]; +}; + +const productSchema = z.object({ + name: z.string().min(1), + price: z.number().positive(), + supplierId: z.number().int().positive(), +}); + // Create a new product router.post('/', (req, res) => { - const newProduct: Product = req.body; - products.push(newProduct); - res.status(201).json(newProduct); + const result = productSchema.safeParse(req.body); + if (!result.success) { + res.status(400).json({ error: 'Validation failed', details: result.error.issues }); + } else { + const newProduct: Product = req.body; + products.push(newProduct); + res.status(201).json(newProduct); + } }); // Get all products diff --git a/package-lock.json b/package-lock.json index cd641af..5304568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "cors": "^2.8.5", "express": "^4.21.2", "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1" + "swagger-ui-express": "^5.0.1", + "zod": "^4.4.3" }, "devDependencies": { "@types/express": "^5.0.0", @@ -8442,6 +8443,15 @@ "engines": { "node": "^12.20.0 || >=14" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } }