diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 00000000..bb13dfc4 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/package-lock.json b/package-lock.json index c86872bf..bbc0ca34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -64,6 +64,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1472,10 +1473,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -1541,7 +1543,6 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, - "peer": true, "engines": { "node": ">= 18" } @@ -1570,7 +1571,6 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0", "universal-user-agent": "^7.0.2" @@ -1584,7 +1584,6 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, - "peer": true, "dependencies": { "@octokit/request": "^9.0.0", "@octokit/types": "^13.0.0", @@ -1598,8 +1597,7 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "2.21.3", @@ -1661,7 +1659,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, - "peer": true, "dependencies": { "@octokit/endpoint": "^10.0.0", "@octokit/request-error": "^6.0.1", @@ -1677,7 +1674,6 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", "dev": true, - "peer": true, "dependencies": { "@octokit/types": "^13.0.0" }, @@ -1865,7 +1861,6 @@ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dev": true, - "peer": true, "dependencies": { "@octokit/openapi-types": "^22.2.0" } @@ -2220,6 +2215,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2658,8 +2654,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true, - "peer": true + "dev": true }, "node_modules/body-parser": { "version": "1.20.2", @@ -2738,6 +2733,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3580,6 +3576,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3635,6 +3632,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3725,6 +3723,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3802,6 +3801,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -3852,6 +3852,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz", "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -3875,6 +3876,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -5420,6 +5422,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7842,6 +7845,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8976,8 +8980,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/universalify": { "version": "2.0.1", diff --git a/package.json b/package.json index 9b8e432d..b5470cee 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", diff --git a/src/createServer.js b/src/createServer.js index 5b405372..51c0a3f3 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,11 +1,227 @@ 'use strict'; -// const express = require('express'); +const express = require('express'); function createServer() { - // Use express to create a server - // Add a routes to the server - // Return the server (express app) + const app = express(); + const users = []; + const expenses = []; + let nextUserId = 1; + let nextExpenseId = 1; + + app.use(express.json()); + + const getUserById = (id) => users.find((user) => user.id === id); + const getExpenseById = (id) => expenses.find((expense) => expense.id === id); + + app.post('/users', (req, res) => { + const { name } = req.body; + + if (!name) { + return res.status(400).json({ message: 'Name is required' }); + } + + const user = { + id: nextUserId, + name, + }; + + nextUserId += 1; + users.push(user); + + return res.status(201).json(user); + }); + + app.get('/users', (req, res) => { + res.json(users); + }); + + app.get('/users/:userId', (req, res) => { + const userId = Number(req.params.userId); + const user = getUserById(userId); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + return res.json(user); + }); + + const updateUser = (req, res) => { + const userId = Number(req.params.userId); + const user = getUserById(userId); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const { name } = req.body; + + if (!name) { + return res.status(400).json({ message: 'Name is required' }); + } + + user.name = name; + + return res.json(user); + }; + + app.patch('/users/:userId', updateUser); + app.put('/users/:userId', updateUser); + + app.delete('/users/:userId', (req, res) => { + const userId = Number(req.params.userId); + const userIndex = users.findIndex((user) => user.id === userId); + + if (userIndex === -1) { + return res.status(404).json({ message: 'User not found' }); + } + + users.splice(userIndex, 1); + + return res.status(204).send(); + }); + + app.post('/expenses', (req, res) => { + const { userId, spentAt, title, amount, category, note } = req.body; + + if ( + userId === undefined || + !spentAt || + !title || + amount === undefined || + !category + ) { + return res.status(400).json({ message: 'Required fields are missing' }); + } + + const user = getUserById(Number(userId)); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const expense = { + id: nextExpenseId, + userId: Number(userId), + spentAt, + title, + amount, + category, + note, + }; + + nextExpenseId += 1; + expenses.push(expense); + + return res.status(201).json(expense); + }); + + app.get('/expenses', (req, res) => { + const { userId, from, to, categories } = req.query; + let filteredExpenses = [...expenses]; + + if (userId !== undefined) { + filteredExpenses = filteredExpenses.filter( + (expense) => expense.userId === Number(userId), + ); + } + + if (from !== undefined) { + const fromTime = new Date(from).getTime(); + + filteredExpenses = filteredExpenses.filter( + (expense) => new Date(expense.spentAt).getTime() >= fromTime, + ); + } + + if (to !== undefined) { + const toTime = new Date(to).getTime(); + + filteredExpenses = filteredExpenses.filter( + (expense) => new Date(expense.spentAt).getTime() <= toTime, + ); + } + + if (categories !== undefined) { + const categoriesList = String(categories).split(','); + + filteredExpenses = filteredExpenses.filter((expense) => { + return categoriesList.includes(expense.category); + }); + } + + res.json(filteredExpenses); + }); + + app.get('/expenses/:expenseId', (req, res) => { + const expenseId = Number(req.params.expenseId); + const expense = getExpenseById(expenseId); + + if (!expense) { + return res.status(404).json({ message: 'Expense not found' }); + } + + return res.json(expense); + }); + + app.patch('/expenses/:expenseId', (req, res) => { + const expenseId = Number(req.params.expenseId); + const expense = getExpenseById(expenseId); + + if (!expense) { + return res.status(404).json({ message: 'Expense not found' }); + } + + if (req.body.userId !== undefined) { + const user = getUserById(Number(req.body.userId)); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + expense.userId = Number(req.body.userId); + } + + if (req.body.spentAt !== undefined) { + expense.spentAt = req.body.spentAt; + } + + if (req.body.title !== undefined) { + expense.title = req.body.title; + } + + if (req.body.amount !== undefined) { + expense.amount = req.body.amount; + } + + if (req.body.category !== undefined) { + expense.category = req.body.category; + } + + if (req.body.note !== undefined) { + expense.note = req.body.note; + } + + return res.json(expense); + }); + + app.delete('/expenses/:expenseId', (req, res) => { + const expenseId = Number(req.params.expenseId); + const expenseIndex = expenses.findIndex( + (expense) => expense.id === expenseId, + ); + + if (expenseIndex === -1) { + return res.status(404).json({ message: 'Expense not found' }); + } + + expenses.splice(expenseIndex, 1); + + return res.status(204).send(); + }); + + return app; } module.exports = { diff --git a/tests/expense.test.js b/tests/expense.test.js index b5bcbf1d..3053a481 100644 --- a/tests/expense.test.js +++ b/tests/expense.test.js @@ -43,11 +43,11 @@ describe('Expense', () => { ); }); - it('should return 400 if name is not provided', async () => { + it('should return 400 if required fields are not provided', async () => { await api.post('/expenses').send({}).expect(400); }); - it('should return 400 if user not found', async () => { + it('should return 404 if user not found', async () => { const expenseData = { userId: 1, spentAt: '2022-10-19T11:01:43.462Z', @@ -57,7 +57,7 @@ describe('Expense', () => { note: 'I need a new laptop', }; - await api.post('/expenses').send(expenseData).expect(400); + await api.post('/expenses').send(expenseData).expect(404); }); }); diff --git a/tests/user.test.js b/tests/user.test.js index 2902f5ae..7099d535 100644 --- a/tests/user.test.js +++ b/tests/user.test.js @@ -107,7 +107,7 @@ describe('User', () => { describe('updateUser', () => { it('should return 404 if user does not exist', async () => { await api - .put('/users/1') + .patch('/users/1') .send({ name: 'John Doe', })