diff --git a/README.md b/README.md index 96516fae..ba0fcb10 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## Sunglasses.io Server -This project has been created by a student at Project Shift, a software engineering fellowship located in Downtown Durham. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks. +To access this project, fork and clone it from this github repository then "cd" into the project directory and type "npm install" into the terminal. Make sure the necessary dependencies are installed in order to run it properly. Update the hardcoded token on line 160 of server.test.js with the latest "Generated Token" in the console log. You can find that console log and run the tests by typing "mocha test/server.test.js --watch" into the terminal. -If you have any questions about this project or the program in general, visit projectshift.io or email hello@projectshift.io. +This project has been created by a student at Project Shift, a software engineering fellowship located in Downtown Durham. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks. diff --git a/api/brands.json b/api/brands.json new file mode 100644 index 00000000..86a147eb --- /dev/null +++ b/api/brands.json @@ -0,0 +1,22 @@ +[ + { + "id": "1", + "name": "Oakley" + }, + { + "id": "2", + "name": "Ray Ban" + }, + { + "id": "3", + "name": "Levi's" + }, + { + "id": "4", + "name": "DKNY" + }, + { + "id": "5", + "name": "Burberry" + } +] diff --git a/api/products.json b/api/products.json new file mode 100644 index 00000000..054839b6 --- /dev/null +++ b/api/products.json @@ -0,0 +1,101 @@ +[ + { + "id": "1", + "brandId": "1", + "categoryId": "1", + "name": "Superglasses", + "description": "The best glasses in the world", + "price":150, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "2", + "brandId": "1", + "categoryId": "1", + "name": "Black Sunglasses", + "description": "The best glasses in the world", + "price":100, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "3", + "brandId": "1", + "categoryId": "1", + "name": "Brown Sunglasses", + "description": "The best glasses in the world", + "price":50, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "4", + "brandId": "2", + "categoryId": "2", + "name": "Better glasses", + "description": "The best glasses in the world", + "price":1500, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "5", + "brandId": "2", + "categoryId": "2", + "name": "Glasses", + "description": "The most normal glasses in the world", + "price":150, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "6", + "brandId": "3", + "categoryId": "3", + "name": "glas", + "description": "Pretty awful glasses", + "price":10, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "7", + "brandId": "3", + "categoryId": "3", + "name": "QDogs Glasses", + "description": "They bark", + "price":1500, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "8", + "brandId": "4", + "categoryId": "4", + "name": "Coke cans", + "description": "The thickest glasses in the world", + "price":110, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "9", + "brandId": "4", + "categoryId": "4", + "name": "Sugar", + "description": "The sweetest glasses in the world", + "price":125, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "10", + "brandId": "5", + "categoryId": "5", + "name": "Peanut Butter", + "description": "The stickiest glasses in the world", + "price":103, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + }, + { + "id": "11", + "brandId": "5", + "categoryId": "5", + "name": "Habanero", + "description": "The spiciest glasses in the world", + "price":153, + "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] + } +] \ No newline at end of file diff --git a/api/users.json b/api/users.json new file mode 100644 index 00000000..ff981c99 --- /dev/null +++ b/api/users.json @@ -0,0 +1,107 @@ +[ + { + "userId": "1", + "gender": "female", + "cart": [], + "name": { + "title": "mrs", + "first": "susanna", + "last": "richards" + }, + "location": { + "street": "2343 herbert road", + "city": "duleek", + "state": "donegal", + "postcode": 38567 + }, + "email": "susanna.richards@example.com", + "login": { + "username": "yellowleopard753", + "password": "jonjon", + "salt": "eNuMvema", + "md5": "a8be2a69c8c91684588f4e1a29442dd7", + "sha1": "f9a60bbf8b550c10712e470d713784c3ba78a68e", + "sha256": "4dca9535634c102fbadbe62dc5b37cd608f9f3ced9aacf42a5669e5a312690a0" + }, + "dob": "1954-10-09 10:47:17", + "registered": "2003-08-03 01:12:24", + "phone": "031-941-6700", + "cell": "081-032-7884", + "picture": { + "large": "https://randomuser.me/api/portraits/women/55.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/55.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/55.jpg" + }, + "nat": "IE" + }, + { + "userId": "2", + "gender": "male", + "cart": [], + "name": { + "title": "mr", + "first": "salvador", + "last": "jordan" + }, + "location": { + "street": "9849 valley view ln", + "city": "burkburnett", + "state": "delaware", + "postcode": 78623 + }, + "email": "salvador.jordan@example.com", + "login": { + "username": "lazywolf342", + "password": "tucker", + "salt": "oSngghny", + "md5": "30079fb24f447efc355585fcd4d97494", + "sha1": "dbeb2d0155dad0de0ab9bbe21c062e260a61d741", + "sha256": "4f9416fa89bfd251e07da3ca0aed4d077a011d6ef7d6ed75e1d439c96d75d2b2" + }, + "dob": "1955-07-28 22:32:14", + "registered": "2010-01-10 06:52:31", + "phone": "(944)-261-2164", + "cell": "(888)-556-7285", + "picture": { + "large": "https://randomuser.me/api/portraits/men/4.jpg", + "medium": "https://randomuser.me/api/portraits/med/men/4.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/men/4.jpg" + }, + "nat": "US" + }, + { + "userId": "3", + "gender": "female", + "cart": [], + "name": { + "title": "mrs", + "first": "natalia", + "last": "ramos" + }, + "location": { + "street": "7934 avenida de salamanca", + "city": "madrid", + "state": "aragón", + "postcode": 43314 + }, + "email": "natalia.ramos@example.com", + "login": { + "username": "greenlion235", + "password": "waters", + "salt": "w10ZFgoO", + "md5": "19f6fb510c58be44b2df1816d88b739d", + "sha1": "18e545aee27156ee6be35596631353a14ee03007", + "sha256": "2b23b25939ece8ba943fe9abcb3074105867c267d122081a2bc6322f935ac809" + }, + "dob": "1947-03-05 15:23:07", + "registered": "2004-07-19 02:44:19", + "phone": "903-556-986", + "cell": "696-867-013", + "picture": { + "large": "https://randomuser.me/api/portraits/women/54.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/54.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/54.jpg" + }, + "nat": "ES" + } +] diff --git a/app/server.js b/app/server.js index 5201d84d..1fcfb38b 100644 --- a/app/server.js +++ b/app/server.js @@ -3,29 +3,249 @@ const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const swaggerUi = require('swagger-ui-express'); const YAML = require('yamljs'); -const swaggerDocument = YAML.load('./swagger.yaml'); // Replace './swagger.yaml' with the path to your Swagger file +const swaggerDocument = YAML.load('./swagger-updated.yaml'); const app = express(); +const fs = require('fs'); app.use(bodyParser.json()); -// Importing the data from JSON files -const users = require('../initial-data/users.json'); -const brands = require('../initial-data/brands.json'); -const products = require('../initial-data/products.json'); +/** + * Accesses data from JSON files and parses them. + */ +const products = JSON.parse(fs.readFileSync('api/products.json', 'utf-8')); +const users = JSON.parse(fs.readFileSync('api/users.json', 'utf-8')); +const brands = JSON.parse(fs.readFileSync('api/brands.json', 'utf-8')); -// Error handling +/** + * Error handling. + */ app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); }); -// Swagger +/** + * Uses swagger document. + */ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); -// Starting the server +/** + * Function that verifies authorization token. + */ +function verifyToken(req, res, next) { + const authorizationHeader = req.header('Authorization'); + + if (!authorizationHeader) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const token = authorizationHeader.replace('Bearer ', ''); + + jwt.verify(token, 'secretkey', (err, decoded) => { + if (err) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + req.username = decoded.username; + next(); + }); +} + +/** + * Define GET /api/brands endpoint. + */ +app.get('/api/brands', (req, res) => { + res.status(200).json(brands); + }); + +/** + * Define GET /api/brands/:brandId/products endpoint. + */ +app.get('/api/brands/:brandId/products', (req, res) => { + const brandId = req.params.brandId; + + + /** + * Filter products based on brandId. + */ + const productsFromBrandId = products.filter(product => product.brandId === brandId); + + res.status(200).json(productsFromBrandId); + }); + +/** + * Define GET /api/products endpoint. + */ +app.get('/api/products', (req, res) => { + res.status(200).json(products); + }); + +/** + * Define POST /api/login endpoint. + */ +app.post('/api/login', (req, res) => { + const { username, password } = req.body; + const secretKey = 'secretkey'; + const options = { expiresIn: '1h' }; + +/** + * Finds the user. + */ + const user = users.find(user => user.login.username === username && user.login.password === password); + + if (user) { + /** + * Generate and send a token if login is successful. + */ + const token = jwt.sign({ username: user.login.username }, secretKey, options); + console.log('Generated Token:', token); + res.status(200).json({ message: 'Login successful', token: token, user: user }); + } else { + res.status(401).json({ error: 'Invalid credentials' }); + } +}); + +/** + * Define GET api/me/cart endpoint. + */ +app.get('/api/me/cart', verifyToken, (req, res) => { + + const username = req.username; + + /** + * Find the user based on the username in the users array. + */ + const user = users.find((user) => user.login.username === username); + + if (user) { + /** + * Return the user/cart data. + */ + res.status(200).json(user.cart); + } else { + res.status(404).json({ error: 'User not found' }); + } +}); + +/** + * Define POST /api/me/cart endpoint. + */ +app.post('/api/me/cart', verifyToken, (req, res) => { + + const username = req.username; + + /** + * Find the user based on the username. + */ + const user = users.find(user => user.login.username === username); + + if (user) { + res.status(200).json(user.cart); + } else { + res.status(404).json({ error: 'User not found' }); + } + }); + +/** + * Define POST /api/me/cart/:productId endpoint. + */ +app.post('/api/me/cart/:productId', verifyToken, (req, res) => { + + const username = req.username; + + /** + * Find the user based on the username. + */ + const user = users.find(user => user.login.username === username); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const productId = req.params.productId; + const productToAdd = products.find(product => product.id === productId); + + if (!productToAdd) { + return res.status(404).json({ error: 'Product not found' }); + } + + /** + * Check if the product is already in the user's cart. + */ + const existingCartItem = user.cart.find(item => item.product.id === productId); + + if (existingCartItem) { + /** + * If the product is already in the cart, increment the quantity. + */ + existingCartItem.quantity++; + } else { + /** + * If the product is not in the cart, add it with a quantity of 1. + */ + user.cart.push({ + product: productToAdd, + quantity: 1, + }); + } + /** + * Return the updated cart. + */ + res.status(200).json(user.cart); +}); + +/** + * Define DELETE /api/me/cart/:productId endpoint. + */ +app.delete('/api/me/cart/:productId', verifyToken, (req, res) => { + + const username = req.username; + + /** + * Find the user based on the username. + */ + const user = users.find(user => user.login.username === username); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const productId = req.params.productId; + + /** + * Find the product in the user's cart. + */ + const existingCartItem = user.cart.find(item => item.product.id === productId); + + if (existingCartItem) { + /** + * If the product is in the cart, remove it. + */ + user.cart = user.cart.filter(item => item.product.id !== productId); + + /** + * Return the updated cart. + */ + res.status(200).json(user.cart); + } else { + /** + * If the product is not in the cart, respond with an error. + */ + res.status(404).json({ error: 'Product not found in the users cart' }); + } +}); + + +/** + * Starting the server. + */ const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); + + module.exports = app; + + diff --git a/initial-data/brands.json b/initial-data/brands.json deleted file mode 100644 index 139062c9..00000000 --- a/initial-data/brands.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "id": "1", - "name" : "Oakley" - }, - { - "id": "2", - "name" : "Ray Ban" - }, - { - "id": "3", - "name" : "Levi's" - }, - { - "id": "4", - "name" : "DKNY" - }, - { - "id": "5", - "name" : "Burberry" - } -] \ No newline at end of file diff --git a/initial-data/products.json b/initial-data/products.json deleted file mode 100644 index eb85b83f..00000000 --- a/initial-data/products.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "id": "1", - "categoryId": "1", - "name": "Superglasses", - "description": "The best glasses in the world", - "price":150, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "2", - "categoryId": "1", - "name": "Black Sunglasses", - "description": "The best glasses in the world", - "price":100, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "3", - "categoryId": "1", - "name": "Brown Sunglasses", - "description": "The best glasses in the world", - "price":50, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "4", - "categoryId": "2", - "name": "Better glasses", - "description": "The best glasses in the world", - "price":1500, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "5", - "categoryId": "2", - "name": "Glasses", - "description": "The most normal glasses in the world", - "price":150, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "6", - "categoryId": "3", - "name": "glas", - "description": "Pretty awful glasses", - "price":10, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "7", - "categoryId": "3", - "name": "QDogs Glasses", - "description": "They bark", - "price":1500, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "8", - "categoryId": "4", - "name": "Coke cans", - "description": "The thickest glasses in the world", - "price":110, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "9", - "categoryId": "4", - "name": "Sugar", - "description": "The sweetest glasses in the world", - "price":125, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "10", - "categoryId": "5", - "name": "Peanut Butter", - "description": "The stickiest glasses in the world", - "price":103, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "11", - "categoryId": "5", - "name": "Habanero", - "description": "The spiciest glasses in the world", - "price":153, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - } -] \ No newline at end of file diff --git a/initial-data/users.json b/initial-data/users.json deleted file mode 100644 index 9a6231e8..00000000 --- a/initial-data/users.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "gender": "female", - "cart":[], - "name": { - "title": "mrs", - "first": "susanna", - "last": "richards" - }, - "location": { - "street": "2343 herbert road", - "city": "duleek", - "state": "donegal", - "postcode": 38567 - }, - "email": "susanna.richards@example.com", - "login": { - "username": "yellowleopard753", - "password": "jonjon", - "salt": "eNuMvema", - "md5": "a8be2a69c8c91684588f4e1a29442dd7", - "sha1": "f9a60bbf8b550c10712e470d713784c3ba78a68e", - "sha256": "4dca9535634c102fbadbe62dc5b37cd608f9f3ced9aacf42a5669e5a312690a0" - }, - "dob": "1954-10-09 10:47:17", - "registered": "2003-08-03 01:12:24", - "phone": "031-941-6700", - "cell": "081-032-7884", - "picture": { - "large": "https://randomuser.me/api/portraits/women/55.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/55.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/55.jpg" - }, - "nat": "IE" - }, - { - "gender": "male", - "cart":[], - "name": { - "title": "mr", - "first": "salvador", - "last": "jordan" - }, - "location": { - "street": "9849 valley view ln", - "city": "burkburnett", - "state": "delaware", - "postcode": 78623 - }, - "email": "salvador.jordan@example.com", - "login": { - "username": "lazywolf342", - "password": "tucker", - "salt": "oSngghny", - "md5": "30079fb24f447efc355585fcd4d97494", - "sha1": "dbeb2d0155dad0de0ab9bbe21c062e260a61d741", - "sha256": "4f9416fa89bfd251e07da3ca0aed4d077a011d6ef7d6ed75e1d439c96d75d2b2" - }, - "dob": "1955-07-28 22:32:14", - "registered": "2010-01-10 06:52:31", - "phone": "(944)-261-2164", - "cell": "(888)-556-7285", - "picture": { - "large": "https://randomuser.me/api/portraits/men/4.jpg", - "medium": "https://randomuser.me/api/portraits/med/men/4.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/men/4.jpg" - }, - "nat": "US" - }, - { - "gender": "female", - "cart":[], - "name": { - "title": "mrs", - "first": "natalia", - "last": "ramos" - }, - "location": { - "street": "7934 avenida de salamanca", - "city": "madrid", - "state": "aragón", - "postcode": 43314 - }, - "email": "natalia.ramos@example.com", - "login": { - "username": "greenlion235", - "password": "waters", - "salt": "w10ZFgoO", - "md5": "19f6fb510c58be44b2df1816d88b739d", - "sha1": "18e545aee27156ee6be35596631353a14ee03007", - "sha256": "2b23b25939ece8ba943fe9abcb3074105867c267d122081a2bc6322f935ac809" - }, - "dob": "1947-03-05 15:23:07", - "registered": "2004-07-19 02:44:19", - "phone": "903-556-986", - "cell": "696-867-013", - "picture": { - "large": "https://randomuser.me/api/portraits/women/54.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/54.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/54.jpg" - }, - "nat": "ES" - } -] \ No newline at end of file diff --git a/rand-token.d.ts b/rand-token.d.ts new file mode 100644 index 00000000..50ab2b4b --- /dev/null +++ b/rand-token.d.ts @@ -0,0 +1,6 @@ +declare module "rand-token" { + function uid(length: number): string; + // Add other declarations if needed + + export { uid }; +} diff --git a/swagger-updated.yaml b/swagger-updated.yaml new file mode 100644 index 00000000..ecf03a18 --- /dev/null +++ b/swagger-updated.yaml @@ -0,0 +1,313 @@ +swagger: "2.0" +info: + version: "1.0.0" + title: "E-Commerce API" + description: "API for managing brands, products, and user cart" +host: "localhost:3000" +schemes: + - "http" +basePath: "/api" +produces: + - "application/json" +tags: + - name: brands + description: Brands carried in store + - name: products + description: Products carried in store + - name: user + description: Everything about the user including the user's cart + +paths: + /api/brands: + get: + tags: + - brands + summary: View all brands carried in store + description: "" + operationId: viewBrands + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: Brands + description: Array of brands in store + schema: + $ref: "#/definitions/Brands" + responses: + "200": + description: successful + "404": + description: unsuccessful + /api/brands/{brandId}/products: + get: + tags: + - brands + summary: View all products carried by a specific brand + description: Retrieve a list of products carried by a specific brand. + operationId: viewBrandProducts + parameters: + - name: brandId + in: path + description: ID of the brand to retrieve products for + required: true + type: string + responses: + "200": + description: successful + schema: + type: array + items: + $ref: "#/definitions/Product" + + /api/products: + get: + tags: + - products + summary: View all products in the store + description: "" + operationId: viewProducts + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: successful + schema: + type: array # Corrected to an array type + items: + $ref: "#/definitions/Product" + + /api/login: + post: + tags: + - user + summary: Logs user into the system + description: "" + operationId: loginUser + produces: + - application/json + - application/xml + parameters: + - name: username + in: query + description: The user name for login + required: true + type: string + - name: password + in: query + description: The password for login in clear text + required: true + type: string + responses: + "200": + description: successful operation + headers: + X-Expires-After: + type: string + format: date-time + X-Rate-Limit: + type: integer + format: int32 + schema: + type: string + "400": + description: Invalid username/password supplied + /api/me/cart: + get: + tags: + - user + summary: Get user's cart + description: Retrieve the contents of the user's cart + operationId: getUserCart + produces: + - application/json + parameters: + - name: username + in: query + description: The username to get the cart for + required: true + type: string + responses: + "200": + description: successful operation + schema: + $ref: "#/definitions/UserCart" + "404": + description: User not found + post: + tags: + - user + summary: Create the user's cart and add a product + description: Add a product to the user's cart or create the cart if it doesn't exist (requires authentication) + operationId: addUserCart + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + description: Product object to add to the cart + required: true + schema: + $ref: "#/definitions/Product" + responses: + "200": + description: successful operation + "401": + description: Unauthorized (user not logged in) + /api/me/cart/{productId}: + delete: + tags: + - user + summary: Remove a product from the user's cart + description: Remove a product from the user's cart (requires authentication) + operationId: removeFromUserCart + consumes: + - application/json + produces: + - application/json + parameters: + - name: productId + in: path + description: ID of the product to be removed + required: true + type: integer + format: int64 + responses: + "200": + description: successful operation + "401": + description: Unauthorized (user not logged in) + "404": + description: User or product not found + post: + tags: + - user + summary: Add a product to the user's cart + description: Add a product to the user's cart (requires authentication) + operationId: addToUserCart + consumes: + - application/json + produces: + - application/json + parameters: + - name: productId + in: path + description: ID of the product to be added + required: true + type: integer + format: int64 + responses: + "200": + description: successful operation + "401": + description: Unauthorized (user not logged in) + "404": + description: User or product not found + +definitions: + Product: + type: object + required: + - name + - photoUrl + properties: + id: + type: integer + format: int64 + price: + type: integer + example: 120 + brand: + $ref: "#/definitions/Brand" + style: + type: string + example: Beach + description: + type: string + example: These sunglasses are good for the beach! + color: + type: string + example: Blue + photoUrl: + type: string + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + inventory: + type: integer + example: 2 + Brand: + type: object + properties: + id: + type: string + example: "1" + name: + type: string + example: Oakley + Brands: + type: array + items: + $ref: "#/definitions/Brand" + Store: + type: array + items: + $ref: "#/definitions/Product" + User: + type: object + properties: + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + loggedIn: + type: boolean + default: false + description: Indicates whether the user is logged in or not + xml: + name: User + CartItem: + type: object + properties: + productId: + type: integer + format: int64 + description: ID of the product in the cart + quantity: + type: integer + description: Quantity of the product in the cart + xml: + name: CartItem + UserCart: + type: object + properties: + username: + type: string + description: The username associated with the cart + items: + type: array + items: + $ref: "#/definitions/CartItem" + description: List of items in the cart + xml: + name: UserCart diff --git a/swagger.yaml b/swagger.yaml deleted file mode 100644 index 1c2eb22f..00000000 --- a/swagger.yaml +++ /dev/null @@ -1,11 +0,0 @@ -swagger: '2.0' -info: - version: '1.0.0' - title: 'E-Commerce API' - description: 'API for managing brands, products, and user cart' -host: 'localhost:3000' -schemes: - - 'http' -basePath: '/api' -produces: - - 'application/json' diff --git a/test/server.test.js b/test/server.test.js index 7ff14c8f..3f4a5df5 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -1,14 +1,387 @@ const chai = require('chai'); const chaiHttp = require('chai-http'); -const server = require('../app/server'); // Adjust the path as needed +const server = require('../app/server'); const should = chai.should(); +const { expect } = require('chai'); chai.use(chaiHttp); -// TODO: Write tests for the server -describe('Brands', () => {}); +/** + * Tests for brands + */ +describe('Brands', () => { + describe('GET /api/brands', () => { + it('should get all brands in the store', (done) => { + chai.request(server) + .get('/api/brands') + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + + done(); + }); + }); + }); -describe('Login', () => {}); + describe('GET /api/brands/{brandId}/products', () => { + it('should get all products specific to brand ID', (done) => { + + const brandId = "3"; -describe('Cart', () => {}); + chai.request(server) + .get(`/api/brands/${brandId}/products`) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + /** + * Check if all returned products have the correct brandId. + */ + res.body.forEach(product => { + product.should.have.property('brandId').equal(brandId); + }); + + done(); + }); + }); + }); + + +/** + * Tests for products. + */ +describe('Products', () => { + describe('GET /api/products', () => { + it('should get all products in the store', (done) => { + chai.request(server) + .get('/api/products') + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + + done(); + }); + }); + }); + + +/** + * Tests for user. + */ +describe('User', () => { + describe('POST /api/login', () => { + it('should log the user into the system with valid credentials', (done) => { + const userLoginInfo = { + username: 'yellowleopard753', + password: 'jonjon', + }; + + chai.request(server) + .post('/api/login') + .send(userLoginInfo) + .end((err, res) => { + // + /** + * Successful login. + */ + res.should.have.status(200); + res.body.should.have.property('token'); + + done(); + }); + }); + + it('should return 401 with invalid credentials', (done) => { + const invalidUserLoginInfo = { + username: 'invalidUsername', + password: 'invalidPassword', + }; + + chai.request(server) + .post('/api/login') + .send(invalidUserLoginInfo) + .end((err, res) => { + /** + * Unsuccessful login. + */ + res.should.have.status(401); + + done(); + }); + }); + }); + + describe('GET /api/me/cart', () => { + it('should get the contents of the users cart', (done) => { + /** + * Perform a login to get a valid token. + */ + chai.request(server) + .post('/api/login') + .set('content-type', 'application/json') + .send({ + username: 'yellowleopard753', + password: 'jonjon', + }) + .end((loginErr, loginRes) => { + /** + * Check if login was successful. + */ + loginRes.should.have.status(200); + loginRes.body.should.have.property('token'); + + const token = loginRes.body.token; + /** + * Perform a GET request to /api/me/cart with the obtained token. + */ + chai.request(server) + .get('/api/me/cart') + .set('Authorization', `Bearer ${token}`) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + + res.body.forEach((cartItem) => { + cartItem.should.have.property('product'); + cartItem.should.have.property('quantity'); + }); + + done(); + }); + }); + }); + }); + describe('POST /api/me/cart', () => { + it('should post the users current cart', (done) => { + + /** + * Replace token with console logged token in the terminal, expires every hour. + */ + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InllbGxvd2xlb3BhcmQ3NTMiLCJpYXQiOjE3MDg3MDkzOTAsImV4cCI6MTcwODcxMjk5MH0.GLKt_gkAxLCtmzqXS6q6rPFBtxu5ZIH3Uc3kKfQIQfk'; + + chai.request(server) + .post('/api/me/cart') + .set('Authorization', `Bearer ${token}`) + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body).to.be.an('array'); + + done(); + }); + }); + + it('should return 401 for unauthorized access', (done) => { + chai.request(server) + .post('/api/me/cart') + .end((err, res) => { + expect(res).to.have.status(401); + + done(); + }); + }); + }); + + describe('POST /api/me/cart/:productId', () => { + it('should add a product to the users cart', (done) => { + + /** + * Test product to be added to cart. + */ + const productToAdd = { + id: '10' + }; + + /** + * Login to get token. + */ + chai.request(server) + .post('/api/login') + .set('content-type', 'application/json') + .send({ + username: 'yellowleopard753', + password: 'jonjon' + }) + .end((loginErr, loginRes) => { + /** + * Check if login was successful. + */ + loginRes.should.have.status(200); + loginRes.body.should.have.property('token'); + + const token = loginRes.body.token; + + /** + * Perform a POST request to /api/me/cart/:productId with the obtained token. + */ + chai.request(server) + /** + * Hardcoded ID as '10' for testing. + */ + .post(`/api/me/cart/10`) + .set('Authorization', `Bearer ${token}`) + .send() + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + + /** + * Check if the product has been added to the cart. + */ + const addedProduct = res.body.find(item => item.product.id === productToAdd.id); + expect(addedProduct).to.exist; + expect(addedProduct.quantity).to.equal(1); + + done(); + }); + }); + }); + + it('should return 401 for unauthorized access', (done) => { + chai.request(server) + /** + * Assuming productId is '10'. + */ + .post('/api/me/cart/10') + .end((err, res) => { + expect(res).to.have.status(401); + + done(); + }); + }); + + it('should return 404 for non-existing product', (done) => { + /** + * Perform a login to get a valid token. + */ + chai.request(server) + .post('/api/login') + .set('content-type', 'application/json') + .send({ + username: 'yellowleopard753', + password: 'jonjon', + }) + .end((loginErr, loginRes) => { + /** + * Check if login was successful. + */ + loginRes.should.have.status(200); + loginRes.body.should.have.property('token'); + + const token = loginRes.body.token; + /** + * Perform a POST request to /api/me/cart/:productId with the obtained token for a non-existing product. + */ + chai.request(server) + .post('/api/me/cart/nonExistingProductId') + .set('Authorization', `Bearer ${token}`) + .send() + .end((err, res) => { + expect(res).to.have.status(404); + + done(); + }); + }); + }); + }); + + + describe('DELETE /api/me/cart/:productId', () => { + it('should remove a product from the users cart', (done) => { + + /** + * Product to be removed from cart. + */ + const productIdToRemove = '10'; + + + /** + * Login to get token. + */ + chai.request(server) + .post('/api/login') + .set('content-type', 'application/json') + .send({ + username: 'yellowleopard753', + password: 'jonjon' + }) + .end((loginErr, loginRes) => { + /** + * Check if login was successful. + */ + loginRes.should.have.status(200); + loginRes.body.should.have.property('token'); + + const token = loginRes.body.token; + + /** + * Perform a DELETE request to /api/me/cart/:productId with the obtained token. + */ + chai.request(server) + .delete(`/api/me/cart/${productIdToRemove}`) + .set('Authorization', `Bearer ${token}`) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an('array'); + + /** + * Check if the product has been removed from the cart. + */ + const removedProduct = res.body.find(item => item.product.id === productIdToRemove); + expect(removedProduct).to.not.exist; + + done(); + }); + }); + }); + + it('should return 401 for unauthorized access', (done) => { + chai.request(server) + /** + * Assuming productId is '10'. + */ + .delete('/api/me/cart/10') // + + .end((err, res) => { + expect(res).to.have.status(401); + + done(); + }); + }); + + it('should return 404 for non-existing product', (done) => { + /** + * Perform a login to get a valid token. + */ + chai.request(server) + .post('/api/login') + .set('content-type', 'application/json') + .send({ + username: 'yellowleopard753', + password: 'jonjon', + }) + .end((loginErr, loginRes) => { + /** + * Check if login was successful. + */ + loginRes.should.have.status(200); + loginRes.body.should.have.property('token'); + + const token = loginRes.body.token; + + /** + * Perform a DELETE request to /api/me/cart/:productId with the obtained token for a non-existing product. + */ + chai.request(server) + .delete('/api/me/cart/nonExistingProductId') + .set('Authorization', `Bearer ${token}`) + .end((err, res) => { + expect(res).to.have.status(404); + + done(); + }); + }); + }); + }); +}); +}); +});