├── 05-http-server-streams ├── 01-file-server-get │ ├── files │ │ └── .gitkeep │ ├── test │ │ ├── fixtures │ │ │ ├── index.js │ │ │ ├── big.png │ │ │ └── small.png │ │ └── server.test.js │ ├── index.js │ ├── README.md │ └── server.js ├── 03-file-server-delete │ ├── files │ │ └── .gitkeep │ ├── test │ │ ├── fixtures │ │ │ ├── index.js │ │ │ ├── big.png │ │ │ └── small.png │ │ └── server.test.js │ ├── index.js │ ├── README.md │ └── server.js └── 02-file-server-post │ ├── test │ ├── fixtures │ │ ├── index.js │ │ ├── big.png │ │ └── small.png │ └── server.test.js │ ├── index.js │ ├── LimitExceededError.js │ ├── LimitSizeStream.js │ ├── README.md │ └── server.js ├── config.yml ├── .gitignore ├── 02-event-loop └── 01-events-order │ ├── solution.txt │ ├── index.js │ ├── test │ └── index.test.js │ └── README.md ├── 06-koajs └── 01-chat-app │ ├── index.js │ ├── README.md │ ├── app.js │ ├── public │ └── index.html │ └── test │ └── chat.test.js ├── 07-mongodb-mongoose ├── 02-rest-api │ ├── index.js │ ├── config.js │ ├── controllers │ │ ├── categories.js │ │ └── products.js │ ├── libs │ │ └── connection.js │ ├── models │ │ ├── Category.js │ │ └── Product.js │ ├── app.js │ ├── README.md │ └── test │ │ └── app.test.js └── 01-schema-model │ ├── config.js │ ├── libs │ └── connection.js │ ├── models │ ├── Category.js │ └── Product.js │ ├── README.md │ └── test │ └── Models.test.js ├── README.md ├── .eslintrc.yml ├── .travis.yml ├── 01-intro └── 01-sum │ ├── sum.js │ ├── README.md │ └── test │ └── sum.test.js ├── 03-streams ├── 01-limit-size-stream │ ├── LimitExceededError.js │ ├── LimitSizeStream.js │ ├── README.md │ └── test │ │ └── LimitSizeStream.test.js └── 02-line-split-stream │ ├── LineSplitStream.js │ ├── README.md │ └── test │ └── LineSplitStream.test.js ├── data └── users.json ├── reporter.js ├── local.js └── package.json /05-http-server-streams/01-file-server-get/files/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/files/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | git: 3 | copy: 4 | - data/** 5 | - '*.*' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | package-lock.json 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/test/fixtures/index.js: -------------------------------------------------------------------------------- 1 | console.log('hello world'); 2 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/test/fixtures/index.js: -------------------------------------------------------------------------------- 1 | console.log('hello world'); 2 | -------------------------------------------------------------------------------- /02-event-loop/01-events-order/solution.txt: -------------------------------------------------------------------------------- 1 | James 2 | Richard 3 | John 4 | Robert 5 | James 6 | Michael -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/test/fixtures/index.js: -------------------------------------------------------------------------------- 1 | console.log('hello world'); 2 | -------------------------------------------------------------------------------- /06-koajs/01-chat-app/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | 3 | app.listen(3000, () => { 4 | console.log('App is running on http://localhost:3000'); 5 | }); 6 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | 3 | app.listen(3000, () => { 4 | console.log('App is running on http://localhost:3000'); 5 | }); 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasks 2 | 3 | 4 | Задачник для курса по Node.JS на сайте https://learn.javascript.ru/courses/nodejs. 5 | 6 | Содержит теоретические материалы и практические задания. 7 | -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/test/fixtures/big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ushev-s/nodejs-20200914-2_ushev-s90/HEAD/05-http-server-streams/01-file-server-get/test/fixtures/big.png -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server'); 2 | 3 | server.listen(3000, () => { 4 | console.log('Server is listening on http://localhost:3000'); 5 | }); 6 | -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/test/fixtures/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ushev-s/nodejs-20200914-2_ushev-s90/HEAD/05-http-server-streams/01-file-server-get/test/fixtures/small.png -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server'); 2 | 3 | server.listen(3000, () => { 4 | console.log('Server is listening on http://localhost:3000'); 5 | }); 6 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/test/fixtures/big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ushev-s/nodejs-20200914-2_ushev-s90/HEAD/05-http-server-streams/02-file-server-post/test/fixtures/big.png -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/test/fixtures/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ushev-s/nodejs-20200914-2_ushev-s90/HEAD/05-http-server-streams/02-file-server-post/test/fixtures/small.png -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/test/fixtures/big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ushev-s/nodejs-20200914-2_ushev-s90/HEAD/05-http-server-streams/03-file-server-delete/test/fixtures/big.png -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server'); 2 | 3 | server.listen(3000, () => { 4 | console.log('Server is listening on http://localhost:3000'); 5 | }); 6 | -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/test/fixtures/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ushev-s/nodejs-20200914-2_ushev-s90/HEAD/05-http-server-streams/03-file-server-delete/test/fixtures/small.png -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodb: { 3 | uri: (process.env.NODE_ENV === 'test') ? 4 | 'mongodb://localhost/6-module-2-task' : 5 | 'mongodb://localhost/any-shop', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/01-schema-model/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodb: { 3 | uri: (process.env.NODE_ENV === 'test' ? 4 | 'mongodb://localhost/6-module-1-task' : 5 | 'mongodb://localhost/any-shop'), 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | commonjs: true 3 | es6: true 4 | node: true 5 | extends: google 6 | parserOptions: 7 | ecmaVersion: 2018 8 | rules: 9 | max-len: 10 | - error 11 | - code: 100 12 | require-jsdoc: 13 | - off 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14.4.0 4 | 5 | services: 6 | - mongodb 7 | 8 | script: npm test --silent 9 | 10 | notifications: 11 | webhooks: 12 | - https://learn.javascript.ru/taskbook/travis/notifications 13 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/controllers/categories.js: -------------------------------------------------------------------------------- 1 | const Category = require('../models/Category'); 2 | 3 | module.exports.categoryList = async function categoryList(ctx, next) { 4 | const categories = await Category.find({}); 5 | ctx.body = { categories }; 6 | }; 7 | -------------------------------------------------------------------------------- /01-intro/01-sum/sum.js: -------------------------------------------------------------------------------- 1 | function sum(a, b) { 2 | if (typeof a !== 'number' || typeof b !== 'number') { 3 | throw new TypeError( 4 | 'Incorrect Type. You have to pass numbers into arguments' 5 | ); 6 | } else { 7 | return a + b; 8 | } 9 | } 10 | 11 | module.exports = sum; 12 | -------------------------------------------------------------------------------- /03-streams/01-limit-size-stream/LimitExceededError.js: -------------------------------------------------------------------------------- 1 | class LimitExceededError extends Error { 2 | constructor() { 3 | super('Limit has been exceeded.'); 4 | 5 | this.name = this.constructor.name; 6 | Error.captureStackTrace(this, this.constructor); 7 | 8 | this.code = 'LIMIT_EXCEEDED'; 9 | } 10 | } 11 | 12 | module.exports = LimitExceededError; 13 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/LimitExceededError.js: -------------------------------------------------------------------------------- 1 | class LimitExceededError extends Error { 2 | constructor() { 3 | super('Limit has been exceeded.'); 4 | 5 | this.name = this.constructor.name; 6 | Error.captureStackTrace(this, this.constructor); 7 | 8 | this.code = 'LIMIT_EXCEEDED'; 9 | } 10 | } 11 | 12 | module.exports = LimitExceededError; 13 | -------------------------------------------------------------------------------- /data/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "email": "user1@mail.com", 5 | "displayName": "user1", 6 | "password": "123123" 7 | }, 8 | { 9 | "email": "user2@mail.com", 10 | "displayName": "user2", 11 | "password": "123123" 12 | }, 13 | { 14 | "email": "user3@mail.com", 15 | "displayName": "user3", 16 | "password": "123123" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /01-intro/01-sum/README.md: -------------------------------------------------------------------------------- 1 | # Сумма двух чисел 2 | 3 | Это первая задача курса, которая поможет разобраться с тем, как устроен задачник, а также с тем, как 4 | оформлять решение и отправлять его на проверку. 5 | 6 | В модуле `sum.js` необходимо реализовать функцию, которая принимает два аргумента и возвращает их 7 | сумму. Если аргументы не являются числами — функция должна бросать ошибку TypeError. 8 | 9 | ```js 10 | 11 | sum(1, 2); // 3 12 | sum('1', []); // TypeError 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/libs/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const beautifyUnique = require('mongoose-beautiful-unique-validation'); 3 | const config = require('../config'); 4 | 5 | mongoose.set('useNewUrlParser', true); 6 | mongoose.set('useFindAndModify', false); 7 | mongoose.set('useCreateIndex', true); 8 | 9 | mongoose.set('debug', false); 10 | 11 | mongoose.plugin(beautifyUnique); 12 | 13 | module.exports = mongoose.createConnection(config.mongodb.uri); 14 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/01-schema-model/libs/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const beautifyUnique = require('mongoose-beautiful-unique-validation'); 3 | const config = require('../config'); 4 | 5 | mongoose.set('useNewUrlParser', true); 6 | mongoose.set('useFindAndModify', false); 7 | mongoose.set('useCreateIndex', true); 8 | 9 | mongoose.set('debug', false); 10 | 11 | mongoose.plugin(beautifyUnique); 12 | 13 | module.exports = mongoose.createConnection(config.mongodb.uri); 14 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/01-schema-model/models/Category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const connection = require('../libs/connection'); 3 | 4 | const subCategorySchema = new mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true 8 | } 9 | }); 10 | 11 | const categorySchema = new mongoose.Schema({ 12 | title: { 13 | type: String, 14 | required: true, 15 | }, 16 | subcategories: [subCategorySchema], 17 | }); 18 | 19 | module.exports = connection.model('Category', categorySchema); 20 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/models/Category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const connection = require('../libs/connection'); 3 | 4 | const subCategorySchema = new mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true, 8 | }, 9 | }); 10 | 11 | const categorySchema = new mongoose.Schema({ 12 | title: { 13 | type: String, 14 | required: true, 15 | }, 16 | 17 | subcategories: [subCategorySchema], 18 | }); 19 | 20 | module.exports = connection.model('Category', categorySchema); 21 | -------------------------------------------------------------------------------- /01-intro/01-sum/test/sum.test.js: -------------------------------------------------------------------------------- 1 | const sum = require('../sum'); 2 | const expect = require('chai').expect; 3 | 4 | describe('intro/sum', () => { 5 | describe('функция sum', () => { 6 | it('складывает два числа', () => { 7 | expect(sum(1, 2)).to.equal(3); 8 | }); 9 | 10 | [ 11 | ['1', []], 12 | ['1', '1'] 13 | ].forEach(([a, b]) => { 14 | it('бросает TypeError, если аргументы - не числа', () => { 15 | expect(() => sum(a, b)).throw(TypeError); 16 | }); 17 | }) 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /02-event-loop/01-events-order/index.js: -------------------------------------------------------------------------------- 1 | const intervalId = setInterval(() => { 2 | console.log('James'); 3 | }, 10); 4 | 5 | setTimeout(() => { 6 | const promise = new Promise((resolve) => { 7 | console.log('Richard'); 8 | resolve('Robert'); 9 | }); 10 | 11 | promise 12 | .then((value) => { 13 | console.log(value); 14 | 15 | setTimeout(() => { 16 | console.log('Michael'); 17 | 18 | clearInterval(intervalId); 19 | }, 10); 20 | }); 21 | 22 | console.log('John'); 23 | }, 10); 24 | -------------------------------------------------------------------------------- /03-streams/01-limit-size-stream/LimitSizeStream.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | const LimitExceededError = require('./LimitExceededError'); 3 | 4 | class LimitSizeStream extends stream.Transform { 5 | constructor(options) { 6 | super(options); 7 | this.limit = options.limit; 8 | this.totalSize = 0; 9 | } 10 | 11 | _transform(chunk, encoding, callback) { 12 | this.totalSize += chunk.length; 13 | 14 | if (this.limit) { 15 | if (+this.limit < this.totalSize) { 16 | callback(new LimitExceededError()); 17 | return; 18 | } 19 | } 20 | 21 | this.push(chunk); 22 | callback(); 23 | } 24 | } 25 | 26 | module.exports = LimitSizeStream; 27 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/01-schema-model/models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const connection = require('../libs/connection'); 4 | 5 | const productSchema = new Schema({ 6 | title: { 7 | type: String, 8 | required: true, 9 | }, 10 | description: { 11 | type: String, 12 | required: true, 13 | }, 14 | price: { 15 | type: Number, 16 | required: true, 17 | }, 18 | category: { 19 | type: Schema.Types.ObjectId, 20 | ref: 'Category', 21 | required: true, 22 | }, 23 | subcategory: { 24 | type: Schema.Types.ObjectId, 25 | required: true, 26 | }, 27 | images: [String] 28 | }); 29 | 30 | module.exports = connection.model('Product', productSchema); 31 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const connection = require('../libs/connection'); 3 | 4 | const productSchema = new mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true, 8 | }, 9 | 10 | description: { 11 | type: String, 12 | required: true, 13 | }, 14 | 15 | price: { 16 | type: Number, 17 | required: true, 18 | }, 19 | 20 | category: { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: 'Category', 23 | required: true, 24 | }, 25 | 26 | subcategory: { 27 | type: mongoose.Schema.Types.ObjectId, 28 | required: true, 29 | }, 30 | 31 | images: [String], 32 | 33 | }); 34 | 35 | module.exports = connection.model('Product', productSchema); 36 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/LimitSizeStream.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | const LimitExceededError = require('./LimitExceededError'); 3 | 4 | class LimitSizeStream extends stream.Transform { 5 | constructor(options) { 6 | super(options); 7 | 8 | this.limit = options.limit; 9 | this.size = 0; 10 | this.isObjectMode = !!options.readableObjectMode; 11 | } 12 | 13 | _transform(chunk, encoding, callback) { 14 | if (this.isObjectMode) { 15 | this.size += 1; 16 | } else { 17 | this.size += chunk.length; 18 | } 19 | 20 | if (this.size > this.limit) { 21 | callback(new LimitExceededError()); 22 | } else { 23 | callback(null, chunk); 24 | } 25 | } 26 | } 27 | 28 | module.exports = LimitSizeStream; 29 | -------------------------------------------------------------------------------- /03-streams/02-line-split-stream/LineSplitStream.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | const os = require('os'); 3 | 4 | class LineSplitStream extends stream.Transform { 5 | constructor(options) { 6 | super(options); 7 | this.line = ''; 8 | } 9 | 10 | _transform(chunk, encoding, callback) { 11 | const rows = chunk.toString().split(`${os.EOL}`); 12 | for (let i = 0; i < rows.length; i++) { 13 | if (i === 0) { 14 | this.line += rows[i]; 15 | } else { 16 | this.push(this.line); 17 | this.line = rows[i]; 18 | } 19 | } 20 | callback(); 21 | } 22 | 23 | _flush(callback) { 24 | if (this.line) { 25 | this.push(this.line); 26 | } 27 | callback(); 28 | } 29 | } 30 | 31 | module.exports = LineSplitStream; 32 | -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/README.md: -------------------------------------------------------------------------------- 1 | # Файловый сервер - удаление файла 2 | 3 | В данной задаче вам необходимо будет реализовать http-сервер, который по запросу пользователя будет 4 | удалять файл с диска. 5 | 6 | - `DELETE /[filename]` - удаление файла из папки `files`. 7 | - При успешном удалении сервер должен вернуть ответ со статусом `200` 8 | - Если файла на диске нет - сервер должен вернуть ошибку `404`. 9 | - Вложенные папки не поддерживаются, при запросе вида `/dir1/dir2/filename` - ошибка `400`. 10 | 11 | При любых других ошибках сервер должен, по возможности, возвращать ошибку `500`. 12 | 13 | Для выполнения запросов можно воспользоваться программой `postman`. Проверить правильность 14 | реализации можно также с помощью тестов. 15 | 16 | Запуск приложения осуществляется с помощью команды `node index.js`. 17 | -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/README.md: -------------------------------------------------------------------------------- 1 | # Файловый сервер - отдача файла 2 | 3 | В данной задаче вам необходимо будет реализовать http-сервер, который по запросу пользователя 4 | отдавать файл с диска. 5 | 6 | - `GET /[filename]` - получание файла из папки `files` (поддерживаются любые типы файлов). 7 | - Если файла на диске нет - сервер должен вернуть ошибку `404`. 8 | - Вложенные папки не поддерживаются, при запросе вида `/dir1/dir2/filename` - ошибка `400`. 9 | - При обрыве соединения необходимо завершить работу стрима. 10 | 11 | При любых ошибках сервер должен, по возможности, возвращать ошибку `500`. 12 | 13 | Для выполнения запросов можно воспользоваться программой `postman`. Проверить правильность 14 | реализации можно также с помощью тестов. 15 | 16 | Запуск приложения осуществляется с помощью команды `node index.js`. 17 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const Router = require('koa-router'); 3 | const {productsBySubcategory, productList, productById} = require('./controllers/products'); 4 | const {categoryList} = require('./controllers/categories'); 5 | 6 | const app = new Koa(); 7 | 8 | app.use(async (ctx, next) => { 9 | try { 10 | await next(); 11 | } catch (err) { 12 | if (err.status) { 13 | ctx.status = err.status; 14 | ctx.body = {error: err.message}; 15 | } else { 16 | console.error(err); 17 | ctx.status = 500; 18 | ctx.body = {error: 'Internal server error'}; 19 | } 20 | } 21 | }); 22 | 23 | const router = new Router({prefix: '/api'}); 24 | 25 | router.get('/categories', categoryList); 26 | router.get('/products', productsBySubcategory, productList); 27 | router.get('/products/:id', productById); 28 | 29 | app.use(router.routes()); 30 | 31 | module.exports = app; 32 | -------------------------------------------------------------------------------- /06-koajs/01-chat-app/README.md: -------------------------------------------------------------------------------- 1 | # Чат на Koa.js 2 | 3 | В этом задании вам необходимо будет реализовать простой чат на koa.js, используя технологию 4 | [long polling](http://learn.javascript.ru/xhr-longpoll). 5 | 6 | 7 | Суть технологии достаточно проста: клиент делает запрос за получением новых сообщений, а сервер этот 8 | запрос "подвешивает" до тех пор, пока новое сообщение не будет отправлено. После получения сообщения 9 | клиент вновь делает запрос и точно также ждет, пока сервер не ответит. 10 | 11 | 12 | Клиентская часть для браузера уже реализована и находится в статических файлах в папке public, 13 | необходимо реализовать лишь обработчики для двух типов запросов: 14 | 15 | - `GET /subscribe` - получение новых сообщений 16 | - `POST /publish` - отправка сообщения 17 | 18 | 19 | Подсказка: "задержать" запрос в Koa.js очень просто - надо лишь создать новый объект Promise и 20 | через `await` в обработчике запроса ждать его. 21 | 22 | Запуск приложения осуществляется с помощью команды `node index.js`. 23 | -------------------------------------------------------------------------------- /02-event-loop/01-events-order/test/index.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const {EOL} = require('os'); 3 | const {execSync} = require('child_process'); 4 | const path = require('path'); 5 | const expect = require('chai').expect; 6 | 7 | describe('event-loop/events-order', () => { 8 | describe('Порядок вывода сообщений', () => { 9 | it('файл с решением должен быть в папке с задачей', () => { 10 | const isExists = fs.existsSync(path.join(__dirname, '../solution.txt')); 11 | expect(isExists).to.be.true; 12 | }); 13 | 14 | it('порядок вывода совпадает', () => { 15 | const solution = fs.readFileSync( 16 | path.join(__dirname, '../solution.txt'), 17 | { 18 | encoding: 'utf-8', 19 | } 20 | ).replace(/\r\n|\r|\n/g, EOL); 21 | 22 | const output = execSync(`node ${path.join(__dirname, '../index.js')}`, { 23 | encoding: 'utf-8', 24 | }).replace(/\r\n|\r|\n/g, EOL); 25 | 26 | expect(solution.trim()).to.equal(output.trim()); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /02-event-loop/01-events-order/README.md: -------------------------------------------------------------------------------- 1 | # Порядок вывода сообщений в консоль 2 | 3 | В данной задаче вам необходимо (не запуская код) определить, в каком порядке будут выводиться 4 | сообщения в консоль. Не расстраивайтесь если вывод не соответствует вашим ожиданиям, попробуйте 5 | понять, в чем вы допустили просчет. 6 | 7 | В качестве решения создайте текстовый файл `solution.txt` в папке с заданием, в котором каждый вывод 8 | начинается с новой строки. 9 | 10 | Например, 11 | ```text 12 | James 13 | Michael 14 | ``` 15 | 16 | Код для запуска находится в файле `index.js`: 17 | ```js 18 | const intervalId = setInterval(() => { 19 | console.log('James'); 20 | }, 10); 21 | 22 | setTimeout(() => { 23 | const promise = new Promise((resolve) => { 24 | console.log('Richard'); 25 | resolve('Robert'); 26 | }); 27 | 28 | promise 29 | .then((value) => { 30 | console.log(value); 31 | 32 | setTimeout(() => { 33 | console.log('Michael'); 34 | 35 | clearInterval(intervalId); 36 | }, 10); 37 | }); 38 | 39 | console.log('John'); 40 | }, 10); 41 | ``` 42 | -------------------------------------------------------------------------------- /reporter.js: -------------------------------------------------------------------------------- 1 | const mocha = require('mocha'); 2 | 3 | function Reporter(runner) { 4 | mocha.reporters.Base.call(this, runner); 5 | let passes = 0; 6 | let failures = 0; 7 | const tests = []; 8 | 9 | runner.on('pass', function(test) { 10 | passes++; 11 | tests.push({ 12 | description: test.title, 13 | success: true, 14 | suite: test.parent.titlePath(), 15 | time: test.duration, 16 | }); 17 | }); 18 | 19 | runner.on('fail', function(test, err) { 20 | failures++; 21 | tests.push({ 22 | description: test.title, 23 | success: false, 24 | suite: test.parent.titlePath(), 25 | time: test.duration, 26 | }); 27 | }); 28 | 29 | runner.on('end', function() { 30 | console.log(JSON.stringify({ 31 | result: { 32 | mocha: tests, 33 | }, 34 | summary: { 35 | success: passes, 36 | failed: failures, 37 | }, 38 | })); 39 | }); 40 | } 41 | 42 | module.exports = Reporter; 43 | 44 | // To have this reporter "extend" a built-in reporter uncomment the following line: 45 | // mocha.utils.inherits(MyReporter, mocha.reporters.Spec); 46 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/README.md: -------------------------------------------------------------------------------- 1 | # Файловый сервер - запись файла 2 | 3 | В данной задаче вам необходимо будет реализовать http-сервер, который по запросу пользователя 4 | создавать файл на диске и записывать туда тело запроса. 5 | 6 | - `POST /[filename]` - создание нового файла в папке `files` и запись в него тела запроса. 7 | - Если файл уже есть на диске - сервер должен вернуть ошибку `409`. 8 | - Максимальный размер загружаемого файла не должен превышать 1МБ, при превышении лимита - ошибка 9 | `413`. 10 | - Если в процессе загрузки файла на сервер произошел обрыв соединения — созданный файл с диска 11 | надо удалять. 12 | - Вложенные папки не поддерживаются, при запросе вида `/dir1/dir2/filename` - ошибка `400`. 13 | 14 | При любых ошибках сервер должен, по возможности, возвращать ошибку `500`. 15 | 16 | Для ограничения размера загружаемого файла можно воспользоваться классом `LimitSizeStream` из 2-го 17 | модуля. 18 | 19 | Для выполнения запросов можно воспользоваться программой `postman`. Проверить правильность 20 | реализации можно также с помощью тестов. 21 | 22 | Запуск приложения осуществляется с помощью команды `node index.js`. 23 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/controllers/products.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | const Product = require('../models/Product'); 3 | 4 | module.exports.productsBySubcategory = async function productsBySubcategory( 5 | ctx, 6 | next 7 | ) { 8 | const { subcategory } = ctx.request.query; 9 | if (!subcategory) { 10 | ctx.response.status = 400; 11 | ctx.body = 'Please, specify subcategory param'; 12 | } else { 13 | const products = await Product.find({ subcategory }); 14 | ctx.response.status = 200; 15 | ctx.body = { products }; 16 | } 17 | }; 18 | 19 | module.exports.productList = async function productList(ctx, next) { 20 | const products = await Product.find({}); 21 | ctx.response.status = 200; 22 | ctx.body = { products }; 23 | }; 24 | 25 | module.exports.productById = async function productById(ctx, next) { 26 | const { id } = ctx.params; 27 | if (!mongoose.Types.ObjectId.isValid(id)) { 28 | ctx.response.status = 400; 29 | ctx.body = 'Invalid product ID'; 30 | } else { 31 | const product = await Product.findById(id); 32 | if (!product) { 33 | ctx.response.status = 404; 34 | ctx.body = 'Product not found'; 35 | } else { 36 | ctx.response.status = 200; 37 | ctx.body = { product }; 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/server.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const http = require('http'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const server = new http.Server(); 7 | 8 | server.on('request', (req, res) => { 9 | try { 10 | let pathname = url.parse(req.url).pathname.slice(1); 11 | if (pathname.indexOf('/') >= 0) { 12 | res.statusCode = 400; 13 | res.end('Your have to send only a filename!'); 14 | return; 15 | } 16 | const filepath = path.join(__dirname, 'files', pathname); 17 | 18 | switch (req.method) { 19 | case 'GET': 20 | const readStream = fs.createReadStream(filepath).on('error', (err) => { 21 | if (err.code === 'ENOENT') { 22 | res.statusCode = 404; 23 | res.end('No such file or directory'); 24 | } else { 25 | res.statusCode = 500; 26 | res.end('Internal Server Error'); 27 | } 28 | }); 29 | readStream.pipe(res); 30 | break; 31 | 32 | default: 33 | res.statusCode = 501; 34 | res.end('Not implemented'); 35 | } 36 | } catch (err) { 37 | console.log(err); 38 | res.statusCode = 500; 39 | res.end('Internal Server Error'); 40 | } 41 | }); 42 | 43 | module.exports = server; 44 | -------------------------------------------------------------------------------- /06-koajs/01-chat-app/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Koa = require('koa'); 3 | const app = new Koa(); 4 | const subscribers = {} 5 | 6 | app.use(require('koa-static')(path.join(__dirname, 'public'))); 7 | app.use(require('koa-bodyparser')()); 8 | 9 | const Router = require('koa-router'); 10 | const router = new Router(); 11 | 12 | router.get('/subscribe', async (ctx, next) => { 13 | const id = Math.random(); 14 | subscribers[id] = ctx.res; 15 | await new Promise((resolve, reject) => { 16 | ctx.res.on('close', () => { 17 | delete subscribers[id]; 18 | resolve(`Client id=${id} disconnected`); 19 | }).on('finish', () =>{ 20 | resolve('Message received'); 21 | }) 22 | }) 23 | }); 24 | 25 | router.post('/publish', async (ctx, next) => { 26 | const message = ctx.request.body.message; 27 | if (message) { 28 | for (const id in subscribers) { 29 | //console.log(`your message: ${message} delivered to id: ${id}`); 30 | const res = subscribers[id]; 31 | res.statusCode = 200; 32 | res.end(message); 33 | } 34 | 35 | ctx.response.status = 200; 36 | ctx.response.body = 'You have successfully send message'; 37 | } else { 38 | ctx.response.status = 400; 39 | ctx.response.body = 'Your message is empty!'; 40 | } 41 | }); 42 | 43 | app.use(router.routes()); 44 | 45 | module.exports = app; 46 | -------------------------------------------------------------------------------- /03-streams/02-line-split-stream/README.md: -------------------------------------------------------------------------------- 1 | # Стрим, разбивающий текст на строки 2 | 3 | При работе с большими объемами текстовых данных при помощи стримов очень часто возникает задача 4 | разбить данные по какому-то ключу или признаку. Например, при обработке большого CSV файла 5 | необходимо разбить данные по символу переноса строки, для того, чтобы потоково обработать каждый 6 | элемент файла. 7 | 8 | В текущей задаче вам потребуется написать `LineSplitStream` - стрим, который принимает текстовые 9 | данные, а отдает их же, но уже построчно, например: 10 | 11 | ```js 12 | 13 | const LineSplitStream = require('./LineSplitStream'); 14 | const os = require('os'); 15 | 16 | const lines = new LineSplitStream({ 17 | encoding: 'utf-8', 18 | }); 19 | 20 | function onData(line) { 21 | console.log(line); 22 | } 23 | 24 | lines.on('data', onData); 25 | 26 | lines.write(`первая строка${os.EOL}вторая строка${os.EOL}третья строка`); 27 | 28 | lines.end(); 29 | 30 | ``` 31 | 32 | В результате выполнения кода выше функция `onData` будет вызвана три раза. 33 | 34 | Символ переноса строки отличаются для разных операционных систем, в Windows - это `\r\n`, в Mac и 35 | Linux - `\n`. Для того, чтобы наш код работал корректно во всех операционных системах можно 36 | воспользоваться свойством `os.EOL` модуля `os`, которое будет содержать корректный символ для той 37 | ОС, на которой запущена программа. 38 | -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/server.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const http = require('http'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const server = new http.Server(); 7 | 8 | server.on('request', async (req, res) => { 9 | try { 10 | let pathname = url.parse(req.url).pathname.slice(1); 11 | if (pathname.indexOf('/') >= 0) { 12 | res.statusCode = 400; 13 | res.end('Your have to send only a filename!'); 14 | return; 15 | } 16 | 17 | const filepath = path.join(__dirname, 'files', pathname); 18 | 19 | if (req.method === 'DELETE') { 20 | fs.unlink(filepath, (error) => { 21 | if (error) { 22 | if (error.code === 'ENOENT') { 23 | res.statusCode = 404; 24 | res.end('File does not exist'); 25 | } else { 26 | console.log(error); 27 | res.statusCode = 500; 28 | res.end(error.message); 29 | } 30 | } else { 31 | res.statusCode = 200; 32 | res.end('File successfully deleted'); 33 | } 34 | }); 35 | } else { 36 | res.statusCode = 501; 37 | res.end(`Requested ${req.method} method is not implemented`); 38 | } 39 | } catch (err) { 40 | console.log(err); 41 | res.statusCode = 500; 42 | res.end('Internal Server Error'); 43 | } 44 | }); 45 | 46 | module.exports = server; 47 | -------------------------------------------------------------------------------- /local.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const Mocha = require('mocha'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const glob = require('glob'); 7 | 8 | require('colors'); 9 | 10 | const _module = process.argv[2]; 11 | const task = process.argv[3]; 12 | 13 | if (!_module) { 14 | return console.error( 15 | `${'Не указан модуль с задачей. Например:'.red.bold} 16 | ${'npm run test:local 01-intro 01-sum'.yellow}` 17 | ); 18 | } 19 | 20 | if (!task) { 21 | return console.error( 22 | `${'Не указана задача. Например:'.red.bold} 23 | ${'npm run test:local 01-intro 01-sum'.yellow}` 24 | ); 25 | } 26 | 27 | const mocha = new Mocha({ 28 | reporter: 'spec', 29 | useColors: true, 30 | }); 31 | 32 | 33 | const testDir = path.join(__dirname, _module, task, 'test'); 34 | 35 | if (!fs.existsSync(testDir)) { 36 | return console.error( 37 | `${'Задача'.red.bold} ${`${_module}/${task}`.yellow} ${'отсутствует. Проверьте правильность команды.'.red.bold}` 38 | ); 39 | } 40 | 41 | const files = glob.sync(`${testDir}/**/*.test.js`); 42 | 43 | if (!files.length) { 44 | return console.error( 45 | `${'К задаче'.red.bold} ${`${_module}/${task}`.yellow} ${'отсутствуют тесты'.red.bold}` 46 | ); 47 | } 48 | 49 | files.forEach(file => { 50 | mocha.addFile(file); 51 | }); 52 | 53 | // Run the tests. 54 | mocha.run(function(failures) { 55 | process.exitCode = failures ? 1 : 0; 56 | }); 57 | -------------------------------------------------------------------------------- /06-koajs/01-chat-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Добро пожаловать в чат!

9 | 10 |
11 | 12 | 13 |
14 | 15 | 16 | 17 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /03-streams/02-line-split-stream/test/LineSplitStream.test.js: -------------------------------------------------------------------------------- 1 | const LineSplitStream = require('../LineSplitStream'); 2 | const expect = require('chai').expect; 3 | const sinon = require('sinon'); 4 | const os = require('os'); 5 | 6 | describe('streams/line-split-stream', () => { 7 | describe('LineSplitStream', () => { 8 | it('стрим разбивает данные по строкам', (done) => { 9 | const lines = new LineSplitStream({encoding: 'utf-8'}); 10 | 11 | const onData = sinon.spy(); 12 | 13 | lines.on('data', onData); 14 | lines.on('end', () => { 15 | expect(onData.calledTwice, 'событие data должно быть вызвано 2 раза').to.be.true; 16 | expect(onData.firstCall.args[0]).to.equal('a'); 17 | expect(onData.secondCall.args[0]).to.equal('b'); 18 | 19 | done(); 20 | }); 21 | 22 | lines.write(`a${os.EOL}b`); 23 | lines.end(); 24 | }); 25 | 26 | it('стрим корректно передает данные даже если чанк не завершается переводом строки', (done) => { 27 | const lines = new LineSplitStream({encoding: 'utf-8'}); 28 | 29 | const onData = sinon.spy(); 30 | 31 | lines.on('data', onData); 32 | lines.on('end', () => { 33 | expect(onData.calledThrice, 'событие data должно быть вызвано 3 раза').to.be.true; 34 | expect(onData.firstCall.args[0]).to.equal('ab'); 35 | expect(onData.secondCall.args[0]).to.equal('cd'); 36 | expect(onData.thirdCall.args[0]).to.equal('ef'); 37 | 38 | done(); 39 | }); 40 | 41 | lines.write('a'); 42 | lines.write(`b${os.EOL}c`); 43 | lines.write(`d${os.EOL}e`); 44 | lines.write('f'); 45 | 46 | lines.end(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /03-streams/01-limit-size-stream/README.md: -------------------------------------------------------------------------------- 1 | # Стрим с лимитом передачи данных 2 | 3 | Основное преимущество стримов заключается в том, что мы не работаем со всеми данными сразу и 4 | экономим таким образом ресурсы компьютера. Однако иногда это может также стать и недостатком - т.к. 5 | мы не знаем в процессе работы сколько данных мы уже обработали. 6 | На практике часто возникают задачи посчитать количество передаваемых данных, например, чтобы 7 | ограничить размер загружаемого пользователем на сервер файла. 8 | 9 | В данной задаче вам необходимо реализовать класс `LimitSizeStream`, который будет подсчитывать 10 | количество переданной через него информации и бросать ошибку если ее объем превысит допустимое 11 | значение. 12 | 13 | Класс является наследником `stream.Transform` и принимает параметр `limit`, который и является 14 | максимальным размером передаваемых данных в байтах. 15 | 16 | Таким образом, при включении этого стрима в цепочку он должен будет подсчитывать количество 17 | передаемых данных, а при превышении максимально допустимого значения выбрасывать ошибку 18 | `LimitExceededError`. Стрим не изменяет передаваемые данные, просто передавая их дальше. 19 | 20 | Для простоты поддерживать объектный режим стримов не нужно, однако вы можете это сделать 21 | опционально. 22 | 23 | Пример: 24 | ```js 25 | const LimitSizeStream = require('./LimitSizeStream'); 26 | const fs = require('fs'); 27 | 28 | const limitedStream = new LimitSizeStream({limit: 8}); // 8 байт 29 | const outStream = fs.createWriteStream('out.txt'); 30 | 31 | limitedStream.pipe(outStream); 32 | 33 | limitedStream.write('hello'); // 'hello' - это 5 байт, поэтому эта строчка целиком записана в файл 34 | 35 | setTimeout(() => { 36 | limitedStream.write('world'); // ошибка LimitExceeded! в файле осталось только hello 37 | }, 10); 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-basic-tasks-ru", 3 | "version": "1.0.0", 4 | "description": "Node.JS Course Materials", 5 | "scripts": { 6 | "test": "cross-env NODE_ENV=test mocha --no-warnings --reporter=reporter.js --ignore=node_modules/** **/*.test.js", 7 | "test:local": "cross-env NODE_ENV=test node --no-warnings local.js", 8 | "lint": "eslint --ignore-pattern 'public' --ignore-pattern '*.test.js' --ignore-pattern 'client' ." 9 | }, 10 | "keywords": [], 11 | "author": "Sergey Zelenov", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.8.4", 15 | "@babel/preset-react": "^7.8.3", 16 | "babel-loader": "^8.0.6", 17 | "chai": "^4.2.0", 18 | "colors": "^1.3.3", 19 | "eslint": "^7.0.0", 20 | "eslint-config-google": "^0.14.0", 21 | "glob": "^7.1.3", 22 | "html-webpack-plugin": "^4.3.0", 23 | "mocha": "^7.1.2", 24 | "webpack": "^4.41.6", 25 | "webpack-cli": "^3.3.11" 26 | }, 27 | "dependencies": { 28 | "axios": "^0.19.0", 29 | "cross-env": "^7.0.2", 30 | "fs-extra": "^9.0.0", 31 | "juice": "^6.0.0", 32 | "koa": "^2.7.0", 33 | "koa-bodyparser": "^4.2.1", 34 | "koa-favicon": "^2.0.1", 35 | "koa-logger": "^3.2.0", 36 | "koa-passport": "^4.1.3", 37 | "koa-router": "^8.0.8", 38 | "koa-static": "^5.0.0", 39 | "lodash": "^4.17.11", 40 | "mongoose": "^5.5.2", 41 | "mongoose-beautiful-unique-validation": "^7.1.1", 42 | "nodemailer": "^6.1.1", 43 | "nodemailer-html-to-text": "^3.0.0", 44 | "nodemailer-smtp-transport": "^2.7.4", 45 | "nodemailer-stub-transport": "^1.1.0", 46 | "passport-facebook": "^3.0.0", 47 | "passport-github": "^1.1.0", 48 | "passport-local": "^1.0.0", 49 | "passport-vkontakte": "^0.3.2", 50 | "pug": "^2.0.3", 51 | "react": "^16.12.0", 52 | "react-dom": "^16.12.0", 53 | "react-router-dom": "^5.1.2", 54 | "sinon": "^9.0.2", 55 | "socket.io": "^2.2.0", 56 | "socket.io-client": "^2.2.0", 57 | "uuid": "^8.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /03-streams/01-limit-size-stream/test/LimitSizeStream.test.js: -------------------------------------------------------------------------------- 1 | const LimitSizeStream = require('../LimitSizeStream'); 2 | const LimitExceededError = require('../LimitExceededError'); 3 | const expect = require('chai').expect; 4 | const sinon = require('sinon'); 5 | 6 | describe('streams/limit-size-stream', () => { 7 | describe('LimitSizeStream', () => { 8 | it('стрим передает поступающие данные без изменений', (done) => { 9 | const limitStream = new LimitSizeStream({limit: 3, encoding: 'utf-8'}); 10 | 11 | const onData = sinon.spy(); 12 | 13 | limitStream.on('data', onData); 14 | limitStream.on('end', () => { 15 | expect(onData.calledTwice, 'событие \'data\' должно произойти 2 раза').to.be.true; 16 | expect(onData.firstCall.args[0], 17 | `при первом вызове события 'data' в обработчик должна быть передана строка 'a'`) 18 | .to.equal('a'); 19 | expect(onData.secondCall.args[0], 20 | `при втором вызове события 'data' в обработчик должна быть передана строка 'b'`) 21 | .to.equal('b'); 22 | done(); 23 | }); 24 | 25 | limitStream.write('a'); 26 | limitStream.write('b'); 27 | limitStream.end(); 28 | }); 29 | 30 | it('при превышении лимита выбрасывается ошибка', (done) => { 31 | const limitStream = new LimitSizeStream({limit: 2, encoding: 'utf-8'}); 32 | 33 | const onData = sinon.spy(); 34 | 35 | limitStream.on('data', onData); 36 | limitStream.on('error', (err) => { 37 | expect(err).to.be.instanceOf(LimitExceededError); 38 | expect(onData.calledTwice, `событие 'data' должно произойти только 2 раза`).to.be.true; 39 | expect(onData.firstCall.args[0], 40 | `при первом вызове события 'data' в обработчик должна быть передана строка 'a'`) 41 | .to.equal('a'); 42 | expect(onData.secondCall.args[0], 43 | `при втором вызове события 'data' в обработчик должна быть передана строка 'b'`) 44 | .to.equal('b'); 45 | 46 | done(); 47 | }); 48 | 49 | limitStream.write('a'); 50 | limitStream.write('b'); 51 | limitStream.write('c'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/server.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const http = require('http'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const LimitSizeStream = require('./LimitSizeStream'); 6 | 7 | const server = new http.Server(); 8 | 9 | server.on('request', async (req, res) => { 10 | try { 11 | let pathname = url.parse(req.url).pathname.slice(1); 12 | if (pathname.indexOf('/') >= 0) { 13 | res.statusCode = 400; 14 | res.end('Your have to send only a filename!'); 15 | return; 16 | } 17 | 18 | const filepath = path.join(__dirname, 'files', pathname); 19 | 20 | if (req.method === 'POST') { 21 | const writeStream = fs.createWriteStream(filepath, { flags: 'wx' }); 22 | const limitedStream = new LimitSizeStream({ limit: 1048576 }); //1 Mb 23 | 24 | // req.on('abort', () => { 25 | // fs.unlink(filepath, () => {}); 26 | // }); 27 | 28 | res.on('close', () => { 29 | if (!res.writableFinished) { 30 | fs.unlink(filepath, () => {}); 31 | } 32 | }); 33 | 34 | limitedStream.on('error', (err) => { 35 | if (err.code === 'LIMIT_EXCEEDED') { 36 | fs.unlink(filepath, () => {}); 37 | res.statusCode = 413; 38 | res.end('1 Mb Limit has been exceeded'); 39 | } else { 40 | res.statusCode = 500; 41 | res.end(err.message); 42 | } 43 | }); 44 | 45 | writeStream 46 | .on('error', (err) => { 47 | if (err.code === 'EEXIST') { 48 | res.statusCode = 409; 49 | res.end('File already exists'); 50 | } else { 51 | res.statusCode = 500; 52 | res.end(err.message); 53 | } 54 | }) 55 | .on('finish', (data) => { 56 | res.statusCode = 201; 57 | res.end('File saved successfully'); 58 | }); 59 | 60 | req.pipe(limitedStream).pipe(writeStream); 61 | } else { 62 | res.statusCode = 501; 63 | res.end(`Requested ${req.method} method is not implemented`); 64 | } 65 | } catch (err) { 66 | console.log(err); 67 | res.statusCode = 500; 68 | res.end('Internal Server Error'); 69 | } 70 | }); 71 | 72 | module.exports = server; 73 | -------------------------------------------------------------------------------- /06-koajs/01-chat-app/test/chat.test.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const app = require('../app'); 3 | const expect = require('chai').expect; 4 | 5 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 6 | 7 | describe('koajs/chat-app', () => { 8 | describe('тесты на чат', () => { 9 | let server; 10 | before((done) => { 11 | server = app.listen(3000, done); 12 | }); 13 | 14 | after((done) => { 15 | server.close(done); 16 | }); 17 | 18 | describe('POST /publish', () => { 19 | it('сообщение должно быть доставлено всем подписчикам', async () => { 20 | const message = 'text'; 21 | 22 | const subscribers = Promise.all([ 23 | axios.get('http://127.0.0.1:3000/subscribe', { 24 | timeout: 500, 25 | }), 26 | axios.get('http://127.0.0.1:3000/subscribe', { 27 | timeout: 500, 28 | }), 29 | ]); 30 | 31 | await sleep(50); 32 | 33 | await axios.post('http://127.0.0.1:3000/publish', { 34 | message, 35 | }); 36 | 37 | const messages = await subscribers; 38 | 39 | messages.forEach(response => { 40 | expect(response.data, 'каждый подписчик должен получить исходное сообщение').to.equal(message); 41 | }); 42 | }); 43 | 44 | it('если нет сообщения - запрос должен игнорироваться', async () => { 45 | const message = 'text'; 46 | 47 | const subscribers = Promise.all([ 48 | axios.get('http://127.0.0.1:3000/subscribe', { 49 | timeout: 500, 50 | }), 51 | axios.get('http://127.0.0.1:3000/subscribe', { 52 | timeout: 500, 53 | }), 54 | ]); 55 | 56 | await sleep(50); 57 | 58 | await axios.post('http://127.0.0.1:3000/publish', {}, { 59 | validateStatus: () => true, 60 | }); 61 | 62 | await sleep(50); 63 | 64 | await axios.post('http://127.0.0.1:3000/publish', { 65 | message, 66 | }); 67 | 68 | const messages = await subscribers; 69 | 70 | messages.forEach(response => { 71 | expect(response.data, 'каждый подписчик должен получить исходное сообщение').to.equal(message); 72 | }); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /05-http-server-streams/01-file-server-get/test/server.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../server'); 2 | const http = require('http'); 3 | const expect = require('chai').expect; 4 | const fse = require('fs-extra'); 5 | const path = require('path'); 6 | 7 | const filesFolder = path.resolve(__dirname, '../files'); 8 | const fixturesFolder = path.resolve(__dirname, './fixtures'); 9 | 10 | describe('http-server-streams/file-server-get', () => { 11 | describe('тесты на файловый сервер', () => { 12 | before((done) => { 13 | fse.emptyDirSync(filesFolder); 14 | server.listen(3001, done); 15 | }); 16 | 17 | after((done) => { 18 | fse.emptyDirSync(filesFolder); 19 | fse.writeFileSync(path.join(filesFolder, '.gitkeep'), ''); 20 | server.close(done); 21 | }); 22 | 23 | beforeEach(() => { 24 | fse.emptyDirSync(filesFolder); 25 | }); 26 | 27 | describe('GET', () => { 28 | it('файл отдается по запросу', (done) => { 29 | fse.copyFileSync( 30 | path.join(fixturesFolder, 'index.js'), 31 | path.join(filesFolder, 'index.js') 32 | ); 33 | 34 | const content = fse.readFileSync(path.join(filesFolder, 'index.js')); 35 | 36 | const request = http.request('http://localhost:3001/index.js', async (response) => { 37 | expect(response.statusCode, 'статус код ответа 200').to.equal(200); 38 | 39 | const body = []; 40 | for await (const chunk of response) { 41 | body.push(chunk); 42 | } 43 | 44 | expect( 45 | Buffer.concat(body).equals(content), 46 | 'ответ сервера - исходный файл index.js' 47 | ).to.be.true; 48 | done(); 49 | }); 50 | 51 | request.on('error', done); 52 | request.end(); 53 | }); 54 | 55 | it('если файла нет - отдается 404', (done) => { 56 | const request = http.request('http://localhost:3001/not_exists.png', (response) => { 57 | expect(response.statusCode, 'статус код ответа 404').to.equal(404); 58 | done(); 59 | }); 60 | 61 | request.on('error', done); 62 | request.end(); 63 | }); 64 | 65 | it('если путь вложенный - возвращается ошибка 400', (done) => { 66 | const request = http.request('http://localhost:3001/nested/path', (response) => { 67 | expect(response.statusCode, 'статус код ответа 400').to.equal(400); 68 | done(); 69 | }); 70 | 71 | request.on('error', done); 72 | request.end(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /05-http-server-streams/03-file-server-delete/test/server.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../server'); 2 | const http = require('http'); 3 | const expect = require('chai').expect; 4 | const fse = require('fs-extra'); 5 | const path = require('path'); 6 | 7 | const filesFolder = path.resolve(__dirname, '../files'); 8 | const fixturesFolder = path.resolve(__dirname, './fixtures'); 9 | 10 | describe('http-server-streams/file-server-delete', () => { 11 | describe('тесты на файловый сервер', () => { 12 | before((done) => { 13 | fse.emptyDirSync(filesFolder); 14 | server.listen(3001, done); 15 | }); 16 | after((done) => { 17 | fse.emptyDirSync(filesFolder); 18 | fse.writeFileSync(path.join(filesFolder, '.gitkeep'), ''); 19 | server.close(done); 20 | }); 21 | 22 | beforeEach(() => { 23 | fse.emptyDirSync(filesFolder); 24 | }); 25 | 26 | describe('DELETE', () => { 27 | it('файл должен удаляться', (done) => { 28 | fse.copyFileSync( 29 | path.join(fixturesFolder, 'small.png'), 30 | path.join(filesFolder, 'small.png'), 31 | ); 32 | 33 | const request = http.request( 34 | 'http://localhost:3001/small.png', 35 | {method: 'DELETE'}, 36 | (response) => { 37 | expect(response.statusCode).to.equal(200); 38 | 39 | setTimeout(() => { 40 | expect( 41 | fse.existsSync(path.join(filesFolder, 'small.png')), 42 | 'файл small.png не должен оставаться на диске' 43 | ).to.be.false; 44 | 45 | done(); 46 | }, 100); 47 | }); 48 | 49 | request.on('error', done); 50 | request.end(); 51 | }); 52 | 53 | it('если файла нет - ошибка 404', (done) => { 54 | const request = http.request( 55 | 'http://localhost:3001/small.png', 56 | {method: 'DELETE'}, 57 | (response) => { 58 | expect(response.statusCode).to.equal(404); 59 | done(); 60 | }); 61 | 62 | request.on('error', done); 63 | request.end(); 64 | }); 65 | 66 | it('если путь вложенный - возвращается ошибка 400', (done) => { 67 | const request = http.request( 68 | 'http://localhost:3001/nested/path', 69 | {method: 'DELETE'}, 70 | (response) => { 71 | expect(response.statusCode, 'статус код ответа 400').to.equal(400); 72 | done(); 73 | }); 74 | 75 | request.on('error', done); 76 | request.end(); 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/01-schema-model/README.md: -------------------------------------------------------------------------------- 1 | # Модели товара и категории 2 | 3 | В данной задаче вам необходимо реализовать модели товара и модель категории, используя Mongoose. Эти 4 | модели необходимы для того, чтобы мы на сервере далее смогли по запросу клиента возвращать как 5 | список категорий, так и список товаров по конкретной категории или подкатегории. Эти запросы будут 6 | выполняться на многим страницах нашего приложения. 7 | 8 | ## Категории 9 | 10 | Модель категории (`models/Category.js`) должна иметь следующий набор полей: 11 | 12 | - `title`, в этом поле будет находиться название категории, например, "Детские товары" или 13 | "Компьютерная техника". 14 | - строковое 15 | - обязательное 16 | - `subcategories`, массив подкатегорий. Каждая подкатегория имеет слеюуще поле: 17 | - `title`, в этом поле будет находиться название подкатегории. 18 | - строковое 19 | - обязательное 20 | 21 | Пример документа категории: 22 | ```js 23 | { 24 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fb4"), 25 | "title" : "Детские товары и игрушки", 26 | "subcategories" : [ 27 | { 28 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fba"), 29 | "title" : "Прогулки и детская комната" 30 | }, 31 | { 32 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fb9"), 33 | "title" : "Кормление и гигиена" 34 | }, 35 | { 36 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fb8"), 37 | "title" : "Игрушки и развлечения" 38 | }, 39 | { 40 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fb7"), 41 | "title" : "Активный отдых и улица" 42 | }, 43 | { 44 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fb6"), 45 | "title" : "Радиоуправляемые модели" 46 | }, 47 | { 48 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fb5"), 49 | "title" : "Школьные товары" 50 | } 51 | ] 52 | } 53 | ``` 54 | 55 | ## Товары 56 | 57 | Модель продукта (`models/Product.js`) должна иметь следующий набор полей: 58 | 59 | - `title`, в этом поле будет находиться название товара, например, "Коляска Adamex Barletta". 60 | - строковое 61 | - обязательное 62 | - `description`, описание товара. 63 | - строковое 64 | - обязательное 65 | - `price`, цена товара. 66 | - числовое 67 | - обязательное 68 | - `category`, идентификатор категории товара. 69 | - ObjectId (ref='Category') 70 | - обязательное 71 | - `subcategory`, идентификатор категории товара. 72 | - ObjectId 73 | - обязательное 74 | - `images`, массив ссылок изображений 75 | - массив строк 76 | 77 | Пример документа товара: 78 | ```js 79 | { 80 | "_id" : ObjectId("5d0ddb2a2b873c70961f6fe4"), 81 | "images" : [ 82 | "...", "..." 83 | ], 84 | "title" : "Коляска Adamex Barletta 2 in 1", 85 | "description" : "Универсальная модель, которая с легкостью заменит родителям сразу ...", 86 | "price" : 21230, 87 | "category" : ObjectId("5d0ddb2a2b873c70961f6fb4"), 88 | "subcategory" : ObjectId("5d0ddb2a2b873c70961f6fba") 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/README.md: -------------------------------------------------------------------------------- 1 | # Онлайн-магазин (товары и категории) 2 | 3 | В этом задании вам предстоит реализовать получение списка категорий, списка товаров по конкретной 4 | подкатегории, а также товара по его идентификатору. Это необходимая часть для работы нашего будущего 5 | интернет-магазина. 6 | 7 | В общем случае процесс обработки запроса разделяется на следующие этапы: 8 | 1. Обработка запроса (получение параметров, определение условий) 9 | 2. Получение необходимых данных из базы данных (в зависимости от условий, определенных на предыдущем 10 | этапе). 11 | 3. Преобразование данных из базы данных в тот вид, который необходимо вернуть. 12 | 13 | 14 | ## Получение списка категорий 15 | 16 | | Метод | Ссылка | Описание | Параметры | 17 | |-------|-----------------|----------------------------|-----------| 18 | | GET | /api/categories | Получение списка категорий | - | 19 | 20 | 21 | Пример ответа сервера: 22 | ```js 23 | { 24 | categories: [{ 25 | id: '5d208e631866a7366d831ffc', 26 | title: 'Category1', 27 | subcategories: [{ 28 | id: '5d208e631866a7366d831ffd', 29 | title: 'Subcategory1' 30 | }] 31 | }] 32 | } 33 | ``` 34 | 35 | ## Получение товаров 36 | 37 | ### По подкатегории 38 | 39 | Важный момент: запрос делается именно по идентификатору подкатегории, а не просто категории. 40 | 41 | | Метод | Ссылка | Описание | Параметры | 42 | |-------|-----------------|----------------------------|-------------| 43 | | GET | /api/products | Получение списка товаров | subcategory | 44 | 45 | 46 | Пример ответа сервера: 47 | ```js 48 | { 49 | products: [ 50 | { 51 | id: '5d20cf5bba02bff789f8e29f', 52 | title: 'Product1', 53 | images: ['image1', 'image2'], 54 | category: '5d20cf5bba02bff789f8e29d', 55 | subcategory: '5d20cf5bba02bff789f8e29e', 56 | price: 10, 57 | description: 'Description1' 58 | } 59 | ] 60 | } 61 | ``` 62 | 63 | ### По идентификатору 64 | 65 | Этот метод должен возвращать товар из базы по его идентификатору. Идентификатор должен быть валидным 66 | `ObjectId`, если переданный идентификатор невалидный - сервер должен вернуть ошибку со статусом 67 | `400`. Если товара с заданным идентификатором нет - сервер должен вернуть ошибку со статусом `404`. 68 | 69 | | Метод | Ссылка | Описание | Параметры | 70 | |-------|---------------------|--------------------------------------|-------------| 71 | | GET | /api/products/:id | Получение товара по идентификатору | | 72 | 73 | 74 | Пример ответа сервера: 75 | ```js 76 | { 77 | product: { 78 | id: '5d20d32d3a0676032a9a3174', 79 | title: 'Product1', 80 | images: [ 'image1' ], 81 | category: '5d20d32d3a0676032a9a3172', 82 | subcategory: '5d20d32d3a0676032a9a3173', 83 | price: 10, 84 | description: 'Description1' 85 | } 86 | } 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/01-schema-model/test/Models.test.js: -------------------------------------------------------------------------------- 1 | const Category = require('../models/Category'); 2 | const Product = require('../models/Product'); 3 | const connection = require('../libs/connection'); 4 | const mongoose = require('mongoose'); 5 | const expect = require('chai').expect; 6 | 7 | describe('mongodb-mongoose/schema-model', () => { 8 | after(() => { 9 | connection.close(); 10 | }); 11 | 12 | describe('модель категории', () => { 13 | it('у модели есть поля title и subcategories', () => { 14 | const fields = Category.schema.obj; 15 | 16 | expect(fields, 'у модели есть поле title').to.have.property('title'); 17 | expect(fields, 'у модели есть поле subcategories').to.have.property('subcategories'); 18 | }); 19 | 20 | it('поле title имеет правильную конфигурацию', () => { 21 | const title = Category.schema.obj.title; 22 | 23 | expect(title.type, 'title - строковое поле').to.eql(String); 24 | expect(title.required, 'title - обязательное поле').to.be.true; 25 | }); 26 | 27 | it('поле subcategories имеет правильную конфигурацию', () => { 28 | const subcategories = Category.schema.obj.subcategories; 29 | 30 | expect(subcategories, 'subcategories - массив').to.be.an('array'); 31 | 32 | const title = subcategories[0].obj.title; 33 | expect(title.type, 'title - строковое поле').to.eql(String); 34 | expect(title.required, 'title - обязательное поле').to.be.true; 35 | }); 36 | }); 37 | 38 | describe('модель товара', () => { 39 | it('у модели есть поля: title, description, price, category, subcategory и images', () => { 40 | const fields = Product.schema.obj; 41 | 42 | expect(fields, 'у модели есть поле title').to.have.property('title'); 43 | expect(fields, 'у модели есть поле description').to.have.property('description'); 44 | expect(fields, 'у модели есть поле price').to.have.property('price'); 45 | expect(fields, 'у модели есть поле category').to.have.property('category'); 46 | expect(fields, 'у модели есть поле subcategory').to.have.property('subcategory'); 47 | expect(fields, 'у модели есть поле images').to.have.property('images'); 48 | }); 49 | 50 | it('поле title имеет правильную конфигурацию', () => { 51 | const title = Product.schema.obj.title; 52 | 53 | expect(title.type, 'title - строковое поле').to.eql(String); 54 | expect(title.required, 'title - обязательное поле').to.be.true; 55 | }); 56 | 57 | it('поле description имеет правильную конфигурацию', () => { 58 | const description = Product.schema.obj.description; 59 | 60 | expect(description.type, 'description - строковое поле').to.eql(String); 61 | expect(description.required, 'description - обязательное поле').to.be.true; 62 | }); 63 | 64 | it('поле price имеет правильную конфигурацию', () => { 65 | const price = Product.schema.obj.price; 66 | 67 | expect(price.type, 'price - числовое поле').to.eql(Number); 68 | expect(price.required, 'price - обязательное поле').to.be.true; 69 | }); 70 | 71 | it('поле category имеет правильную конфигурацию', () => { 72 | const category = Product.schema.obj.category; 73 | 74 | expect(category.type, 'category - ObjectId').to.eql(mongoose.Schema.Types.ObjectId); 75 | expect(category.required, 'category - обязательное поле').to.be.true; 76 | }); 77 | 78 | it('поле subcategory имеет правильную конфигурацию', () => { 79 | const subcategory = Product.schema.obj.subcategory; 80 | 81 | expect(subcategory.type, 'subcategory - ObjectId').to.eql(mongoose.Schema.Types.ObjectId); 82 | expect(subcategory.required, 'subcategory - обязательное поле').to.be.true; 83 | }); 84 | 85 | it('поле images имеет правильную конфигурацию', () => { 86 | const images = Product.schema.obj.images; 87 | 88 | expect(images, 'images - массив').to.be.an('array'); 89 | expect(images[0], 'images - массив строк').to.eql(String); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /07-mongodb-mongoose/02-rest-api/test/app.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../app'); 2 | const Category = require('../models/Category'); 3 | const Product = require('../models/Product'); 4 | const connection = require('../libs/connection'); 5 | const mongoose = require('mongoose'); 6 | const ObjectId = mongoose.Types.ObjectId; 7 | const axios = require('axios'); 8 | const expect = require('chai').expect; 9 | 10 | const client = axios.create({ 11 | validateStatus: () => true, 12 | }); 13 | 14 | describe('mongodb-mongoose/rest-api', () => { 15 | describe('получение категорий и товаров', function() { 16 | let _server; 17 | let category; 18 | let product; 19 | 20 | before(async () => { 21 | await Category.deleteMany(); 22 | await Product.deleteMany(); 23 | 24 | category = await Category.create({ 25 | title: 'Category1', 26 | subcategories: [{ 27 | title: 'Subcategory1', 28 | }], 29 | }); 30 | 31 | product = await Product.create({ 32 | title: 'Product1', 33 | description: 'Description1', 34 | price: 10, 35 | category: category.id, 36 | subcategory: category.subcategories[0].id, 37 | images: ['image1'], 38 | }); 39 | 40 | await new Promise((resolve) => { 41 | _server = app.listen(3000, resolve); 42 | }); 43 | }); 44 | 45 | after(async () => { 46 | await Category.deleteMany(); 47 | await Product.deleteMany(); 48 | connection.close(); 49 | _server.close(); 50 | }); 51 | 52 | describe('категории', () => { 53 | it('по запросу должен возвращаться список категорий', async () => { 54 | const response = await client.get('http://localhost:3000/api/categories'); 55 | 56 | expect( 57 | response.data, 58 | 'ответ сервера содержит массив .categories' 59 | ).to.have.property('categories').that.is.an('array'); 60 | 61 | const categories = response.data.categories; 62 | 63 | expect( 64 | categories[0], 65 | 'категория содержит поля title, id и subcategories' 66 | ).to.have.keys(['title', 'id', 'subcategories']); 67 | 68 | expect( 69 | categories[0], 70 | 'категория содержит массив subcategories' 71 | ).to.have.property('subcategories').that.is.an('array'); 72 | 73 | expect( 74 | categories[0].id, 75 | 'идентификатор категории содержит тоже значение, что и в базе' 76 | ).to.equal(category.id); 77 | 78 | expect( 79 | categories[0].subcategories[0].id, 80 | 'идентификатор подкатегории содержит тоже значение, что и в базе' 81 | ).to.equal(category.subcategories[0].id); 82 | }); 83 | }); 84 | 85 | describe('товары', () => { 86 | describe('получение списка товаров по подкатегории', () => { 87 | it('если товары есть в базе - должен вернуться массив с товарами', async () => { 88 | const response = await client.get('http://localhost:3000/api/products'); 89 | 90 | expect( 91 | response.data, 92 | 'ответ сервера содержит массив .products' 93 | ).to.have.property('products').that.is.an('array'); 94 | 95 | expect( 96 | response.data.products, 97 | 'массив должен содержать существующие продукты в базе', 98 | ).to.be.lengthOf(1); 99 | expect( 100 | response.data.products[0], 101 | 'id должен соответствовать id созданного продукта', 102 | ).to.have.property('id', product.id); 103 | }); 104 | 105 | it('если товаров не найдено - должен возвращаться пустой массив', async () => { 106 | const response = await client.get('http://localhost:3000/api/products', { 107 | params: {subcategory: (new ObjectId()).toString()}, 108 | }); 109 | 110 | expect( 111 | response.data, 112 | 'ответ сервера содержит массив .products' 113 | ).to.have.property('products').that.is.an('array'); 114 | 115 | expect( 116 | response.data.products, 117 | 'массив пустой' 118 | ).to.be.empty; 119 | }); 120 | 121 | it('товары по существующей подкатегории', async () => { 122 | const response = await client.get('http://localhost:3000/api/products', { 123 | params: {subcategory: category.subcategories[0].id}, 124 | }); 125 | 126 | expect( 127 | response.data, 128 | 'ответ сервера содержит массив .products' 129 | ).to.have.property('products').that.is.an('array'); 130 | 131 | const products = response.data.products; 132 | 133 | expect( 134 | products[0], 135 | 'товар содержит поля title, id, category, subcategory, price, description и images' 136 | ).to.have.keys([ 137 | 'title', 'id', 'category', 'subcategory', 'price', 'description', 'images', 138 | ]); 139 | 140 | expect( 141 | products[0].id, 142 | 'идентификатор товара содержит тоже значение, что и в базе' 143 | ).to.equal(product.id); 144 | }); 145 | }); 146 | 147 | describe('получение товара по идентификатору', () => { 148 | it('сервер должен вернуть ошибку если идентификатор невалидный', async () => { 149 | const response = await client.get('http://localhost:3000/api/products/invalid-id'); 150 | expect(response.status).to.equal(400); 151 | }); 152 | 153 | it('сервер должен вернуть статус 404', async () => { 154 | const response = await client 155 | .get('http://localhost:3000/api/products/5d208f60e13792398c2aa944'); 156 | 157 | expect(response.status).to.equal(404); 158 | }); 159 | 160 | it('сервер должен вернуть товар по его айди', async () => { 161 | const response = await client.get(`http://localhost:3000/api/products/${product.id}`); 162 | 163 | expect( 164 | response.data, 165 | 'ответ сервера содержит ключ .product' 166 | ).to.have.property('product'); 167 | 168 | expect(response.data.product.id).to.equal(product.id); 169 | }); 170 | }); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /05-http-server-streams/02-file-server-post/test/server.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../server'); 2 | const expect = require('chai').expect; 3 | const fse = require('fs-extra'); 4 | const path = require('path'); 5 | const http = require('http'); 6 | 7 | const filesFolder = path.resolve(__dirname, '../files'); 8 | const fixturesFolder = path.resolve(__dirname, './fixtures'); 9 | 10 | describe('http-server-streams/file-server-post', () => { 11 | describe('тесты на файловый сервер', () => { 12 | before((done) => { 13 | fse.emptyDirSync(filesFolder); 14 | server.listen(3001, done); 15 | }); 16 | 17 | after((done) => { 18 | fse.emptyDirSync(filesFolder); 19 | fse.writeFileSync(path.join(filesFolder, '.gitkeep'), ''); 20 | server.close(done); 21 | }); 22 | 23 | beforeEach(() => { 24 | fse.emptyDirSync(filesFolder); 25 | }); 26 | 27 | describe('POST', () => { 28 | it('возвращается ошибка 409 при создании файла, который есть', (done) => { 29 | fse.copyFileSync( 30 | path.join(fixturesFolder, 'small.png'), 31 | path.join(filesFolder, 'small.png'), 32 | ); 33 | 34 | const mtime = fse.statSync(path.join(filesFolder, 'small.png')).mtime; 35 | 36 | const request = http.request( 37 | 'http://localhost:3001/small.png', 38 | {method: 'POST'}, 39 | (response) => { 40 | const newMtime = fse.statSync(path.join(filesFolder, 'small.png')).mtime; 41 | 42 | expect(response.statusCode, 'статус код ответа 409').to.equal(409); 43 | expect(mtime, 'файл не должен перезаписываться').to.eql(newMtime); 44 | done(); 45 | }); 46 | 47 | request.on('error', done); 48 | fse.createReadStream(path.join(fixturesFolder, 'small.png')).pipe(request); 49 | }); 50 | 51 | it('если тело запроса пустое файл не перезаписывается', (done) => { 52 | fse.copyFileSync( 53 | path.join(fixturesFolder, 'small.png'), 54 | path.join(filesFolder, 'small.png'), 55 | ); 56 | 57 | const mtime = fse.statSync(path.join(filesFolder, 'small.png')).mtime; 58 | 59 | const request = http.request( 60 | 'http://localhost:3001/small.png', 61 | {method: 'POST'}, 62 | (response) => { 63 | const newMtime = fse.statSync(path.join(filesFolder, 'small.png')).mtime; 64 | 65 | expect(response.statusCode, 'статус код ответа сервера 409').to.equal(409); 66 | expect(mtime, 'файл не должен перезаписываться').to.eql(newMtime); 67 | done(); 68 | }); 69 | 70 | request.on('error', done); 71 | request.end(); 72 | }); 73 | 74 | it('при попытке создания слишком большого файла - ошибка 413', (done) => { 75 | const request = http.request( 76 | 'http://localhost:3001/big.png', 77 | {method: 'POST'}, 78 | (response) => { 79 | expect( 80 | response.statusCode, 81 | 'статус код ответа сервера 413' 82 | ).to.equal(413); 83 | 84 | setTimeout(() => { 85 | expect( 86 | fse.existsSync(path.join(filesFolder, 'big.png')), 87 | 'файл big.png не должен оставаться на диске' 88 | ).to.be.false; 89 | done(); 90 | }, 100); 91 | }); 92 | 93 | request.on('error', (err) => { 94 | // EPIPE/ECONNRESET error should occur because we try to pipe after res closed 95 | if (!['ECONNRESET', 'EPIPE'].includes(err.code)) done(err); 96 | }); 97 | 98 | fse.createReadStream(path.join(fixturesFolder, 'big.png')).pipe(request); 99 | }); 100 | 101 | it('успешное создание файла', (done) => { 102 | const request = http.request( 103 | 'http://localhost:3001/small.png', 104 | {method: 'POST'}, 105 | (response) => { 106 | expect( 107 | response.statusCode, 108 | 'статус код ответа сервера 201' 109 | ).to.equal(201); 110 | 111 | expect( 112 | fse.existsSync(path.join(filesFolder, 'small.png')), 113 | 'файл small.png должен быть на диске' 114 | ).to.be.true; 115 | 116 | done(); 117 | }); 118 | 119 | request.on('error', done); 120 | fse.createReadStream(path.join(fixturesFolder, 'small.png')).pipe(request); 121 | }); 122 | 123 | it('файл не должен оставаться на диске при обрыве соединения', (done) => { 124 | const request = http.request( 125 | 'http://localhost:3001/example.txt', 126 | {method: 'POST'}, 127 | (response) => { 128 | expect( 129 | response.statusCode, 130 | 'статус код ответа сервера 201' 131 | ).to.equal(201); 132 | 133 | expect( 134 | fse.existsSync(path.join(filesFolder, 'small.png')), 135 | 'файл small.png должен быть на диске' 136 | ).to.be.true; 137 | 138 | done(); 139 | }); 140 | 141 | request.on('error', (err) => { 142 | if (err.code !== 'ECONNRESET') return done(err); 143 | 144 | setTimeout(() => { 145 | expect( 146 | fse.existsSync(path.join(filesFolder, 'example.txt')), 147 | 'файл example.txt не должен оставаться на диске' 148 | ).to.be.false; 149 | 150 | done(); 151 | }, 100); 152 | }); 153 | 154 | request.on('response', (res) => { 155 | expect.fail('there should be no response'); 156 | }); 157 | 158 | request.write('content'); 159 | 160 | setTimeout(() => { 161 | request.abort(); 162 | }, 300); 163 | }); 164 | 165 | it('если путь вложенный - возвращается ошибка 400', (done) => { 166 | const request = http.request( 167 | 'http://localhost:3001/nested/path', 168 | {method: 'POST'}, 169 | (response) => { 170 | expect(response.statusCode, 'статус код ответа 400').to.equal(400); 171 | done(); 172 | }); 173 | 174 | request.on('error', done); 175 | request.end(); 176 | }); 177 | }); 178 | }); 179 | }); 180 | --------------------------------------------------------------------------------